@dingtalk-real-ai/dingtalk-connector 0.8.1 → 0.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/CHANGELOG.md +20 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/channel.ts +51 -51
- package/src/config/accounts.ts +13 -1
- package/src/core/connection.ts +22 -25
- package/src/core/message-handler.ts +61 -88
- package/src/core/provider.ts +2 -1
- package/src/onboarding.ts +91 -31
- package/src/services/messaging.ts +2 -2
- package/src/utils/logger.ts +3 -3
- package/src/utils/session.ts +7 -2
- package/src/utils/utils-legacy.ts +4 -101
- package/test-dingtalk-connection.mjs +0 -105
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.8.2] - 2026-03-22
|
|
9
|
+
|
|
10
|
+
### 修复 / Fixes
|
|
11
|
+
- 🐛 **多账号重复启动问题** - 修复 `enabled: false` 的账号仍会建立 WebSocket 连接的问题,禁用账号现在正确保持 pending 状态直到 Gateway 停止
|
|
12
|
+
**Multi-account duplicate startup** - Fixed accounts with `enabled: false` still establishing WebSocket connections; disabled accounts now correctly remain in a pending state until the Gateway stops
|
|
13
|
+
|
|
14
|
+
- 🐛 **相同 clientId 账号去重** - 修复多个账号配置相同 `clientId` 时建立重复连接的问题,通过静态配置分析确保同一 `clientId` 只有列表中第一个启用账号建立连接
|
|
15
|
+
**Duplicate clientId deduplication** - Fixed duplicate connections when multiple accounts share the same `clientId`; static config analysis now ensures only the first enabled account per `clientId` establishes a connection
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### 改进 / Improvements
|
|
19
|
+
- ✅ **Onboarding 配置向导优化** - 改进钉钉连接器配置引导逻辑,调整凭据输入顺序(先 Client ID 后 Client Secret),优化引导文案
|
|
20
|
+
**Onboarding wizard improvement** - Improved DingTalk connector onboarding flow, adjusted credential input order (Client ID first, then Client Secret), and refined guidance text
|
|
21
|
+
|
|
22
|
+
- ✅ **会话 Key 遵循 OpenClaw 规范** - 会话上下文按 OpenClaw 标准规则构建,通过 `channel`、`accountId`、`chatType`、`peerId` 唯一标识会话,支持 `sharedMemoryAcrossConversations` 跨会话记忆共享
|
|
23
|
+
**Session key follows OpenClaw convention** - Session context now built per OpenClaw standard rules, uniquely identified via `channel`, `accountId`, `chatType`, `peerId`; supports `sharedMemoryAcrossConversations` for cross-conversation memory sharing
|
|
24
|
+
|
|
25
|
+
- ✅ **消息处理逻辑优化** - 重构消息处理流程,提升消息响应速度和处理可靠性,确保消息按序正确处理
|
|
26
|
+
**Message processing logic optimization** - Refactored message processing flow to improve response speed and reliability, ensuring messages are processed correctly in order
|
|
27
|
+
|
|
8
28
|
## [0.8.1] - 2026-03-20
|
|
9
29
|
|
|
10
30
|
### 修复 / Fixes
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -495,64 +495,64 @@ export const dingtalkPlugin: ChannelPlugin<ResolvedDingtalkAccount> = {
|
|
|
495
495
|
gateway: {
|
|
496
496
|
startAccount: async (ctx) => {
|
|
497
497
|
const account = resolveDingtalkAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
|
|
498
|
-
|
|
498
|
+
|
|
499
|
+
// 检查账号是否启用和配置
|
|
500
|
+
if (!account.enabled) {
|
|
501
|
+
ctx.log?.info?.(`dingtalk-connector[${ctx.accountId}] is disabled, skipping startup`);
|
|
502
|
+
// 返回一个永不 resolve 的 Promise,保持 pending 状态直到 abort
|
|
503
|
+
return new Promise<void>((resolve) => {
|
|
504
|
+
if (ctx.abortSignal?.aborted) {
|
|
505
|
+
resolve();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
ctx.abortSignal?.addEventListener('abort', () => resolve(), { once: true });
|
|
509
|
+
});
|
|
510
|
+
}
|
|
499
511
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
// 使用直接属性访问而不是解构
|
|
527
|
-
const monitorDingtalkProvider = monitorModule.monitorDingtalkProvider;
|
|
528
|
-
logger.info(`解构 monitorDingtalkProvider 完成: ${typeof monitorDingtalkProvider}`);
|
|
529
|
-
|
|
530
|
-
if (!monitorDingtalkProvider) {
|
|
531
|
-
ctx.log?.error?.(`monitorDingtalkProvider 未找到!可用导出: ${Object.keys(monitorModule).join(', ')}`);
|
|
532
|
-
throw new Error("monitorDingtalkProvider not found in monitor module");
|
|
512
|
+
if (!account.configured) {
|
|
513
|
+
throw new Error(`DingTalk account "${ctx.accountId}" is not properly configured`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 去重检查:如果列表中排在当前账号之前的账号已使用相同 clientId,则跳过当前账号
|
|
517
|
+
// 使用静态配置分析(而非运行时状态),避免并发竞态条件
|
|
518
|
+
// 规则:同一 clientId 只有列表中第一个启用且已配置的账号才会建立连接
|
|
519
|
+
if (account.clientId) {
|
|
520
|
+
const clientId = String(account.clientId);
|
|
521
|
+
const allAccountIds = listDingtalkAccountIds(ctx.cfg);
|
|
522
|
+
const currentIndex = allAccountIds.indexOf(ctx.accountId);
|
|
523
|
+
const priorAccountWithSameClientId = allAccountIds.slice(0, currentIndex).find((otherId) => {
|
|
524
|
+
const other = resolveDingtalkAccount({ cfg: ctx.cfg, accountId: otherId });
|
|
525
|
+
return other.enabled && other.configured && other.clientId && String(other.clientId) === clientId;
|
|
526
|
+
});
|
|
527
|
+
if (priorAccountWithSameClientId) {
|
|
528
|
+
ctx.log?.info?.(
|
|
529
|
+
`dingtalk-connector[${ctx.accountId}] skipped: clientId "${clientId.substring(0, 8)}..." is already used by account "${priorAccountWithSameClientId}"`
|
|
530
|
+
);
|
|
531
|
+
return new Promise<void>((resolve) => {
|
|
532
|
+
if (ctx.abortSignal?.aborted) {
|
|
533
|
+
resolve();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
ctx.abortSignal?.addEventListener('abort', () => resolve(), { once: true });
|
|
537
|
+
});
|
|
533
538
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
logger.info(`准备调用 monitorDingtalkProvider`);
|
|
543
|
-
|
|
544
|
-
const result = await monitorDingtalkProvider({
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
ctx.setStatus({ accountId: ctx.accountId, port: null });
|
|
542
|
+
ctx.log?.info(
|
|
543
|
+
`starting dingtalk-connector[${ctx.accountId}] (mode: stream)`,
|
|
544
|
+
);
|
|
545
|
+
try {
|
|
546
|
+
return await monitorDingtalkProvider({
|
|
545
547
|
config: ctx.cfg,
|
|
546
548
|
runtime: ctx.runtime,
|
|
547
549
|
abortSignal: ctx.abortSignal,
|
|
548
550
|
accountId: ctx.accountId,
|
|
549
551
|
});
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
ctx.log?.error?.(`错误堆栈: ${error.stack}`);
|
|
555
|
-
throw error;
|
|
552
|
+
} catch (err: any) {
|
|
553
|
+
// 打印真实错误到 stderr,绕过框架 log 系统(框架的 runtime.log 可能未初始化)
|
|
554
|
+
ctx.log?.error(`[dingtalk-connector][${ctx.accountId}] startAccount error:`, err?.message ?? err, err?.stack);
|
|
555
|
+
throw err;
|
|
556
556
|
}
|
|
557
557
|
},
|
|
558
558
|
},
|
package/src/config/accounts.ts
CHANGED
|
@@ -222,9 +222,21 @@ export function resolveDingtalkAccount(params: {
|
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
224
|
* List all enabled and configured accounts.
|
|
225
|
+
* Deduplicates by clientId to avoid creating multiple connections with the same credentials.
|
|
225
226
|
*/
|
|
226
227
|
export function listEnabledDingtalkAccounts(cfg: ClawdbotConfig): ResolvedDingtalkAccount[] {
|
|
227
|
-
|
|
228
|
+
const accounts = listDingtalkAccountIds(cfg)
|
|
228
229
|
.map((accountId) => resolveDingtalkAccount({ cfg, accountId }))
|
|
229
230
|
.filter((account) => account.enabled && account.configured);
|
|
231
|
+
|
|
232
|
+
// Deduplicate by clientId to avoid multiple connections with same credentials
|
|
233
|
+
const seen = new Set<string>();
|
|
234
|
+
return accounts.filter((account) => {
|
|
235
|
+
if (!account.clientId) return true;
|
|
236
|
+
if (seen.has(account.clientId)) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
seen.add(account.clientId);
|
|
240
|
+
return true;
|
|
241
|
+
});
|
|
230
242
|
}
|
package/src/core/connection.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - 连接统计和监控(每分钟输出)
|
|
14
14
|
*/
|
|
15
15
|
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
16
|
-
import type {
|
|
16
|
+
import type { ResolvedDingtalkAccount } from "../types/index.ts";
|
|
17
17
|
import {
|
|
18
18
|
isMessageProcessed,
|
|
19
19
|
markMessageProcessed,
|
|
@@ -30,14 +30,6 @@ export type DingtalkReactionCreatedEvent = {
|
|
|
30
30
|
emoji: string;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
export type MonitorDingtalkAccountOpts = {
|
|
34
|
-
cfg: ClawdbotConfig;
|
|
35
|
-
account: ResolvedDingtalkAccount;
|
|
36
|
-
runtime?: RuntimeEnv;
|
|
37
|
-
abortSignal?: AbortSignal;
|
|
38
|
-
messageHandler: MessageHandler; // 直接传入消息处理器
|
|
39
|
-
};
|
|
40
|
-
|
|
41
33
|
// 消息处理器函数类型
|
|
42
34
|
export type MessageHandler = (params: {
|
|
43
35
|
accountId: string;
|
|
@@ -49,6 +41,14 @@ export type MessageHandler = (params: {
|
|
|
49
41
|
cfg: ClawdbotConfig;
|
|
50
42
|
}) => Promise<void>;
|
|
51
43
|
|
|
44
|
+
export type MonitorDingtalkAccountOpts = {
|
|
45
|
+
cfg: ClawdbotConfig;
|
|
46
|
+
account: ResolvedDingtalkAccount;
|
|
47
|
+
runtime?: RuntimeEnv;
|
|
48
|
+
abortSignal?: AbortSignal;
|
|
49
|
+
messageHandler: MessageHandler;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
52
|
// ============ 连接配置 ============
|
|
53
53
|
|
|
54
54
|
/** 心跳间隔(毫秒) */
|
|
@@ -272,7 +272,7 @@ export async function monitorSingleAccount(
|
|
|
272
272
|
logger.info(`✅ 重连成功 (socket 状态=${client.socket?.readyState})`);
|
|
273
273
|
} catch (err: any) {
|
|
274
274
|
reconnectAttempts++;
|
|
275
|
-
|
|
275
|
+
logger.error(
|
|
276
276
|
`重连失败:${err.message} (尝试 ${reconnectAttempts})`,
|
|
277
277
|
);
|
|
278
278
|
throw err;
|
|
@@ -298,7 +298,7 @@ export async function monitorSingleAccount(
|
|
|
298
298
|
if (!isStopped && !isReconnecting) {
|
|
299
299
|
// 立即重连,不退避
|
|
300
300
|
doReconnect(true).catch((err) => {
|
|
301
|
-
|
|
301
|
+
logger.error(`[${accountId}] 重连失败:${err.message}`);
|
|
302
302
|
});
|
|
303
303
|
}
|
|
304
304
|
}
|
|
@@ -322,7 +322,7 @@ export async function monitorSingleAccount(
|
|
|
322
322
|
// 立即重连,不退避
|
|
323
323
|
setTimeout(() => {
|
|
324
324
|
doReconnect(true).catch((err) => {
|
|
325
|
-
|
|
325
|
+
logger.error(`重连失败:${err.message}`);
|
|
326
326
|
});
|
|
327
327
|
}, 0);
|
|
328
328
|
});
|
|
@@ -388,11 +388,11 @@ export async function monitorSingleAccount(
|
|
|
388
388
|
lastSocketAvailableTime = Date.now();
|
|
389
389
|
logger.debug(`💓 发送 PING 心跳成功`);
|
|
390
390
|
} catch (err: any) {
|
|
391
|
-
|
|
391
|
+
logger.warn(`发送 PING 失败:${err.message}`);
|
|
392
392
|
// 发送失败也计入超时
|
|
393
393
|
}
|
|
394
394
|
} catch (err: any) {
|
|
395
|
-
|
|
395
|
+
logger.error(`keepAlive 检测失败:${err.message}`);
|
|
396
396
|
}
|
|
397
397
|
}, HEARTBEAT_INTERVAL); // 每 10 秒检测一次
|
|
398
398
|
|
|
@@ -445,7 +445,7 @@ export async function monitorSingleAccount(
|
|
|
445
445
|
await client.disconnect();
|
|
446
446
|
}
|
|
447
447
|
} catch (err: any) {
|
|
448
|
-
|
|
448
|
+
logger.warn(`断开连接时出错:${err.message}`);
|
|
449
449
|
}
|
|
450
450
|
resolve();
|
|
451
451
|
};
|
|
@@ -486,12 +486,13 @@ export async function monitorSingleAccount(
|
|
|
486
486
|
client.socketCallBackResponse(messageId, { success: true });
|
|
487
487
|
logger.info(`✅ 已立即确认回调:messageId=${messageId}`);
|
|
488
488
|
} else {
|
|
489
|
-
|
|
489
|
+
logger.warn(`⚠️ 警告:消息没有 messageId`);
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
// 消息去重
|
|
493
493
|
if (messageId && isMessageProcessed(messageId)) {
|
|
494
|
-
|
|
494
|
+
processedCount++; // ✅ 修复:重复消息也要计入 processedCount
|
|
495
|
+
logger.warn(`⚠️ 检测到重复消息,跳过处理:messageId=${messageId} (${processedCount}/${receivedCount})`);
|
|
495
496
|
logger.info(`========== 消息处理结束(重复) ==========\n`);
|
|
496
497
|
return;
|
|
497
498
|
}
|
|
@@ -579,13 +580,9 @@ export async function monitorSingleAccount(
|
|
|
579
580
|
const errorMsg = `❌ 处理消息异常 (${processedCount}/${receivedCount}): ${error?.message || "未知错误"}`;
|
|
580
581
|
const errorStack = error?.stack || "无堆栈信息";
|
|
581
582
|
|
|
582
|
-
// 使用 logger
|
|
583
|
-
logger.
|
|
584
|
-
logger.
|
|
585
|
-
|
|
586
|
-
// 同时使用 log?.error 记录(如果可用)
|
|
587
|
-
log?.error?.(errorMsg);
|
|
588
|
-
log?.error?.(`错误堆栈:\n${errorStack}`);
|
|
583
|
+
// 使用 logger 记录错误信息
|
|
584
|
+
logger.error(errorMsg);
|
|
585
|
+
logger.error(`错误堆栈:\n${errorStack}`);
|
|
589
586
|
|
|
590
587
|
logger.info(`========== 消息处理结束(失败) ==========\n`);
|
|
591
588
|
} finally {
|
|
@@ -672,7 +669,7 @@ export async function monitorSingleAccount(
|
|
|
672
669
|
// client.on('close', ...) - 已移除,使用 setupCloseListener
|
|
673
670
|
|
|
674
671
|
client.on("error", (err: Error) => {
|
|
675
|
-
|
|
672
|
+
logger.error(`Connection error: ${err.message}`);
|
|
676
673
|
});
|
|
677
674
|
|
|
678
675
|
// 监听重连事件(仅用于日志,实际重连由自定义逻辑处理)
|
|
@@ -44,6 +44,7 @@ import { sendProactive, type AICardTarget } from "../services/messaging/index.ts
|
|
|
44
44
|
import { createDingtalkReplyDispatcher, normalizeSlashCommand } from "../reply-dispatcher.ts";
|
|
45
45
|
import { getDingtalkRuntime } from "../runtime.ts";
|
|
46
46
|
import { dingtalkHttp } from '../utils/http-client.ts';
|
|
47
|
+
import { createLoggerFromConfig } from '../utils/logger.ts';
|
|
47
48
|
import * as fs from 'fs';
|
|
48
49
|
import * as path from 'path';
|
|
49
50
|
import * as os from 'os';
|
|
@@ -537,7 +538,10 @@ interface HandleMessageParams {
|
|
|
537
538
|
* 内部消息处理函数(实际执行消息处理逻辑)
|
|
538
539
|
*/
|
|
539
540
|
export async function handleDingTalkMessageInternal(params: HandleMessageParams): Promise<void> {
|
|
540
|
-
const { accountId, config, data, sessionWebhook, runtime, log, cfg } = params;
|
|
541
|
+
const { accountId, config, data, sessionWebhook, runtime, log: inputLog, cfg } = params;
|
|
542
|
+
|
|
543
|
+
// 如果传入的 log 为空,则使用基于 config 的 logger
|
|
544
|
+
const log = createLoggerFromConfig(config, `DingTalk:${accountId}`);
|
|
541
545
|
|
|
542
546
|
const content = extractMessageContent(data);
|
|
543
547
|
if (!content.text && content.imageUrls.length === 0 && content.downloadCodes.length === 0) return;
|
|
@@ -546,32 +550,7 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
546
550
|
const senderId = data.senderStaffId || data.senderId;
|
|
547
551
|
const senderName = data.senderNick || 'Unknown';
|
|
548
552
|
|
|
549
|
-
|
|
550
|
-
const chatType = isDirect ? "direct" : "group";
|
|
551
|
-
const peerId = isDirect ? senderId : data.conversationId;
|
|
552
|
-
|
|
553
|
-
// 手动匹配 bindings 获取 agentId
|
|
554
|
-
let matchedAgentId: string | null = null;
|
|
555
|
-
if (cfg.bindings && cfg.bindings.length > 0) {
|
|
556
|
-
for (const binding of cfg.bindings) {
|
|
557
|
-
const match = binding.match;
|
|
558
|
-
if (match.channel && match.channel !== "dingtalk-connector") continue;
|
|
559
|
-
if (match.accountId && match.accountId !== accountId) continue;
|
|
560
|
-
if (match.peer) {
|
|
561
|
-
if (match.peer.kind && match.peer.kind !== chatType) continue;
|
|
562
|
-
if (match.peer.id && match.peer.id !== '*' && match.peer.id !== peerId) continue;
|
|
563
|
-
}
|
|
564
|
-
matchedAgentId = binding.agentId;
|
|
565
|
-
break;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
if (!matchedAgentId) {
|
|
569
|
-
matchedAgentId = cfg.defaultAgent || 'main';
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// 获取 Agent 工作空间路径
|
|
573
|
-
const agentWorkspaceDir = resolveAgentWorkspaceDir(cfg, matchedAgentId);
|
|
574
|
-
log?.info?.(`Agent 工作空间路径: ${agentWorkspaceDir}`);
|
|
553
|
+
|
|
575
554
|
|
|
576
555
|
|
|
577
556
|
|
|
@@ -716,6 +695,29 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
716
695
|
sharedMemoryAcrossConversations: config.sharedMemoryAcrossConversations,
|
|
717
696
|
});
|
|
718
697
|
|
|
698
|
+
// ===== 解析 agentId 和工作空间路径(在 sessionContext 之后,确保 peerId 与会话隔离策略一致)=====
|
|
699
|
+
// 使用 sessionContext.peerId 进行匹配,与后续 sessionKey 构建保持一致,避免两次匹配结果不一致
|
|
700
|
+
let matchedAgentId: string | null = null;
|
|
701
|
+
if (cfg.bindings && cfg.bindings.length > 0) {
|
|
702
|
+
for (const binding of cfg.bindings) {
|
|
703
|
+
const match = binding.match;
|
|
704
|
+
if (match.channel && match.channel !== "dingtalk-connector") continue;
|
|
705
|
+
if (match.accountId && match.accountId !== accountId) continue;
|
|
706
|
+
if (match.peer) {
|
|
707
|
+
if (match.peer.kind && match.peer.kind !== sessionContext.chatType) continue;
|
|
708
|
+
if (match.peer.id && match.peer.id !== '*' && match.peer.id !== sessionContext.peerId) continue;
|
|
709
|
+
}
|
|
710
|
+
matchedAgentId = binding.agentId;
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (!matchedAgentId) {
|
|
715
|
+
matchedAgentId = cfg.defaultAgent || 'main';
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// 获取 Agent 工作空间路径
|
|
719
|
+
const agentWorkspaceDir = resolveAgentWorkspaceDir(cfg, matchedAgentId);
|
|
720
|
+
log?.info?.(`Agent 工作空间路径: ${agentWorkspaceDir}`);
|
|
719
721
|
|
|
720
722
|
// 构建消息内容
|
|
721
723
|
// ✅ 使用 normalizeSlashCommand 归一化新会话命令
|
|
@@ -940,65 +942,24 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
940
942
|
body: finalContent,
|
|
941
943
|
});
|
|
942
944
|
|
|
943
|
-
//
|
|
944
|
-
const
|
|
945
|
-
const peerId = isDirect ? senderId : data.conversationId;
|
|
946
|
-
|
|
947
|
-
// 手动匹配 bindings(支持通配符 *)
|
|
948
|
-
let matchedAgentId: string | null = null;
|
|
949
|
-
let matchedBy = 'default';
|
|
950
|
-
|
|
951
|
-
if (cfg.bindings && cfg.bindings.length > 0) {
|
|
952
|
-
for (const binding of cfg.bindings) {
|
|
953
|
-
const match = binding.match;
|
|
954
|
-
|
|
955
|
-
// 检查 channel
|
|
956
|
-
if (match.channel && match.channel !== "dingtalk-connector") {
|
|
957
|
-
continue;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// 检查 accountId
|
|
961
|
-
if (match.accountId && match.accountId !== accountId) {
|
|
962
|
-
continue;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
// 检查 peer
|
|
966
|
-
if (match.peer) {
|
|
967
|
-
// 检查 peer.kind
|
|
968
|
-
if (match.peer.kind && match.peer.kind !== sessionContext.chatType) {
|
|
969
|
-
continue;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
// ✅ 使用 sessionContext.peerId 进行匹配(支持通配符 *)
|
|
973
|
-
if (match.peer.id && match.peer.id !== '*' && match.peer.id !== sessionContext.peerId) {
|
|
974
|
-
continue;
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
// 匹配成功
|
|
979
|
-
matchedAgentId = binding.agentId;
|
|
980
|
-
matchedBy = 'binding';
|
|
981
|
-
break;
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
// 如果没有匹配到,使用默认 agent
|
|
986
|
-
if (!matchedAgentId) {
|
|
987
|
-
matchedAgentId = cfg.defaultAgent || 'main';
|
|
988
|
-
log?.info?.(`未匹配到 binding,使用默认 agent: ${matchedAgentId}`);
|
|
989
|
-
}
|
|
945
|
+
// matchedAgentId 已在 sessionContext 构建之后通过 bindings 匹配确定,此处直接使用
|
|
946
|
+
const matchedBy = matchedAgentId !== (cfg.defaultAgent || 'main') ? 'binding' : 'default';
|
|
990
947
|
|
|
991
948
|
// ✅ 使用 SDK 标准方法构建 sessionKey,符合 OpenClaw 规范
|
|
992
949
|
// 格式:agent:{agentId}:{channel}:{peerKind}:{peerId}
|
|
993
950
|
// ✅ 修复:使用 sessionContext.peerId,确保会话隔离配置生效
|
|
951
|
+
// ✅ 关键修复:传递 dmScope 参数,让 SDK 使用配置文件中的 session.dmScope 设置
|
|
952
|
+
const dmScope = cfg.session?.dmScope || 'per-channel-peer';
|
|
953
|
+
log?.info?.(`🔍 构建 sessionKey 前的参数: agentId=${matchedAgentId}, channel=dingtalk-connector, accountId=${accountId}, chatType=${sessionContext.chatType}, peerId=${sessionContext.peerId}, dmScope=${dmScope}`);
|
|
994
954
|
const sessionKey = core.channel.routing.buildAgentSessionKey({
|
|
995
955
|
agentId: matchedAgentId,
|
|
996
|
-
channel: 'dingtalk', // ✅ 使用 'dingtalk' 而不是 'dingtalk
|
|
956
|
+
channel: 'dingtalk-connector', // ✅ 使用 'dingtalk-connector' 而不是 'dingtalk'
|
|
997
957
|
accountId: accountId,
|
|
998
958
|
peer: {
|
|
999
959
|
kind: sessionContext.chatType, // ✅ 使用 sessionContext.chatType
|
|
1000
960
|
id: sessionContext.peerId, // ✅ 使用 sessionContext.peerId(包含会话隔离逻辑)
|
|
1001
961
|
},
|
|
962
|
+
dmScope: dmScope, // ✅ 传递 dmScope 参数,确保生成完整格式的 sessionKey
|
|
1002
963
|
});
|
|
1003
964
|
log?.info?.(`路由解析完成: agentId=${matchedAgentId}, sessionKey=${sessionKey}, matchedBy=${matchedBy}`);
|
|
1004
965
|
|
|
@@ -1018,7 +979,7 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
1018
979
|
To: toField, // ✅ 修复:单聊用 senderId,群聊用 conversationId
|
|
1019
980
|
SessionKey: sessionKey, // ✅ 使用手动匹配的 sessionKey
|
|
1020
981
|
AccountId: accountId,
|
|
1021
|
-
ChatType: chatType,
|
|
982
|
+
ChatType: sessionContext.chatType,
|
|
1022
983
|
GroupSubject: isDirect ? undefined : data.conversationId,
|
|
1023
984
|
SenderName: senderId,
|
|
1024
985
|
SenderId: senderId,
|
|
@@ -1201,23 +1162,33 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
1201
1162
|
* 确保同一会话+agent的消息按顺序处理,避免并发冲突
|
|
1202
1163
|
*/
|
|
1203
1164
|
export async function handleDingTalkMessage(params: HandleMessageParams): Promise<void> {
|
|
1204
|
-
const { accountId, data, log, cfg } = params;
|
|
1165
|
+
const { accountId, config, data, log, cfg } = params;
|
|
1205
1166
|
|
|
1206
|
-
//
|
|
1167
|
+
// 使用 buildSessionContext 构建会话标识,与 handleDingTalkMessageInternal 保持一致
|
|
1168
|
+
// 确保 queueKey 的隔离策略(groupSessionScope、sharedMemoryAcrossConversations)与 sessionKey 一致
|
|
1207
1169
|
const isDirect = data.conversationType === '1';
|
|
1208
1170
|
const senderId = data.senderStaffId || data.senderId;
|
|
1209
1171
|
const conversationId = data.conversationId;
|
|
1210
|
-
|
|
1172
|
+
|
|
1173
|
+
const queueSessionContext = buildSessionContext({
|
|
1174
|
+
accountId,
|
|
1175
|
+
senderId,
|
|
1176
|
+
conversationType: data.conversationType,
|
|
1177
|
+
conversationId,
|
|
1178
|
+
separateSessionByConversation: config.separateSessionByConversation,
|
|
1179
|
+
groupSessionScope: config.groupSessionScope,
|
|
1180
|
+
sharedMemoryAcrossConversations: config.sharedMemoryAcrossConversations,
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
const baseSessionId = queueSessionContext.peerId;
|
|
1211
1184
|
|
|
1212
1185
|
if (!baseSessionId) {
|
|
1213
1186
|
log?.warn?.('无法构建会话标识,跳过队列管理');
|
|
1214
1187
|
return handleDingTalkMessageInternal(params);
|
|
1215
1188
|
}
|
|
1216
|
-
|
|
1217
|
-
// 解析 agentId(与消息处理逻辑保持一致)
|
|
1218
|
-
const chatType = isDirect ? "direct" : "group";
|
|
1219
|
-
const peerId = isDirect ? senderId : conversationId;
|
|
1220
1189
|
|
|
1190
|
+
// 解析 agentId:使用 sessionContext.peerId 和 sessionContext.chatType 进行匹配
|
|
1191
|
+
// 与 handleDingTalkMessageInternal 中的匹配逻辑保持一致
|
|
1221
1192
|
let matchedAgentId: string | null = null;
|
|
1222
1193
|
if (cfg.bindings && cfg.bindings.length > 0) {
|
|
1223
1194
|
for (const binding of cfg.bindings) {
|
|
@@ -1225,8 +1196,8 @@ export async function handleDingTalkMessage(params: HandleMessageParams): Promis
|
|
|
1225
1196
|
if (match.channel && match.channel !== "dingtalk-connector") continue;
|
|
1226
1197
|
if (match.accountId && match.accountId !== accountId) continue;
|
|
1227
1198
|
if (match.peer) {
|
|
1228
|
-
if (match.peer.kind && match.peer.kind !== chatType) continue;
|
|
1229
|
-
if (match.peer.id && match.peer.id !== '*' && match.peer.id !== peerId) continue;
|
|
1199
|
+
if (match.peer.kind && match.peer.kind !== queueSessionContext.chatType) continue;
|
|
1200
|
+
if (match.peer.id && match.peer.id !== '*' && match.peer.id !== queueSessionContext.peerId) continue;
|
|
1230
1201
|
}
|
|
1231
1202
|
matchedAgentId = binding.agentId;
|
|
1232
1203
|
break;
|
|
@@ -1235,9 +1206,11 @@ export async function handleDingTalkMessage(params: HandleMessageParams): Promis
|
|
|
1235
1206
|
if (!matchedAgentId) {
|
|
1236
1207
|
matchedAgentId = cfg.defaultAgent || 'main';
|
|
1237
1208
|
}
|
|
1238
|
-
|
|
1239
|
-
// 构建队列标识:会话 + agentId
|
|
1240
|
-
//
|
|
1209
|
+
|
|
1210
|
+
// 构建队列标识:会话 peerId + agentId
|
|
1211
|
+
// queueKey 与 sessionKey 使用相同的 peerId,确保隔离策略一致:
|
|
1212
|
+
// - groupSessionScope: 'group_sender' 时,同群不同用户的消息可并行处理
|
|
1213
|
+
// - sharedMemoryAcrossConversations: true 时,所有消息共享同一队列
|
|
1241
1214
|
const queueKey = `${baseSessionId}:${matchedAgentId}`;
|
|
1242
1215
|
|
|
1243
1216
|
try {
|
package/src/core/provider.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
15
15
|
import * as monitorState from "./state";
|
|
16
|
+
import { createLogger } from "../utils/logger";
|
|
16
17
|
|
|
17
18
|
// 只解构 monitorState 的导出
|
|
18
19
|
const {
|
|
@@ -44,7 +45,7 @@ export async function monitorDingtalkProvider(opts: MonitorDingtalkOpts = {}): P
|
|
|
44
45
|
throw new Error("Config is required for DingTalk monitor");
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
const log =
|
|
48
|
+
const log = createLogger(cfg.channels?.["dingtalk-connector"]?.debug ?? false);
|
|
48
49
|
|
|
49
50
|
// 并行导入所有模块(无循环依赖,可以并行)
|
|
50
51
|
const [accountsModule, monitorAccountModule, monitorSingleModule] = await Promise.all([
|
package/src/onboarding.ts
CHANGED
|
@@ -112,7 +112,7 @@ async function noteDingtalkCredentialHelp(prompter: WizardPrompter): Promise<voi
|
|
|
112
112
|
[
|
|
113
113
|
"1) Go to DingTalk Open Platform (open-dev.dingtalk.com)",
|
|
114
114
|
"2) Create an enterprise internal app",
|
|
115
|
-
"3) Get
|
|
115
|
+
"3) Get Client ID and Client Secret from Credentials page",
|
|
116
116
|
"4) Enable required permissions: im:message, im:chat",
|
|
117
117
|
"5) Publish the app or add it to a test group",
|
|
118
118
|
"Tip: you can also set DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET env vars.",
|
|
@@ -128,7 +128,7 @@ async function promptDingtalkClientId(params: {
|
|
|
128
128
|
}): Promise<string> {
|
|
129
129
|
const clientId = String(
|
|
130
130
|
await params.prompter.text({
|
|
131
|
-
message: "Enter DingTalk
|
|
131
|
+
message: "Enter DingTalk Client ID",
|
|
132
132
|
initialValue: params.initialValue,
|
|
133
133
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
134
134
|
}),
|
|
@@ -274,36 +274,96 @@ export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
274
274
|
await noteDingtalkCredentialHelp(prompter);
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
prompter
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
accountConfigured: Boolean(resolved),
|
|
283
|
-
canUseEnv,
|
|
284
|
-
hasConfigToken: hasConfigSecret,
|
|
285
|
-
envPrompt: "DINGTALK_CLIENT_ID + DINGTALK_CLIENT_SECRET detected. Use env vars?",
|
|
286
|
-
keepPrompt: "DingTalk App Secret already configured. Keep it?",
|
|
287
|
-
inputPrompt: "Enter DingTalk App Secret (Client Secret)",
|
|
288
|
-
preferredEnvVar: "DINGTALK_CLIENT_SECRET",
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
if (clientSecretResult.action === "use-env") {
|
|
292
|
-
next = {
|
|
293
|
-
...next,
|
|
294
|
-
channels: {
|
|
295
|
-
...next.channels,
|
|
296
|
-
"dingtalk-connector": { ...next.channels?.["dingtalk-connector"], enabled: true },
|
|
297
|
-
},
|
|
298
|
-
};
|
|
299
|
-
} else if (clientSecretResult.action === "set") {
|
|
300
|
-
clientSecret = clientSecretResult.value;
|
|
301
|
-
clientSecretProbeValue = clientSecretResult.resolvedValue;
|
|
302
|
-
clientId = await promptDingtalkClientId({
|
|
303
|
-
prompter,
|
|
304
|
-
initialValue:
|
|
305
|
-
normalizeString(dingtalkCfg?.clientId) ?? normalizeString(process.env.DINGTALK_CLIENT_ID),
|
|
277
|
+
// Check if we can use environment variables
|
|
278
|
+
if (canUseEnv) {
|
|
279
|
+
const useEnv = await prompter.confirm({
|
|
280
|
+
message: "DINGTALK_CLIENT_ID + DINGTALK_CLIENT_SECRET detected. Use env vars?",
|
|
281
|
+
initialValue: true,
|
|
306
282
|
});
|
|
283
|
+
|
|
284
|
+
if (useEnv) {
|
|
285
|
+
next = {
|
|
286
|
+
...next,
|
|
287
|
+
channels: {
|
|
288
|
+
...next.channels,
|
|
289
|
+
"dingtalk-connector": { ...next.channels?.["dingtalk-connector"], enabled: true },
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
// Environment variables will be used, skip manual input
|
|
293
|
+
} else {
|
|
294
|
+
// User chose not to use env vars, proceed to manual input
|
|
295
|
+
canUseEnv = false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// If not using env vars, prompt for credentials
|
|
300
|
+
if (!canUseEnv) {
|
|
301
|
+
// Check if we should keep existing configuration
|
|
302
|
+
if (resolved && hasConfigSecret) {
|
|
303
|
+
const keepExisting = await prompter.confirm({
|
|
304
|
+
message: "DingTalk credentials already configured. Keep them?",
|
|
305
|
+
initialValue: true,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (!keepExisting) {
|
|
309
|
+
// User wants to reconfigure, proceed to input
|
|
310
|
+
// Step 1: Prompt for Client ID first
|
|
311
|
+
clientId = await promptDingtalkClientId({
|
|
312
|
+
prompter,
|
|
313
|
+
initialValue:
|
|
314
|
+
normalizeString(dingtalkCfg?.clientId) ?? normalizeString(process.env.DINGTALK_CLIENT_ID),
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Step 2: Then prompt for Client Secret
|
|
318
|
+
const clientSecretResult = await promptSingleChannelSecretInput({
|
|
319
|
+
cfg: next,
|
|
320
|
+
prompter,
|
|
321
|
+
providerHint: "dingtalk",
|
|
322
|
+
credentialLabel: "Client Secret",
|
|
323
|
+
accountConfigured: false, // Force new input
|
|
324
|
+
canUseEnv: false, // Already handled above
|
|
325
|
+
hasConfigToken: false, // Force new input
|
|
326
|
+
envPrompt: "", // Not used
|
|
327
|
+
keepPrompt: "", // Not used
|
|
328
|
+
inputPrompt: "Enter DingTalk Client Secret",
|
|
329
|
+
preferredEnvVar: "DINGTALK_CLIENT_SECRET",
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (clientSecretResult.action === "set") {
|
|
333
|
+
clientSecret = clientSecretResult.value;
|
|
334
|
+
clientSecretProbeValue = clientSecretResult.resolvedValue;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// If keepExisting is true, we don't modify anything
|
|
338
|
+
} else {
|
|
339
|
+
// No existing config, prompt for new credentials
|
|
340
|
+
// Step 1: Prompt for Client ID first
|
|
341
|
+
clientId = await promptDingtalkClientId({
|
|
342
|
+
prompter,
|
|
343
|
+
initialValue:
|
|
344
|
+
normalizeString(dingtalkCfg?.clientId) ?? normalizeString(process.env.DINGTALK_CLIENT_ID),
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Step 2: Then prompt for Client Secret
|
|
348
|
+
const clientSecretResult = await promptSingleChannelSecretInput({
|
|
349
|
+
cfg: next,
|
|
350
|
+
prompter,
|
|
351
|
+
providerHint: "dingtalk",
|
|
352
|
+
credentialLabel: "Client Secret",
|
|
353
|
+
accountConfigured: false,
|
|
354
|
+
canUseEnv: false,
|
|
355
|
+
hasConfigToken: false,
|
|
356
|
+
envPrompt: "",
|
|
357
|
+
keepPrompt: "",
|
|
358
|
+
inputPrompt: "Enter DingTalk Client Secret",
|
|
359
|
+
preferredEnvVar: "DINGTALK_CLIENT_SECRET",
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
if (clientSecretResult.action === "set") {
|
|
363
|
+
clientSecret = clientSecretResult.value;
|
|
364
|
+
clientSecretProbeValue = clientSecretResult.resolvedValue;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
307
367
|
}
|
|
308
368
|
|
|
309
369
|
if (clientId && clientSecret) {
|
|
@@ -858,8 +858,8 @@ async function sendProactiveInternal(
|
|
|
858
858
|
|
|
859
859
|
const {
|
|
860
860
|
msgType = "text",
|
|
861
|
-
useAICard =
|
|
862
|
-
fallbackToNormal =
|
|
861
|
+
useAICard = true, // 默认启用 AI Card,让主动发送消息优先使用卡片形式
|
|
862
|
+
fallbackToNormal = true, // 默认降级,AI Card 失败时自动回退到普通消息
|
|
863
863
|
log: externalLog,
|
|
864
864
|
} = options;
|
|
865
865
|
|
package/src/utils/logger.ts
CHANGED
|
@@ -69,10 +69,10 @@ export function createLogger(debug: boolean = false, prefix: string = '') {
|
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* 从配置中创建日志记录器
|
|
72
|
-
* @param config - 包含 debug
|
|
72
|
+
* @param config - 包含 debug 配置的对象(可选)
|
|
73
73
|
* @param prefix - 日志前缀
|
|
74
74
|
* @returns 日志记录器对象
|
|
75
75
|
*/
|
|
76
|
-
export function createLoggerFromConfig(config: { debug?: boolean }, prefix: string = '') {
|
|
77
|
-
return createLogger(!!config
|
|
76
|
+
export function createLoggerFromConfig(config: { debug?: boolean } | undefined | null, prefix: string = '') {
|
|
77
|
+
return createLogger(!!config?.debug, prefix);
|
|
78
78
|
}
|
package/src/utils/session.ts
CHANGED
|
@@ -49,7 +49,6 @@ export function buildSessionContext(params: {
|
|
|
49
49
|
const isDirect = conversationType === '1';
|
|
50
50
|
|
|
51
51
|
// sharedMemoryAcrossConversations=true 时,所有会话共享记忆
|
|
52
|
-
// 通过将 peerId 设置为 accountId 来实现跨会话记忆共享
|
|
53
52
|
if (sharedMemoryAcrossConversations === true) {
|
|
54
53
|
return {
|
|
55
54
|
channel: 'dingtalk-connector',
|
|
@@ -62,17 +61,20 @@ export function buildSessionContext(params: {
|
|
|
62
61
|
};
|
|
63
62
|
}
|
|
64
63
|
|
|
64
|
+
// separateSessionByConversation=false 时,不区分单聊/群聊,按用户维度维护 session
|
|
65
65
|
if (separateSessionByConversation === false) {
|
|
66
66
|
return {
|
|
67
67
|
channel: 'dingtalk-connector',
|
|
68
68
|
accountId,
|
|
69
69
|
chatType: isDirect ? 'direct' : 'group',
|
|
70
|
-
peerId: senderId,
|
|
70
|
+
peerId: senderId, // 只用 senderId,不区分会话
|
|
71
71
|
senderName,
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// 以下是 separateSessionByConversation=true(默认)的逻辑
|
|
75
76
|
if (isDirect) {
|
|
77
|
+
// 单聊:peerId 为发送者 ID,由 OpenClaw Gateway 根据 dmScope 配置处理
|
|
76
78
|
return {
|
|
77
79
|
channel: 'dingtalk-connector',
|
|
78
80
|
accountId,
|
|
@@ -82,7 +84,9 @@ export function buildSessionContext(params: {
|
|
|
82
84
|
};
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
// 群聊:根据 groupSessionScope 配置决定会话隔离策略
|
|
85
88
|
if (groupSessionScope === 'group_sender') {
|
|
89
|
+
// 群内每个用户独立会话
|
|
86
90
|
return {
|
|
87
91
|
channel: 'dingtalk-connector',
|
|
88
92
|
accountId,
|
|
@@ -94,6 +98,7 @@ export function buildSessionContext(params: {
|
|
|
94
98
|
};
|
|
95
99
|
}
|
|
96
100
|
|
|
101
|
+
// 默认:整个群共享一个会话
|
|
97
102
|
return {
|
|
98
103
|
channel: 'dingtalk-connector',
|
|
99
104
|
accountId,
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import type { DingtalkConfig, ResolvedDingtalkAccount } from '../types/index.ts';
|
|
6
6
|
|
|
7
|
+
// SessionContext 和 buildSessionContext 统一由 session.ts 维护
|
|
8
|
+
export type { SessionContext } from './session.ts';
|
|
9
|
+
export { buildSessionContext } from './session.ts';
|
|
10
|
+
|
|
7
11
|
// ============ 常量 ============
|
|
8
12
|
|
|
9
13
|
/** 默认账号 ID,用于标记单账号模式(无 accounts 配置)时的内部标识 */
|
|
@@ -18,107 +22,6 @@ export const DINGTALK_OAPI = 'https://oapi.dingtalk.com';
|
|
|
18
22
|
|
|
19
23
|
// ============ 会话管理 ============
|
|
20
24
|
|
|
21
|
-
/** OpenClaw 标准会话上下文 */
|
|
22
|
-
export interface SessionContext {
|
|
23
|
-
channel: 'dingtalk-connector';
|
|
24
|
-
accountId: string;
|
|
25
|
-
chatType: 'direct' | 'group';
|
|
26
|
-
peerId: string;
|
|
27
|
-
conversationId?: string;
|
|
28
|
-
senderName?: string;
|
|
29
|
-
groupSubject?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 构建 OpenClaw 标准会话上下文
|
|
34
|
-
* 遵循 OpenClaw session.dmScope 机制,让 Gateway 根据配置自动处理会话隔离
|
|
35
|
-
*/
|
|
36
|
-
export function buildSessionContext(params: {
|
|
37
|
-
accountId: string;
|
|
38
|
-
senderId: string;
|
|
39
|
-
senderName?: string;
|
|
40
|
-
conversationType: string;
|
|
41
|
-
conversationId?: string;
|
|
42
|
-
groupSubject?: string;
|
|
43
|
-
separateSessionByConversation?: boolean;
|
|
44
|
-
groupSessionScope?: 'group' | 'group_sender';
|
|
45
|
-
sharedMemoryAcrossConversations?: boolean;
|
|
46
|
-
}): SessionContext {
|
|
47
|
-
const {
|
|
48
|
-
accountId,
|
|
49
|
-
senderId,
|
|
50
|
-
senderName,
|
|
51
|
-
conversationType,
|
|
52
|
-
conversationId,
|
|
53
|
-
groupSubject,
|
|
54
|
-
separateSessionByConversation,
|
|
55
|
-
groupSessionScope,
|
|
56
|
-
sharedMemoryAcrossConversations,
|
|
57
|
-
} = params;
|
|
58
|
-
const isDirect = conversationType === '1';
|
|
59
|
-
|
|
60
|
-
// sharedMemoryAcrossConversations=true 时,所有会话共享记忆
|
|
61
|
-
if (sharedMemoryAcrossConversations === true) {
|
|
62
|
-
return {
|
|
63
|
-
channel: 'dingtalk-connector',
|
|
64
|
-
accountId,
|
|
65
|
-
chatType: isDirect ? 'direct' : 'group',
|
|
66
|
-
peerId: accountId, // 使用 accountId 作为 peerId,实现跨会话记忆共享
|
|
67
|
-
conversationId: isDirect ? undefined : conversationId,
|
|
68
|
-
senderName,
|
|
69
|
-
groupSubject: isDirect ? undefined : groupSubject,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// separateSessionByConversation=false 时,不区分单聊/群聊,按用户维度维护 session
|
|
74
|
-
if (separateSessionByConversation === false) {
|
|
75
|
-
return {
|
|
76
|
-
channel: 'dingtalk-connector',
|
|
77
|
-
accountId,
|
|
78
|
-
chatType: isDirect ? 'direct' : 'group',
|
|
79
|
-
peerId: senderId, // 只用 senderId,不区分会话
|
|
80
|
-
senderName,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// 以下是 separateSessionByConversation=true(默认)的逻辑
|
|
85
|
-
if (isDirect) {
|
|
86
|
-
// 单聊:peerId 为发送者 ID,由 OpenClaw Gateway 根据 dmScope 配置处理
|
|
87
|
-
return {
|
|
88
|
-
channel: 'dingtalk-connector',
|
|
89
|
-
accountId,
|
|
90
|
-
chatType: 'direct',
|
|
91
|
-
peerId: senderId,
|
|
92
|
-
senderName,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// 群聊:根据 groupSessionScope 配置决定会话隔离策略
|
|
97
|
-
if (groupSessionScope === 'group_sender') {
|
|
98
|
-
// 群内每个用户独立会话
|
|
99
|
-
return {
|
|
100
|
-
channel: 'dingtalk-connector',
|
|
101
|
-
accountId,
|
|
102
|
-
chatType: 'group',
|
|
103
|
-
peerId: `${conversationId}:${senderId}`,
|
|
104
|
-
conversationId,
|
|
105
|
-
senderName,
|
|
106
|
-
groupSubject,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// 默认:整个群共享一个会话
|
|
111
|
-
return {
|
|
112
|
-
channel: 'dingtalk-connector',
|
|
113
|
-
accountId,
|
|
114
|
-
chatType: 'group',
|
|
115
|
-
peerId: conversationId || senderId,
|
|
116
|
-
conversationId,
|
|
117
|
-
senderName,
|
|
118
|
-
groupSubject,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
25
|
/**
|
|
123
26
|
* 检查消息是否是新会话命令
|
|
124
27
|
*/
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 钉钉连接测试脚本
|
|
5
|
-
* 用于诊断 "HTTP request sent to HTTPS port" 问题
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import axios from 'axios';
|
|
9
|
-
import https from 'https';
|
|
10
|
-
import http from 'http';
|
|
11
|
-
|
|
12
|
-
console.log('='.repeat(60));
|
|
13
|
-
console.log('钉钉连接诊断测试');
|
|
14
|
-
console.log('='.repeat(60));
|
|
15
|
-
console.log('');
|
|
16
|
-
|
|
17
|
-
// 1. 检查环境变量
|
|
18
|
-
console.log('1. 环境变量检查');
|
|
19
|
-
console.log('---');
|
|
20
|
-
console.log(`HTTP_PROXY: ${process.env.HTTP_PROXY || '未设置'}`);
|
|
21
|
-
console.log(`HTTPS_PROXY: ${process.env.HTTPS_PROXY || '未设置'}`);
|
|
22
|
-
console.log(`http_proxy: ${process.env.http_proxy || '未设置'}`);
|
|
23
|
-
console.log(`https_proxy: ${process.env.https_proxy || '未设置'}`);
|
|
24
|
-
console.log(`NO_PROXY: ${process.env.NO_PROXY || '未设置'}`);
|
|
25
|
-
console.log('');
|
|
26
|
-
|
|
27
|
-
// 2. 检查 Node.js 版本
|
|
28
|
-
console.log('2. Node.js 版本');
|
|
29
|
-
console.log('---');
|
|
30
|
-
console.log(`Node.js: ${process.version}`);
|
|
31
|
-
console.log('');
|
|
32
|
-
|
|
33
|
-
// 3. 测试 axios 默认配置
|
|
34
|
-
console.log('3. axios 默认配置');
|
|
35
|
-
console.log('---');
|
|
36
|
-
console.log(`axios.defaults.proxy: ${JSON.stringify(axios.defaults.proxy || '未设置')}`);
|
|
37
|
-
console.log(`axios.defaults.httpAgent: ${axios.defaults.httpAgent ? '已设置' : '未设置'}`);
|
|
38
|
-
console.log(`axios.defaults.httpsAgent: ${axios.defaults.httpsAgent ? '已设置' : '未设置'}`);
|
|
39
|
-
console.log('');
|
|
40
|
-
|
|
41
|
-
// 4. 测试直接请求钉钉 API
|
|
42
|
-
console.log('4. 测试钉钉 Gateway API');
|
|
43
|
-
console.log('---');
|
|
44
|
-
|
|
45
|
-
const GATEWAY_URL = 'https://api.dingtalk.com/v1.0/gateway/connections/open';
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
console.log(`请求 URL: ${GATEWAY_URL}`);
|
|
49
|
-
console.log('发送 POST 请求...');
|
|
50
|
-
|
|
51
|
-
const response = await axios({
|
|
52
|
-
url: GATEWAY_URL,
|
|
53
|
-
method: 'POST',
|
|
54
|
-
responseType: 'json',
|
|
55
|
-
data: {
|
|
56
|
-
clientId: 'test',
|
|
57
|
-
clientSecret: 'test',
|
|
58
|
-
ua: 'test',
|
|
59
|
-
subscriptions: [{ type: 'EVENT', topic: '*' }]
|
|
60
|
-
},
|
|
61
|
-
headers: {
|
|
62
|
-
'Accept': 'application/json'
|
|
63
|
-
},
|
|
64
|
-
// 显式设置 HTTPS agent
|
|
65
|
-
httpsAgent: new https.Agent({
|
|
66
|
-
rejectUnauthorized: true
|
|
67
|
-
}),
|
|
68
|
-
// 确保不使用 HTTP agent
|
|
69
|
-
httpAgent: undefined,
|
|
70
|
-
// 禁用代理
|
|
71
|
-
proxy: false,
|
|
72
|
-
timeout: 10000
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
console.log(`✅ 请求成功!`);
|
|
76
|
-
console.log(`状态码: ${response.status}`);
|
|
77
|
-
console.log(`响应数据: ${JSON.stringify(response.data).substring(0, 200)}...`);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
console.log(`❌ 请求失败!`);
|
|
80
|
-
console.log(`错误类型: ${error.constructor.name}`);
|
|
81
|
-
console.log(`错误消息: ${error.message}`);
|
|
82
|
-
|
|
83
|
-
if (error.response) {
|
|
84
|
-
console.log(`响应状态码: ${error.response.status}`);
|
|
85
|
-
console.log(`响应数据: ${typeof error.response.data === 'string' ? error.response.data.substring(0, 500) : JSON.stringify(error.response.data)}`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (error.code) {
|
|
89
|
-
console.log(`错误代码: ${error.code}`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (error.config) {
|
|
93
|
-
console.log(`请求配置:`);
|
|
94
|
-
console.log(` - URL: ${error.config.url}`);
|
|
95
|
-
console.log(` - Method: ${error.config.method}`);
|
|
96
|
-
console.log(` - Proxy: ${JSON.stringify(error.config.proxy)}`);
|
|
97
|
-
console.log(` - httpAgent: ${error.config.httpAgent ? '已设置' : '未设置'}`);
|
|
98
|
-
console.log(` - httpsAgent: ${error.config.httpsAgent ? '已设置' : '未设置'}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
console.log('');
|
|
103
|
-
console.log('='.repeat(60));
|
|
104
|
-
console.log('诊断完成');
|
|
105
|
-
console.log('='.repeat(60));
|