@wu529778790/open-im 1.8.1-beta.9 → 1.8.2
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 +85 -66
- 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 +109 -7
- package/dist/dingtalk/client.js +11 -3
- package/dist/dingtalk/event-handler.js +7 -3
- package/dist/dingtalk/message-sender.js +13 -0
- package/dist/feishu/event-handler.js +127 -11
- package/dist/index.js +9 -0
- package/dist/manager-control.js +7 -0
- package/dist/qq/client.js +111 -88
- 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.js +81 -4
- package/dist/wechat/event-handler.js +7 -4
- package/dist/wework/client.js +32 -6
- 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';
|
|
@@ -103,15 +130,8 @@ export class ClaudeSDKAdapter {
|
|
|
103
130
|
const abortController = new AbortController();
|
|
104
131
|
let streamClosed = false;
|
|
105
132
|
let actualSessionId;
|
|
133
|
+
let pendingTempId; // 记录临时 ID,用于 abort 时清理
|
|
106
134
|
let runSettled = false;
|
|
107
|
-
let timeoutId = null;
|
|
108
|
-
const timeoutMs = options?.timeoutMs ?? 600_000;
|
|
109
|
-
const clearRunTimeout = () => {
|
|
110
|
-
if (timeoutId !== null) {
|
|
111
|
-
clearTimeout(timeoutId);
|
|
112
|
-
timeoutId = null;
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
135
|
const permissionMode = options?.skipPermissions
|
|
116
136
|
? 'bypassPermissions'
|
|
117
137
|
: options?.permissionMode === 'acceptEdits'
|
|
@@ -120,15 +140,6 @@ export class ClaudeSDKAdapter {
|
|
|
120
140
|
? 'plan'
|
|
121
141
|
: 'default';
|
|
122
142
|
const runSession = async () => {
|
|
123
|
-
timeoutId = setTimeout(() => {
|
|
124
|
-
if (runSettled)
|
|
125
|
-
return;
|
|
126
|
-
runSettled = true;
|
|
127
|
-
clearRunTimeout();
|
|
128
|
-
log.warn(`[ClaudeSDK] Request timeout after ${timeoutMs}ms`);
|
|
129
|
-
abortController.abort();
|
|
130
|
-
callbacks.onError(`请求超时(${Math.round(timeoutMs / 1000)}s),请重试或缩短问题。`);
|
|
131
|
-
}, timeoutMs);
|
|
132
143
|
try {
|
|
133
144
|
// 检查环境变量
|
|
134
145
|
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
|
@@ -139,7 +150,10 @@ export class ClaudeSDKAdapter {
|
|
|
139
150
|
log.info(`[V2] Session: ${sessionId ?? 'new'}, prompt="${prompt.slice(0, 50)}..."`);
|
|
140
151
|
log.info(`[V2] model param=${String(options?.model ?? '')} baseUrl=${process.env.ANTHROPIC_BASE_URL ?? '(default)'}`);
|
|
141
152
|
// 获取或创建会话
|
|
142
|
-
const { session } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode);
|
|
153
|
+
const { session, sessionId: returnedId } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode);
|
|
154
|
+
if (returnedId.startsWith('pending-')) {
|
|
155
|
+
pendingTempId = returnedId;
|
|
156
|
+
}
|
|
143
157
|
// 发送用户消息
|
|
144
158
|
await session.send(prompt);
|
|
145
159
|
// 获取响应流
|
|
@@ -205,7 +219,6 @@ export class ClaudeSDKAdapter {
|
|
|
205
219
|
// 检查会话错误
|
|
206
220
|
if (!success) {
|
|
207
221
|
runSettled = true;
|
|
208
|
-
clearRunTimeout();
|
|
209
222
|
const noConvErr = errs.find((e) => e.includes('No conversation found') || e.includes('session not found'));
|
|
210
223
|
if (noConvErr) {
|
|
211
224
|
log.warn(`Session ${actualSessionId} not found, may need to create new one`);
|
|
@@ -233,25 +246,31 @@ export class ClaudeSDKAdapter {
|
|
|
233
246
|
result.result = accumulated;
|
|
234
247
|
}
|
|
235
248
|
runSettled = true;
|
|
236
|
-
clearRunTimeout();
|
|
237
249
|
callbacks.onComplete(result);
|
|
238
250
|
return;
|
|
239
251
|
}
|
|
240
252
|
}
|
|
241
253
|
// 如果流正常结束但没有收到 result 消息
|
|
242
|
-
if (!streamClosed
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
}
|
|
255
274
|
}
|
|
256
275
|
}
|
|
257
276
|
finally {
|
|
@@ -262,16 +281,15 @@ export class ClaudeSDKAdapter {
|
|
|
262
281
|
catch (err) {
|
|
263
282
|
if (abortController.signal.aborted) {
|
|
264
283
|
log.info('Session run aborted');
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (
|
|
268
|
-
activeSessions.delete(
|
|
269
|
-
log.info(`Cleaned up pending session: ${
|
|
284
|
+
// 清理 pending tempId(abort 可能在 init 消息之前发生)
|
|
285
|
+
const idToClean = actualSessionId ?? pendingTempId;
|
|
286
|
+
if (idToClean?.startsWith('pending-')) {
|
|
287
|
+
activeSessions.delete(idToClean);
|
|
288
|
+
log.info(`Cleaned up pending session: ${idToClean}`);
|
|
270
289
|
}
|
|
271
290
|
return;
|
|
272
291
|
}
|
|
273
292
|
runSettled = true;
|
|
274
|
-
clearRunTimeout();
|
|
275
293
|
const errorObj = err;
|
|
276
294
|
const msg = errorObj.message || String(err);
|
|
277
295
|
log.error(`Claude SDK V2 error: ${msg}`);
|
|
@@ -279,9 +297,10 @@ export class ClaudeSDKAdapter {
|
|
|
279
297
|
log.error(`Error stack: ${errorObj.stack}`);
|
|
280
298
|
}
|
|
281
299
|
// 清理 pending tempId(session 在获取真实 ID 前就失败了)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
300
|
+
const errIdToClean = actualSessionId ?? pendingTempId;
|
|
301
|
+
if (errIdToClean?.startsWith('pending-')) {
|
|
302
|
+
activeSessions.delete(errIdToClean);
|
|
303
|
+
log.info(`Cleaned up pending session after error: ${errIdToClean}`);
|
|
285
304
|
}
|
|
286
305
|
callbacks.onError(msg);
|
|
287
306
|
}
|
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
|
@@ -117,6 +117,7 @@ export function getHealthPlatformSnapshot(file, env = process.env) {
|
|
|
117
117
|
const fileQQ = file.platforms?.qq;
|
|
118
118
|
const fileWework = file.platforms?.wework;
|
|
119
119
|
const fileDingtalk = file.platforms?.dingtalk;
|
|
120
|
+
const fileWorkbuddy = file.platforms?.workbuddy;
|
|
120
121
|
const telegramBotToken = env.TELEGRAM_BOT_TOKEN ?? fileTelegram?.botToken ?? file.telegramBotToken;
|
|
121
122
|
const feishuAppId = env.FEISHU_APP_ID ?? fileFeishu?.appId ?? file.feishuAppId;
|
|
122
123
|
const feishuAppSecret = env.FEISHU_APP_SECRET ?? fileFeishu?.appSecret ?? file.feishuAppSecret;
|
|
@@ -126,6 +127,9 @@ export function getHealthPlatformSnapshot(file, env = process.env) {
|
|
|
126
127
|
const weworkSecret = env.WEWORK_SECRET ?? fileWework?.secret;
|
|
127
128
|
const dingtalkClientId = env.DINGTALK_CLIENT_ID ?? fileDingtalk?.clientId;
|
|
128
129
|
const dingtalkClientSecret = env.DINGTALK_CLIENT_SECRET ?? fileDingtalk?.clientSecret;
|
|
130
|
+
const workbuddyAccessToken = fileWorkbuddy?.accessToken;
|
|
131
|
+
const workbuddyRefreshToken = fileWorkbuddy?.refreshToken;
|
|
132
|
+
const workbuddyUserId = fileWorkbuddy?.userId;
|
|
129
133
|
return {
|
|
130
134
|
telegram: {
|
|
131
135
|
configured: !!telegramBotToken,
|
|
@@ -157,6 +161,12 @@ export function getHealthPlatformSnapshot(file, env = process.env) {
|
|
|
157
161
|
healthy: !!(dingtalkClientId && dingtalkClientSecret),
|
|
158
162
|
message: dingtalkClientId && dingtalkClientSecret ? "Client ID and Secret configured" : "Missing credentials",
|
|
159
163
|
},
|
|
164
|
+
workbuddy: {
|
|
165
|
+
configured: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
|
|
166
|
+
enabled: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) && fileWorkbuddy?.enabled !== false,
|
|
167
|
+
healthy: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
|
|
168
|
+
message: workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId ? "OAuth credentials configured" : "Missing credentials",
|
|
169
|
+
},
|
|
160
170
|
};
|
|
161
171
|
}
|
|
162
172
|
function splitCsv(value) {
|
|
@@ -170,10 +180,20 @@ function clean(value) {
|
|
|
170
180
|
const trimmed = value.trim();
|
|
171
181
|
return trimmed ? trimmed : undefined;
|
|
172
182
|
}
|
|
183
|
+
const MAX_REQUEST_BODY_BYTES = 1 * 1024 * 1024; // 1 MB
|
|
173
184
|
function readJson(request) {
|
|
174
185
|
return new Promise((resolve, reject) => {
|
|
175
186
|
const chunks = [];
|
|
176
|
-
|
|
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
|
+
});
|
|
177
197
|
request.on("end", () => {
|
|
178
198
|
try {
|
|
179
199
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
@@ -190,6 +210,11 @@ function json(response, statusCode, body) {
|
|
|
190
210
|
response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
|
|
191
211
|
response.end(JSON.stringify(body));
|
|
192
212
|
}
|
|
213
|
+
function maskSecret(value) {
|
|
214
|
+
if (!value || value.length <= 4)
|
|
215
|
+
return value ? "****" : "";
|
|
216
|
+
return value.slice(0, 2) + "****" + value.slice(-2);
|
|
217
|
+
}
|
|
193
218
|
function buildInitialPayload(file) {
|
|
194
219
|
// Load Claude settings from ~/.claude/settings.json
|
|
195
220
|
const claudeEnv = loadClaudeSettingsEnv();
|
|
@@ -198,7 +223,7 @@ function buildInitialPayload(file) {
|
|
|
198
223
|
telegram: {
|
|
199
224
|
enabled: file.platforms?.telegram?.enabled ?? Boolean(file.platforms?.telegram?.botToken),
|
|
200
225
|
aiCommand: file.platforms?.telegram?.aiCommand ?? "",
|
|
201
|
-
botToken: file.platforms?.telegram?.botToken
|
|
226
|
+
botToken: maskSecret(file.platforms?.telegram?.botToken),
|
|
202
227
|
proxy: file.platforms?.telegram?.proxy ?? "",
|
|
203
228
|
allowedUserIds: (file.platforms?.telegram?.allowedUserIds ?? []).join(", "),
|
|
204
229
|
},
|
|
@@ -206,31 +231,40 @@ function buildInitialPayload(file) {
|
|
|
206
231
|
enabled: file.platforms?.feishu?.enabled ?? Boolean(file.platforms?.feishu?.appId && file.platforms?.feishu?.appSecret),
|
|
207
232
|
aiCommand: file.platforms?.feishu?.aiCommand ?? "",
|
|
208
233
|
appId: file.platforms?.feishu?.appId ?? "",
|
|
209
|
-
appSecret: file.platforms?.feishu?.appSecret
|
|
234
|
+
appSecret: maskSecret(file.platforms?.feishu?.appSecret),
|
|
210
235
|
allowedUserIds: (file.platforms?.feishu?.allowedUserIds ?? []).join(", "),
|
|
211
236
|
},
|
|
212
237
|
qq: {
|
|
213
238
|
enabled: file.platforms?.qq?.enabled ?? Boolean(file.platforms?.qq?.appId && file.platforms?.qq?.secret),
|
|
214
239
|
aiCommand: file.platforms?.qq?.aiCommand ?? "",
|
|
215
240
|
appId: file.platforms?.qq?.appId ?? "",
|
|
216
|
-
secret: file.platforms?.qq?.secret
|
|
241
|
+
secret: maskSecret(file.platforms?.qq?.secret),
|
|
217
242
|
allowedUserIds: (file.platforms?.qq?.allowedUserIds ?? []).join(", "),
|
|
218
243
|
},
|
|
219
244
|
wework: {
|
|
220
245
|
enabled: file.platforms?.wework?.enabled ?? Boolean(file.platforms?.wework?.corpId && file.platforms?.wework?.secret),
|
|
221
246
|
aiCommand: file.platforms?.wework?.aiCommand ?? "",
|
|
222
247
|
corpId: file.platforms?.wework?.corpId ?? "",
|
|
223
|
-
secret: file.platforms?.wework?.secret
|
|
248
|
+
secret: maskSecret(file.platforms?.wework?.secret),
|
|
224
249
|
allowedUserIds: (file.platforms?.wework?.allowedUserIds ?? []).join(", "),
|
|
225
250
|
},
|
|
226
251
|
dingtalk: {
|
|
227
252
|
enabled: file.platforms?.dingtalk?.enabled ?? Boolean(file.platforms?.dingtalk?.clientId && file.platforms?.dingtalk?.clientSecret),
|
|
228
253
|
aiCommand: file.platforms?.dingtalk?.aiCommand ?? "",
|
|
229
254
|
clientId: file.platforms?.dingtalk?.clientId ?? "",
|
|
230
|
-
clientSecret: file.platforms?.dingtalk?.clientSecret
|
|
255
|
+
clientSecret: maskSecret(file.platforms?.dingtalk?.clientSecret),
|
|
231
256
|
cardTemplateId: file.platforms?.dingtalk?.cardTemplateId ?? "",
|
|
232
257
|
allowedUserIds: (file.platforms?.dingtalk?.allowedUserIds ?? []).join(", "),
|
|
233
258
|
},
|
|
259
|
+
workbuddy: {
|
|
260
|
+
enabled: file.platforms?.workbuddy?.enabled ?? Boolean(file.platforms?.workbuddy?.accessToken && file.platforms?.workbuddy?.refreshToken && file.platforms?.workbuddy?.userId),
|
|
261
|
+
aiCommand: file.platforms?.workbuddy?.aiCommand ?? "",
|
|
262
|
+
accessToken: maskSecret(file.platforms?.workbuddy?.accessToken),
|
|
263
|
+
refreshToken: maskSecret(file.platforms?.workbuddy?.refreshToken),
|
|
264
|
+
userId: file.platforms?.workbuddy?.userId ?? "",
|
|
265
|
+
baseUrl: file.platforms?.workbuddy?.baseUrl ?? "",
|
|
266
|
+
allowedUserIds: (file.platforms?.workbuddy?.allowedUserIds ?? []).join(", "),
|
|
267
|
+
},
|
|
234
268
|
},
|
|
235
269
|
ai: {
|
|
236
270
|
aiCommand: file.aiCommand ?? "claude",
|
|
@@ -239,7 +273,7 @@ function buildInitialPayload(file) {
|
|
|
239
273
|
claudeConfigPath: process.platform === 'win32'
|
|
240
274
|
? getClaudeConfigHome() + "\\.claude\\settings.json"
|
|
241
275
|
: getClaudeConfigHome() + "/.claude/settings.json",
|
|
242
|
-
claudeAuthToken: claudeEnv.ANTHROPIC_AUTH_TOKEN
|
|
276
|
+
claudeAuthToken: maskSecret(claudeEnv.ANTHROPIC_AUTH_TOKEN),
|
|
243
277
|
claudeBaseUrl: claudeEnv.ANTHROPIC_BASE_URL ?? "",
|
|
244
278
|
claudeModel: claudeEnv.ANTHROPIC_MODEL ?? "",
|
|
245
279
|
claudeProxy: file.tools?.claude?.proxy ?? "",
|
|
@@ -276,6 +310,12 @@ function validatePayload(payload) {
|
|
|
276
310
|
errors.push("DingTalk client ID is required.");
|
|
277
311
|
if (payload.platforms.dingtalk.enabled && !clean(payload.platforms.dingtalk.clientSecret))
|
|
278
312
|
errors.push("DingTalk client secret is required.");
|
|
313
|
+
if (payload.platforms.workbuddy.enabled && !clean(payload.platforms.workbuddy.accessToken))
|
|
314
|
+
errors.push("WorkBuddy access token is required.");
|
|
315
|
+
if (payload.platforms.workbuddy.enabled && !clean(payload.platforms.workbuddy.refreshToken))
|
|
316
|
+
errors.push("WorkBuddy refresh token is required.");
|
|
317
|
+
if (payload.platforms.workbuddy.enabled && !clean(payload.platforms.workbuddy.userId))
|
|
318
|
+
errors.push("WorkBuddy user ID is required.");
|
|
279
319
|
if (!clean(payload.ai.claudeWorkDir))
|
|
280
320
|
errors.push("Default work directory is required.");
|
|
281
321
|
if (!Number.isFinite(payload.ai.claudeTimeoutMs) || payload.ai.claudeTimeoutMs <= 0)
|
|
@@ -330,6 +370,17 @@ function validateConfigForPlatform(platform, config) {
|
|
|
330
370
|
errors.push("DingTalk client secret is required and must be a non-empty string.");
|
|
331
371
|
}
|
|
332
372
|
break;
|
|
373
|
+
case "workbuddy":
|
|
374
|
+
if (!c.accessToken || typeof c.accessToken !== "string" || !clean(c.accessToken)) {
|
|
375
|
+
errors.push("WorkBuddy access token is required and must be a non-empty string.");
|
|
376
|
+
}
|
|
377
|
+
if (!c.refreshToken || typeof c.refreshToken !== "string" || !clean(c.refreshToken)) {
|
|
378
|
+
errors.push("WorkBuddy refresh token is required and must be a non-empty string.");
|
|
379
|
+
}
|
|
380
|
+
if (!c.userId || typeof c.userId !== "string" || !clean(c.userId)) {
|
|
381
|
+
errors.push("WorkBuddy user ID is required and must be a non-empty string.");
|
|
382
|
+
}
|
|
383
|
+
break;
|
|
333
384
|
default:
|
|
334
385
|
errors.push(`Unknown platform: ${platform}`);
|
|
335
386
|
}
|
|
@@ -455,6 +506,34 @@ async function probeDingTalk(config) {
|
|
|
455
506
|
}
|
|
456
507
|
return "DingTalk credentials are valid.";
|
|
457
508
|
}
|
|
509
|
+
async function probeWorkBuddy(config) {
|
|
510
|
+
const accessToken = clean(String(config.accessToken ?? ""));
|
|
511
|
+
const refreshToken = clean(String(config.refreshToken ?? ""));
|
|
512
|
+
const userId = clean(String(config.userId ?? ""));
|
|
513
|
+
if (!accessToken || !refreshToken || !userId)
|
|
514
|
+
throw new Error("WorkBuddy access token, refresh token, and user ID are required.");
|
|
515
|
+
const baseUrl = clean(String(config.baseUrl ?? "")) || "https://copilot.tencent.com";
|
|
516
|
+
// Validate credentials by attempting to register workspace
|
|
517
|
+
const response = await fetch(`${baseUrl}/api/copilot/workspace/register`, {
|
|
518
|
+
method: "POST",
|
|
519
|
+
headers: {
|
|
520
|
+
"content-type": "application/json",
|
|
521
|
+
"authorization": `Bearer ${accessToken}`,
|
|
522
|
+
},
|
|
523
|
+
body: JSON.stringify({
|
|
524
|
+
userId,
|
|
525
|
+
hostId: "open-im-test",
|
|
526
|
+
workspaceId: "open-im-test-workspace",
|
|
527
|
+
workspaceName: "OpenIM Test Workspace",
|
|
528
|
+
}),
|
|
529
|
+
signal: AbortSignal.timeout(TEST_TIMEOUT_MS),
|
|
530
|
+
});
|
|
531
|
+
if (!response.ok) {
|
|
532
|
+
const body = await response.text();
|
|
533
|
+
throw new Error(`WorkBuddy authentication failed: ${body.slice(0, 200) || `HTTP ${response.status}`}`);
|
|
534
|
+
}
|
|
535
|
+
return "WorkBuddy credentials are valid.";
|
|
536
|
+
}
|
|
458
537
|
export async function testPlatformConfig(platform, config) {
|
|
459
538
|
const errors = validateConfigForPlatform(platform, config);
|
|
460
539
|
if (errors.length > 0) {
|
|
@@ -471,6 +550,8 @@ export async function testPlatformConfig(platform, config) {
|
|
|
471
550
|
return probeWeWork(config);
|
|
472
551
|
case "dingtalk":
|
|
473
552
|
return probeDingTalk(config);
|
|
553
|
+
case "workbuddy":
|
|
554
|
+
return probeWorkBuddy(config);
|
|
474
555
|
default:
|
|
475
556
|
throw new Error(`Unknown platform: ${platform}`);
|
|
476
557
|
}
|
|
@@ -557,6 +638,16 @@ function toFileConfig(payload, existing) {
|
|
|
557
638
|
cardTemplateId: clean(payload.platforms.dingtalk.cardTemplateId),
|
|
558
639
|
allowedUserIds: splitCsv(payload.platforms.dingtalk.allowedUserIds),
|
|
559
640
|
},
|
|
641
|
+
workbuddy: {
|
|
642
|
+
...existing.platforms?.workbuddy,
|
|
643
|
+
enabled: payload.platforms.workbuddy.enabled,
|
|
644
|
+
aiCommand: clean(payload.platforms.workbuddy.aiCommand),
|
|
645
|
+
accessToken: clean(payload.platforms.workbuddy.accessToken),
|
|
646
|
+
refreshToken: clean(payload.platforms.workbuddy.refreshToken),
|
|
647
|
+
userId: clean(payload.platforms.workbuddy.userId),
|
|
648
|
+
baseUrl: clean(payload.platforms.workbuddy.baseUrl),
|
|
649
|
+
allowedUserIds: splitCsv(payload.platforms.workbuddy.allowedUserIds),
|
|
650
|
+
},
|
|
560
651
|
},
|
|
561
652
|
};
|
|
562
653
|
}
|
|
@@ -812,6 +903,7 @@ export async function startWebConfigServer(options) {
|
|
|
812
903
|
const fileQQ = file.platforms?.qq;
|
|
813
904
|
const fileWework = file.platforms?.wework;
|
|
814
905
|
const fileDingtalk = file.platforms?.dingtalk;
|
|
906
|
+
const fileWorkbuddy = file.platforms?.workbuddy;
|
|
815
907
|
const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN ?? fileTelegram?.botToken ?? file.telegramBotToken;
|
|
816
908
|
const feishuAppId = process.env.FEISHU_APP_ID ?? fileFeishu?.appId ?? file.feishuAppId;
|
|
817
909
|
const feishuAppSecret = process.env.FEISHU_APP_SECRET ?? fileFeishu?.appSecret ?? file.feishuAppSecret;
|
|
@@ -821,6 +913,9 @@ export async function startWebConfigServer(options) {
|
|
|
821
913
|
const weworkSecret = process.env.WEWORK_SECRET ?? fileWework?.secret;
|
|
822
914
|
const dingtalkClientId = process.env.DINGTALK_CLIENT_ID ?? fileDingtalk?.clientId;
|
|
823
915
|
const dingtalkClientSecret = process.env.DINGTALK_CLIENT_SECRET ?? fileDingtalk?.clientSecret;
|
|
916
|
+
const workbuddyAccessToken = fileWorkbuddy?.accessToken;
|
|
917
|
+
const workbuddyRefreshToken = fileWorkbuddy?.refreshToken;
|
|
918
|
+
const workbuddyUserId = fileWorkbuddy?.userId;
|
|
824
919
|
const platforms = {};
|
|
825
920
|
// 检查 Telegram
|
|
826
921
|
platforms.telegram = {
|
|
@@ -857,6 +952,13 @@ export async function startWebConfigServer(options) {
|
|
|
857
952
|
healthy: !!(dingtalkClientId && dingtalkClientSecret),
|
|
858
953
|
message: (dingtalkClientId && dingtalkClientSecret) ? "Client ID and Secret configured" : "Missing credentials"
|
|
859
954
|
};
|
|
955
|
+
// 检查 WorkBuddy
|
|
956
|
+
platforms.workbuddy = {
|
|
957
|
+
configured: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
|
|
958
|
+
enabled: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) && fileWorkbuddy?.enabled !== false,
|
|
959
|
+
healthy: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
|
|
960
|
+
message: (workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) ? "OAuth credentials configured" : "Missing credentials"
|
|
961
|
+
};
|
|
860
962
|
json(response, 200, { platforms, serviceStatus: getServiceStatus() });
|
|
861
963
|
return;
|
|
862
964
|
}
|
package/dist/dingtalk/client.js
CHANGED
|
@@ -7,7 +7,9 @@ const TEXT_MSG_KEY = 'sampleText';
|
|
|
7
7
|
const DINGTALK_STREAM_HOST = 'wss-open-connection.dingtalk.com';
|
|
8
8
|
let client = null;
|
|
9
9
|
let messageHandler = null;
|
|
10
|
+
// sessionWebhook 有过期时间(约 2 小时),需要记录时间戳
|
|
10
11
|
const sessionWebhookByChat = new Map();
|
|
12
|
+
const WEBHOOK_TTL_MS = 90 * 60 * 1000; // 90 分钟后视为过期
|
|
11
13
|
const unionIdByUserId = new Map();
|
|
12
14
|
let dingtalkWarnFilterInstalled = false;
|
|
13
15
|
export function shouldSuppressDingTalkSocketWarn(args) {
|
|
@@ -44,13 +46,19 @@ function getClient() {
|
|
|
44
46
|
export function registerSessionWebhook(chatId, sessionWebhook) {
|
|
45
47
|
if (!chatId || !sessionWebhook)
|
|
46
48
|
return;
|
|
47
|
-
sessionWebhookByChat.set(chatId, sessionWebhook);
|
|
49
|
+
sessionWebhookByChat.set(chatId, { webhook: sessionWebhook, registeredAt: Date.now() });
|
|
48
50
|
}
|
|
49
51
|
async function sendByWebhook(chatId, body) {
|
|
50
|
-
const
|
|
51
|
-
if (!
|
|
52
|
+
const entry = sessionWebhookByChat.get(chatId);
|
|
53
|
+
if (!entry) {
|
|
52
54
|
throw new Error(`DingTalk sessionWebhook unavailable for chat ${chatId}`);
|
|
53
55
|
}
|
|
56
|
+
// 检查 webhook 是否过期
|
|
57
|
+
if (Date.now() - entry.registeredAt > WEBHOOK_TTL_MS) {
|
|
58
|
+
sessionWebhookByChat.delete(chatId);
|
|
59
|
+
throw new Error(`DingTalk sessionWebhook expired for chat ${chatId}`);
|
|
60
|
+
}
|
|
61
|
+
const sessionWebhook = entry.webhook;
|
|
54
62
|
const accessToken = await getClient().getAccessToken();
|
|
55
63
|
const res = await fetch(sessionWebhook, {
|
|
56
64
|
method: 'POST',
|
|
@@ -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);
|