dlinkclaw-openclaw 1.0.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/LICENSE +201 -0
- package/README.en.md +46 -0
- package/README.md +15 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1 -0
- package/dist/package.json +95 -0
- package/dist/src/account.d.ts +54 -0
- package/dist/src/account.js +394 -0
- package/dist/src/channel.d.ts +17 -0
- package/dist/src/channel.js +153 -0
- package/dist/src/client/client-manager.d.ts +4 -0
- package/dist/src/client/client-manager.js +10 -0
- package/dist/src/client/const.d.ts +37 -0
- package/dist/src/client/const.js +155 -0
- package/dist/src/client/crypto.d.ts +47 -0
- package/dist/src/client/crypto.js +260 -0
- package/dist/src/client/dl-client.d.ts +117 -0
- package/dist/src/client/dl-client.js +588 -0
- package/dist/src/client/dl-http.d.ts +27 -0
- package/dist/src/client/dl-http.js +223 -0
- package/dist/src/client/media-handler.d.ts +21 -0
- package/dist/src/client/media-handler.js +196 -0
- package/dist/src/client/message.d.ts +96 -0
- package/dist/src/client/message.js +1211 -0
- package/dist/src/client/timeout.d.ts +20 -0
- package/dist/src/client/timeout.js +36 -0
- package/dist/src/client/types.d.ts +226 -0
- package/dist/src/client/types.js +1 -0
- package/dist/src/monitor.d.ts +18 -0
- package/dist/src/monitor.js +322 -0
- package/dist/src/outbound.d.ts +6 -0
- package/dist/src/outbound.js +78 -0
- package/dist/src/policy.d.ts +42 -0
- package/dist/src/policy.js +216 -0
- package/dist/src/runtime.d.ts +9 -0
- package/dist/src/runtime.js +65 -0
- package/dist/src/services/gateway-rpc.d.ts +37 -0
- package/dist/src/services/gateway-rpc.js +37 -0
- package/dist/src/services/index.d.ts +41 -0
- package/dist/src/services/index.js +245 -0
- package/dist/src/services/services.d.ts +169 -0
- package/dist/src/services/services.js +1160 -0
- package/dist/src/services/types.d.ts +391 -0
- package/dist/src/services/types.js +5 -0
- package/dist/src/services/ws-client.d.ts +186 -0
- package/dist/src/services/ws-client.js +896 -0
- package/dist/src/utils.d.ts +20 -0
- package/dist/src/utils.js +128 -0
- package/ext_capabilities/workrelay-speak/CLONE_GUIDE.md +129 -0
- package/ext_capabilities/workrelay-speak/HARDWARE.md +84 -0
- package/ext_capabilities/workrelay-speak/QWEN_GUIDE.md +151 -0
- package/ext_capabilities/workrelay-speak/README.md +79 -0
- package/ext_capabilities/workrelay-speak/__pycache__/patch-kokoro-en.cpython-312.pyc +0 -0
- package/ext_capabilities/workrelay-speak/install.sh +329 -0
- package/ext_capabilities/workrelay-speak/install.sh.bak-20260814 +376 -0
- package/ext_capabilities/workrelay-speak/manifest.json +63 -0
- package/ext_capabilities/workrelay-speak/patch-kokoro-en.py +85 -0
- package/ext_capabilities/workrelay-speak/status.sh +111 -0
- package/ext_capabilities/workrelay-speak/workrelay-tts.sh +118 -0
- package/ext_capabilities/workrelay-speak-luxtts/README.md +44 -0
- package/ext_capabilities/workrelay-speak-luxtts/install.sh +330 -0
- package/ext_capabilities/workrelay-speak-luxtts/manifest.json +63 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/45ea2e2c-0863-4966-a739-8bff8817f4c0.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/800e5af4-cba5-441e-8003-6d36f35409e6.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/85fafe01-d4d6-4a48-890e-9ae113f21905.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/reference_text.txt +4 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/stone-sample-6.4s.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/status.sh +115 -0
- package/ext_capabilities/workrelay-speak-luxtts/workrelay-tts.sh +98 -0
- package/ext_capabilities/workrelay-speak-qwen/README.md +62 -0
- package/ext_capabilities/workrelay-speak-qwen/install.sh +349 -0
- package/ext_capabilities/workrelay-speak-qwen/manifest.json +63 -0
- package/ext_capabilities/workrelay-speak-qwen/status.sh +106 -0
- package/ext_capabilities/workrelay-speak-qwen/workrelay-tts.sh +98 -0
- package/ext_capabilities/workrelay-transcribe/README.md +76 -0
- package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe.cpython-312.pyc +0 -0
- package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe_server.cpython-312.pyc +0 -0
- package/ext_capabilities/workrelay-transcribe/install.sh +227 -0
- package/ext_capabilities/workrelay-transcribe/manifest.json +42 -0
- package/ext_capabilities/workrelay-transcribe/status.sh +63 -0
- package/ext_capabilities/workrelay-transcribe/transcribe.py +56 -0
- package/ext_capabilities/workrelay-transcribe/transcribe_server.py +116 -0
- package/ext_capabilities/workrelay-transcribe/voicebox-transcribe.sh +88 -0
- package/openclaw.plugin.json +142 -0
- package/package.json +95 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 超时控制工具模块
|
|
3
|
+
*
|
|
4
|
+
* 为异步操作提供统一的超时保护机制
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* 为 Promise 添加超时保护
|
|
8
|
+
*
|
|
9
|
+
* @param promise - 原始 Promise
|
|
10
|
+
* @param timeoutMs - 超时时间(毫秒)
|
|
11
|
+
* @param message - 超时错误消息
|
|
12
|
+
* @returns 带超时保护的 Promise
|
|
13
|
+
*/
|
|
14
|
+
export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message?: string): Promise<T>;
|
|
15
|
+
/**
|
|
16
|
+
* 超时错误类型
|
|
17
|
+
*/
|
|
18
|
+
export declare class TimeoutError extends Error {
|
|
19
|
+
constructor(message: string);
|
|
20
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 超时控制工具模块
|
|
3
|
+
*
|
|
4
|
+
* 为异步操作提供统一的超时保护机制
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* 为 Promise 添加超时保护
|
|
8
|
+
*
|
|
9
|
+
* @param promise - 原始 Promise
|
|
10
|
+
* @param timeoutMs - 超时时间(毫秒)
|
|
11
|
+
* @param message - 超时错误消息
|
|
12
|
+
* @returns 带超时保护的 Promise
|
|
13
|
+
*/
|
|
14
|
+
export function withTimeout(promise, timeoutMs, message) {
|
|
15
|
+
if (timeoutMs <= 0 || !Number.isFinite(timeoutMs)) {
|
|
16
|
+
return promise;
|
|
17
|
+
}
|
|
18
|
+
let timeoutId;
|
|
19
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
20
|
+
timeoutId = setTimeout(() => {
|
|
21
|
+
reject(new TimeoutError(message ?? `Operation timed out after ${timeoutMs}ms`));
|
|
22
|
+
}, timeoutMs);
|
|
23
|
+
});
|
|
24
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
25
|
+
clearTimeout(timeoutId);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 超时错误类型
|
|
30
|
+
*/
|
|
31
|
+
export class TimeoutError extends Error {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "TimeoutError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
export type DLCallbackHandler = (...avg: any) => void;
|
|
2
|
+
export interface DLGroupConfig {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
allowFrom?: Array<string | number>;
|
|
5
|
+
topicSessionMode?: 'enabled' | 'disabled';
|
|
6
|
+
}
|
|
7
|
+
export interface DLResolvedAccount {
|
|
8
|
+
accountId: string;
|
|
9
|
+
accountName?: string;
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
config: DLConfig;
|
|
12
|
+
}
|
|
13
|
+
export interface DLConfig {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
apiKey: string;
|
|
16
|
+
secret: string;
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
accountId?: string;
|
|
19
|
+
accountName?: string;
|
|
20
|
+
/** open允许所有用户,allowlist[允许指定的用户],pairing需要管理员对码, disabled完全禁用*/
|
|
21
|
+
dmPolicy?: "open" | "allowlist" | "pairing" | "disabled";
|
|
22
|
+
allowFrom?: Array<string | number>;
|
|
23
|
+
/** 群组访问策略:"open" = 允许所有群组(默认),"allowlist" = 仅允许 groupAllowFrom 中的群组,"disabled" = 禁用群组消息 */
|
|
24
|
+
groupPolicy?: "open" | "allowlist" | "disabled";
|
|
25
|
+
groupAllowFrom?: string[];
|
|
26
|
+
configured?: boolean;
|
|
27
|
+
verificationToken?: string;
|
|
28
|
+
groups?: Record<string, DLGroupConfig>;
|
|
29
|
+
mediaMaxMb?: number;
|
|
30
|
+
historyLimit?: number;
|
|
31
|
+
dynamicAgentConfig?: DLDynamicAgentConfig;
|
|
32
|
+
accounts?: Map<string, DLConfig>;
|
|
33
|
+
}
|
|
34
|
+
export type DLGroupMember = {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
/** 2026-08-13 新增:Agent 成员内部名(main/agent01 等),用于 Agent 间 sessions_send 协作路由 */
|
|
38
|
+
agentId?: string;
|
|
39
|
+
responsibilities?: string;
|
|
40
|
+
type: 'agent' | 'user';
|
|
41
|
+
};
|
|
42
|
+
export type DLMsgType = 'text' | 'image' | 'voice' | 'video' | 'file' | 'mixed' | 'cmd';
|
|
43
|
+
/**
|
|
44
|
+
* 不同渠道的Bot
|
|
45
|
+
*/
|
|
46
|
+
export type BotType = 'ewx' | 'qq' | 'dd' | 'feishu' | 'dLink';
|
|
47
|
+
export type ContactType = 'agent' | 'user' | 'group' | 'workflow' | 'task';
|
|
48
|
+
export interface DLQuoteBase {
|
|
49
|
+
type: DLMsgType;
|
|
50
|
+
}
|
|
51
|
+
export interface DLQuoteText extends DLQuoteBase {
|
|
52
|
+
text: string;
|
|
53
|
+
}
|
|
54
|
+
export interface DLQuoteImage extends DLQuoteBase {
|
|
55
|
+
imageUrl: string;
|
|
56
|
+
aesKey?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface DLQuoteVoice extends DLQuoteBase {
|
|
59
|
+
voiceUrl: string;
|
|
60
|
+
aesKey?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface DLQuoteVideo extends DLQuoteBase {
|
|
63
|
+
videoUrl: string;
|
|
64
|
+
aesKey?: string;
|
|
65
|
+
}
|
|
66
|
+
export interface DLQuoteFile extends DLQuoteBase {
|
|
67
|
+
fileUrl: string;
|
|
68
|
+
aesKey?: string;
|
|
69
|
+
}
|
|
70
|
+
export interface DLQuoteMixed extends DLQuoteBase {
|
|
71
|
+
msgList: Array<DLTextBody | DLImageBody>;
|
|
72
|
+
}
|
|
73
|
+
export interface DLBaseBody {
|
|
74
|
+
msgType: DLMsgType;
|
|
75
|
+
}
|
|
76
|
+
export interface DLMediaBody extends DLBaseBody {
|
|
77
|
+
aesKey?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface DLTextBody extends DLBaseBody {
|
|
80
|
+
text: string;
|
|
81
|
+
quote?: DLQuoteBase;
|
|
82
|
+
}
|
|
83
|
+
export interface DLImageBody extends DLMediaBody {
|
|
84
|
+
imageUrl: string;
|
|
85
|
+
}
|
|
86
|
+
export interface DLVoiceBody extends DLMediaBody {
|
|
87
|
+
voiceUrl: string;
|
|
88
|
+
}
|
|
89
|
+
export interface DLVideoBody extends DLMediaBody {
|
|
90
|
+
videoUrl: string;
|
|
91
|
+
}
|
|
92
|
+
export interface DLFileBody extends DLMediaBody {
|
|
93
|
+
fileUrl: string;
|
|
94
|
+
}
|
|
95
|
+
export interface DLMixedBody extends DLBaseBody {
|
|
96
|
+
msgList: Array<DLTextBody | DLImageBody | DLVoiceBody | DLVideoBody | DLFileBody>;
|
|
97
|
+
quote?: DLQuoteBase;
|
|
98
|
+
}
|
|
99
|
+
export type DLKind = 'block' | 'in_process' | 'tool' | 'plan' | 'patch' | 'command' | 'reasoning' | 'end_reasoning';
|
|
100
|
+
export type MessageStatus = 'read' | 'unRead' | undefined;
|
|
101
|
+
export interface ChatUser {
|
|
102
|
+
id: string;
|
|
103
|
+
nickname?: string;
|
|
104
|
+
avatar?: string;
|
|
105
|
+
jobPost?: string;
|
|
106
|
+
}
|
|
107
|
+
export interface FromUser {
|
|
108
|
+
id: string;
|
|
109
|
+
nickname?: string;
|
|
110
|
+
chatTopic: string;
|
|
111
|
+
notifyTopic: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* DLinkclaw客户端定义
|
|
115
|
+
* 直接发消息给客户端时使用
|
|
116
|
+
*/
|
|
117
|
+
export interface DLMessage {
|
|
118
|
+
msgId: string;
|
|
119
|
+
streamId?: string;
|
|
120
|
+
chatId?: string;
|
|
121
|
+
sender: ChatUser;
|
|
122
|
+
receiver: ChatUser;
|
|
123
|
+
contactType: ContactType;
|
|
124
|
+
contactId: string;
|
|
125
|
+
botType: BotType;
|
|
126
|
+
payload: string;
|
|
127
|
+
createdAt: number | string;
|
|
128
|
+
timezone?: string;
|
|
129
|
+
status: MessageStatus;
|
|
130
|
+
finish: boolean;
|
|
131
|
+
kind?: DLKind;
|
|
132
|
+
sessionKey?: string;
|
|
133
|
+
runId?: string;
|
|
134
|
+
relayType: 'openClaw';
|
|
135
|
+
}
|
|
136
|
+
export interface ToAgent {
|
|
137
|
+
sessionKey: string;
|
|
138
|
+
agentId: string;
|
|
139
|
+
id: string;
|
|
140
|
+
nickname?: string;
|
|
141
|
+
avatarUrl?: string;
|
|
142
|
+
jobPost?: string;
|
|
143
|
+
}
|
|
144
|
+
export interface FromPerson {
|
|
145
|
+
corpId: string;
|
|
146
|
+
empId: string;
|
|
147
|
+
nickname?: string;
|
|
148
|
+
avatarUrl?: string;
|
|
149
|
+
openId: string;
|
|
150
|
+
thr3UserId?: string;
|
|
151
|
+
thr3BotId?: string;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 服务端SpringBoot中定义
|
|
155
|
+
* AgentMessage
|
|
156
|
+
*/
|
|
157
|
+
export interface DLMessagePayload {
|
|
158
|
+
msgType?: Exclude<DLMsgType, 'cmd'>;
|
|
159
|
+
relayType: 'openClaw';
|
|
160
|
+
toAgent: ToAgent;
|
|
161
|
+
fromPerson: FromPerson;
|
|
162
|
+
payload: string;
|
|
163
|
+
finish: boolean;
|
|
164
|
+
msgId: string;
|
|
165
|
+
streamId: string;
|
|
166
|
+
botType: BotType;
|
|
167
|
+
contactType: ContactType;
|
|
168
|
+
contactId: string;
|
|
169
|
+
notify: boolean;
|
|
170
|
+
notifyUrl?: string;
|
|
171
|
+
chatId?: string;
|
|
172
|
+
group: boolean;
|
|
173
|
+
groupIntroduction?: string;
|
|
174
|
+
reqId: string;
|
|
175
|
+
websocket: boolean;
|
|
176
|
+
askQuestion?: string;
|
|
177
|
+
timestamp?: number;
|
|
178
|
+
timezone?: string;
|
|
179
|
+
/** 指定会话路由: 客户端可直接指定完整的 sessionKey。
|
|
180
|
+
* 不传时自动构建默认会话 (agent:main:workrelay:direct:xxx),
|
|
181
|
+
* 传入时直接使用该 sessionKey 路由消息 */
|
|
182
|
+
groupMembers?: Array<DLGroupMember>;
|
|
183
|
+
relayTimeRhreshold?: number;
|
|
184
|
+
kind?: DLKind;
|
|
185
|
+
runId?: string;
|
|
186
|
+
fromUsers?: Array<FromUser>;
|
|
187
|
+
archiveSession?: DLArchiveSession;
|
|
188
|
+
relayId: string;
|
|
189
|
+
}
|
|
190
|
+
export interface DLArchiveSession {
|
|
191
|
+
archived: boolean;
|
|
192
|
+
sessionKey: string;
|
|
193
|
+
newSessionKey?: string;
|
|
194
|
+
runId: string;
|
|
195
|
+
}
|
|
196
|
+
export type DLIMessagePayload = {
|
|
197
|
+
content: string;
|
|
198
|
+
userId: string;
|
|
199
|
+
streamId: string;
|
|
200
|
+
msgId: string;
|
|
201
|
+
timestamp: number;
|
|
202
|
+
};
|
|
203
|
+
export type DLToolsConfig = {
|
|
204
|
+
doc?: boolean;
|
|
205
|
+
wiki?: boolean;
|
|
206
|
+
drive?: boolean;
|
|
207
|
+
perm?: boolean;
|
|
208
|
+
scopes?: boolean;
|
|
209
|
+
};
|
|
210
|
+
export type DLDynamicAgentConfig = {
|
|
211
|
+
enabled?: boolean;
|
|
212
|
+
workspaceTemplate?: string;
|
|
213
|
+
agentDirTemplate?: string;
|
|
214
|
+
maxAgents?: number;
|
|
215
|
+
};
|
|
216
|
+
export type DLMediaType = Exclude<DLMsgType, 'text' | 'mixed' | 'cmd'>;
|
|
217
|
+
export type DLMediaInfo = {
|
|
218
|
+
path: string;
|
|
219
|
+
contentType?: Exclude<DLMsgType, 'text' | 'mixed' | 'cmd'>;
|
|
220
|
+
placeholder: string;
|
|
221
|
+
};
|
|
222
|
+
export type DLDownloadResourceResult = {
|
|
223
|
+
buffer: Buffer;
|
|
224
|
+
contentType?: string;
|
|
225
|
+
fileName?: string;
|
|
226
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { OpenClawConfig as ClawdbotConfig } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
|
3
|
+
import { DLResolvedAccount } from "./client/types.js";
|
|
4
|
+
export type DLMonitorOpts = {
|
|
5
|
+
config: ClawdbotConfig;
|
|
6
|
+
runtime: RuntimeEnv;
|
|
7
|
+
abortSignal: AbortSignal;
|
|
8
|
+
accountId: string;
|
|
9
|
+
channelId: string;
|
|
10
|
+
};
|
|
11
|
+
export type DLMonitorAccountParams = {
|
|
12
|
+
cfg: ClawdbotConfig;
|
|
13
|
+
account: DLResolvedAccount;
|
|
14
|
+
runtime: RuntimeEnv;
|
|
15
|
+
abortSignal: AbortSignal;
|
|
16
|
+
channelId: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function monitorDuolinkProvider(opts: DLMonitorOpts): Promise<void>;
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { listDuolinkConfiguredAccounts, resolveDLAccount } from "./account.js";
|
|
2
|
+
import { getDLLogger, getDLRuntime } from "./runtime.js";
|
|
3
|
+
import { DLClient } from "./client/dl-client.js";
|
|
4
|
+
import { checkDmPolicy, checkGroupPolicy } from "./policy.js";
|
|
5
|
+
import { buildMessageContext, parseMessageContent, routeAndDispatchMessage } from "./client/message.js";
|
|
6
|
+
import { withTimeout } from "./client/timeout.js";
|
|
7
|
+
import { getDLClient, removeDLClient, setDLClient } from "./client/client-manager.js";
|
|
8
|
+
import { downloadAndSaveFiles } from "./client/media-handler.js";
|
|
9
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/core";
|
|
10
|
+
import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
|
|
11
|
+
import { resolveAllowlistMatchSimple } from "openclaw/plugin-sdk/allow-from";
|
|
12
|
+
export async function monitorDuolinkProvider(opts) {
|
|
13
|
+
const cfg = opts.config;
|
|
14
|
+
const { accountId, channelId, runtime, abortSignal } = opts;
|
|
15
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
16
|
+
const account = resolveDLAccount({ cfg, accountId: accountId }, channelId);
|
|
17
|
+
if (!account.enabled || !account.config.configured) {
|
|
18
|
+
throw new Error(`${channelId} account "${opts.accountId}" not configured or disabled`);
|
|
19
|
+
}
|
|
20
|
+
return monitorSingleAccount({
|
|
21
|
+
cfg,
|
|
22
|
+
account,
|
|
23
|
+
runtime,
|
|
24
|
+
abortSignal,
|
|
25
|
+
channelId,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const accounts = listDuolinkConfiguredAccounts(cfg, channelId);
|
|
29
|
+
if (accounts.size === 0) {
|
|
30
|
+
throw new Error(`No enabled ${channelId} accounts[${accountId}] configured`);
|
|
31
|
+
}
|
|
32
|
+
//TODO:处理多帐号信息
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Monitor a single Duolian account.
|
|
36
|
+
*/
|
|
37
|
+
async function monitorSingleAccount(params) {
|
|
38
|
+
const { account, abortSignal, cfg, channelId } = params;
|
|
39
|
+
const { accountId } = account;
|
|
40
|
+
const logger = getDLLogger();
|
|
41
|
+
let client = getDLClient(accountId);
|
|
42
|
+
if (!client) {
|
|
43
|
+
client = new DLClient({
|
|
44
|
+
account,
|
|
45
|
+
channelId,
|
|
46
|
+
cfg,
|
|
47
|
+
}, logger);
|
|
48
|
+
client.on('message', duolinkMessageHandler);
|
|
49
|
+
setDLClient(accountId, client);
|
|
50
|
+
logger.info(`DLClient account[${account.accountId}] is starting...`);
|
|
51
|
+
}
|
|
52
|
+
return new Promise(async (resolve, reject) => {
|
|
53
|
+
const cleanup = () => {
|
|
54
|
+
removeDLClient(accountId);
|
|
55
|
+
};
|
|
56
|
+
if (abortSignal.aborted) {
|
|
57
|
+
if (client.isConnected()) {
|
|
58
|
+
client.stop();
|
|
59
|
+
}
|
|
60
|
+
cleanup();
|
|
61
|
+
resolve();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const handleAbort = () => {
|
|
65
|
+
logger.error(`account[${accountId}]: abort signal received, stopping`);
|
|
66
|
+
if (client.isConnected()) {
|
|
67
|
+
client.stop();
|
|
68
|
+
}
|
|
69
|
+
removeDLClient(accountId);
|
|
70
|
+
};
|
|
71
|
+
abortSignal.addEventListener("abort", handleAbort, { once: true });
|
|
72
|
+
if (!client.isConnected()) {
|
|
73
|
+
try {
|
|
74
|
+
await client.start();
|
|
75
|
+
logger.info(`account[${accountId}] start successfully!`);
|
|
76
|
+
//resolve();
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
logger.error(`account[${accountId}]:DLClient启动失败:${String(e)}`);
|
|
80
|
+
cleanup();
|
|
81
|
+
abortSignal?.removeEventListener("abort", handleAbort);
|
|
82
|
+
reject(e);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function duolinkMessageHandler(dlCtx, client) {
|
|
89
|
+
const { cfg, account, channelId, rbotId } = dlCtx.ctx;
|
|
90
|
+
const accountLogInfo = `account[${account.accountId}-${rbotId}],[Monitor.ts]`;
|
|
91
|
+
const { msg, accStatus } = dlCtx;
|
|
92
|
+
const { config } = account;
|
|
93
|
+
const relayId = rbotId;
|
|
94
|
+
const botType = msg.botType;
|
|
95
|
+
const logger = getDLLogger();
|
|
96
|
+
//const relayTimeRhreshold = msg.relayTimeRhreshold ?? 5 * 60 * 1000;
|
|
97
|
+
//TODO:群组历史
|
|
98
|
+
const chatHistories = new Map();
|
|
99
|
+
const historyLimit = Math.max(0, config.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT);
|
|
100
|
+
//Step 1: 解析消息内容
|
|
101
|
+
const msgContext = parseMessageContent(msg.payload);
|
|
102
|
+
if (!msgContext) {
|
|
103
|
+
logger.error(`${accountLogInfo} resolve payload entity error!`);
|
|
104
|
+
client.publishToServer({
|
|
105
|
+
streamId: msg.streamId,
|
|
106
|
+
content: '[tag_error_resolve_payload_entity]',
|
|
107
|
+
finish: true,
|
|
108
|
+
kind: 'block'
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const { textParts, quoteContent, imageUrls, imageAesKeys, voiceUrls, voiceAesKeys, videoUrls, videoAesKeys, fileUrls, fileAesKeys } = msgContext;
|
|
113
|
+
let textBody = textParts.join("\n").trim();
|
|
114
|
+
if (textBody.length === 0 && quoteContent && quoteContent.length > 0) {
|
|
115
|
+
textBody = quoteContent;
|
|
116
|
+
}
|
|
117
|
+
//Step 2: 群组及DM策略
|
|
118
|
+
const isGroup = msg.group;
|
|
119
|
+
//const mentionTargets: DLMentionTarget[] = [];
|
|
120
|
+
if (isGroup) {
|
|
121
|
+
//这里暂时移除提及,可能需要提取提及
|
|
122
|
+
//textBody = textBody.replace(/@\S+/g, "").trim();
|
|
123
|
+
//群组策略
|
|
124
|
+
const groupPolicy = checkGroupPolicy({
|
|
125
|
+
chatId: msg.chatId,
|
|
126
|
+
senderId: rbotId,
|
|
127
|
+
account,
|
|
128
|
+
channelId,
|
|
129
|
+
logger
|
|
130
|
+
});
|
|
131
|
+
if (!groupPolicy.allowed) {
|
|
132
|
+
logger.error(`${accountLogInfo} group policy is not allowed`);
|
|
133
|
+
client.publishToServer({
|
|
134
|
+
streamId: msg.streamId,
|
|
135
|
+
content: '[tag_error_group_prolicy_not_allow]',
|
|
136
|
+
finish: true,
|
|
137
|
+
kind: 'block'
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (chatHistories && textBody.length > 0) {
|
|
142
|
+
const senderId = msg.fromPerson.empId;
|
|
143
|
+
createChannelHistoryWindow({
|
|
144
|
+
historyMap: chatHistories
|
|
145
|
+
}).record({
|
|
146
|
+
historyKey: msg.chatId,
|
|
147
|
+
limit: historyLimit,
|
|
148
|
+
entry: {
|
|
149
|
+
sender: senderId,
|
|
150
|
+
body: `${senderId}: ${textBody}`,
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
messageId: senderId, //群组发信人,回复时可提及
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const core = getDLRuntime();
|
|
158
|
+
const dmPolicy = await checkDmPolicy({
|
|
159
|
+
core,
|
|
160
|
+
senderId: relayId, //单聊对码使用中继ID效验,只要效验一次,使用该中继的用户均不需要重新对码
|
|
161
|
+
account,
|
|
162
|
+
client,
|
|
163
|
+
channelId,
|
|
164
|
+
streamId: msg.streamId,
|
|
165
|
+
logger
|
|
166
|
+
});
|
|
167
|
+
if (!dmPolicy.allowed) {
|
|
168
|
+
client.publishToServer({
|
|
169
|
+
streamId: msg.streamId,
|
|
170
|
+
content: '[tag_error_policy_refuse]',
|
|
171
|
+
kind: 'block',
|
|
172
|
+
finish: true
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
//const msgInfo = textBody.length > 50 ? textBody.slice(0, 50) + "..." : textBody
|
|
177
|
+
//logger.info(`${accountLogInfo} received message from ${senderId} in ${msg.botType}, message >>> ${msgInfo}`);
|
|
178
|
+
//Step 3: 斜扛“/”命令授权检测
|
|
179
|
+
let commandAuthorized = undefined;
|
|
180
|
+
if (textBody.length > 0) {
|
|
181
|
+
//是否应该授权命令行
|
|
182
|
+
const shouldComputeCommandAuthorized = core.channel.commands.shouldComputeCommandAuthorized(textBody, cfg);
|
|
183
|
+
logger.info(`${accountLogInfo} shouldComputeCommandAuthorized >>> ${shouldComputeCommandAuthorized}`);
|
|
184
|
+
const effectiveDmAllowFrom = [...dmPolicy.allowFrom ?? []];
|
|
185
|
+
const commandAllowFrom = isGroup ? (config.groupAllowFrom ?? [rbotId]) : effectiveDmAllowFrom;
|
|
186
|
+
logger.info(`${accountLogInfo} commandAllowFrom=[${commandAllowFrom.join(',')}]`);
|
|
187
|
+
const senderAllowedForCommands = resolveAllowlistMatchSimple({
|
|
188
|
+
allowFrom: commandAllowFrom,
|
|
189
|
+
senderId: relayId, //使用中继器ID所以命令行效验,即使用该中继的人都可以使用/命令
|
|
190
|
+
allowNameMatching: false,
|
|
191
|
+
}).allowed;
|
|
192
|
+
logger.warn(`${accountLogInfo} senderAllowedForCommands >>> ${senderAllowedForCommands}`);
|
|
193
|
+
// 2026-09-06 适配 openclaw 2026.9.1:CommandsConfig 类型已移除 useAccessGroups 字段,
|
|
194
|
+
// 运行时配置值仍可能由 openclaw.json 下发,这里保持兼容读取(缺省 true)
|
|
195
|
+
const useAccessGroups = cfg.commands?.useAccessGroups ?? true;
|
|
196
|
+
commandAuthorized = shouldComputeCommandAuthorized
|
|
197
|
+
? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
|
198
|
+
useAccessGroups,
|
|
199
|
+
authorizers: [
|
|
200
|
+
{ configured: commandAllowFrom.length > 0, allowed: senderAllowedForCommands },
|
|
201
|
+
],
|
|
202
|
+
})
|
|
203
|
+
: undefined;
|
|
204
|
+
}
|
|
205
|
+
logger.warn(`${accountLogInfo} commandAuthorized >>> ${commandAuthorized}`);
|
|
206
|
+
//Step 4: 下载媒体文
|
|
207
|
+
const [imageMediaList, voiceMediaList, videoMediaList, fileMediaList] = await Promise.all([
|
|
208
|
+
downloadAndSaveFiles({
|
|
209
|
+
client,
|
|
210
|
+
fileUrls: imageUrls,
|
|
211
|
+
fileAesKeys: imageAesKeys,
|
|
212
|
+
account,
|
|
213
|
+
clawConfig: cfg,
|
|
214
|
+
core,
|
|
215
|
+
channelId,
|
|
216
|
+
streamId: msg.streamId,
|
|
217
|
+
logger,
|
|
218
|
+
botType
|
|
219
|
+
}),
|
|
220
|
+
downloadAndSaveFiles({
|
|
221
|
+
client,
|
|
222
|
+
fileUrls: voiceUrls,
|
|
223
|
+
fileAesKeys: voiceAesKeys,
|
|
224
|
+
account,
|
|
225
|
+
clawConfig: cfg,
|
|
226
|
+
core,
|
|
227
|
+
channelId,
|
|
228
|
+
streamId: msg.streamId,
|
|
229
|
+
logger,
|
|
230
|
+
botType,
|
|
231
|
+
// 2026-08-05 语音消息:强制按音频处理(.weba + audio/webm),避免被识别为 video
|
|
232
|
+
forceAudio: true
|
|
233
|
+
}),
|
|
234
|
+
downloadAndSaveFiles({
|
|
235
|
+
client,
|
|
236
|
+
fileUrls: videoUrls,
|
|
237
|
+
fileAesKeys: videoAesKeys,
|
|
238
|
+
account,
|
|
239
|
+
clawConfig: cfg,
|
|
240
|
+
core,
|
|
241
|
+
channelId,
|
|
242
|
+
streamId: msg.streamId,
|
|
243
|
+
logger,
|
|
244
|
+
botType
|
|
245
|
+
}),
|
|
246
|
+
downloadAndSaveFiles({
|
|
247
|
+
client,
|
|
248
|
+
fileUrls: fileUrls,
|
|
249
|
+
fileAesKeys: fileAesKeys,
|
|
250
|
+
account,
|
|
251
|
+
clawConfig: cfg,
|
|
252
|
+
core,
|
|
253
|
+
channelId,
|
|
254
|
+
streamId: msg.streamId,
|
|
255
|
+
logger,
|
|
256
|
+
botType
|
|
257
|
+
}),
|
|
258
|
+
]);
|
|
259
|
+
const mediaList = [...imageMediaList, ...voiceMediaList, ...videoMediaList, ...fileMediaList];
|
|
260
|
+
if (mediaList.length === 0 && textBody.length === 0) {
|
|
261
|
+
client.publishToServer({ streamId: msg.streamId, content: '[tag_error_empty_content]', kind: 'block', finish: true });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
//Step 5: 构建上下文并路由到核心处理流程
|
|
265
|
+
const ctxPayload = await buildMessageContext({
|
|
266
|
+
core,
|
|
267
|
+
channelId,
|
|
268
|
+
account,
|
|
269
|
+
clawConfig: cfg,
|
|
270
|
+
msg,
|
|
271
|
+
imageList: imageMediaList,
|
|
272
|
+
voiceList: voiceMediaList,
|
|
273
|
+
videoList: videoMediaList,
|
|
274
|
+
fileList: fileMediaList,
|
|
275
|
+
textBody,
|
|
276
|
+
quoteContent,
|
|
277
|
+
commandAuthorized,
|
|
278
|
+
chatHistories,
|
|
279
|
+
historyLimit,
|
|
280
|
+
logger,
|
|
281
|
+
client,
|
|
282
|
+
streamId: msg.streamId,
|
|
283
|
+
relayId
|
|
284
|
+
});
|
|
285
|
+
if (!ctxPayload) {
|
|
286
|
+
logger.warn("上下文为空,未进入方法routeAndDispatchMessage...");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
await withTimeout(routeAndDispatchMessage({
|
|
291
|
+
ctxPayload,
|
|
292
|
+
core,
|
|
293
|
+
cfg,
|
|
294
|
+
client,
|
|
295
|
+
logger,
|
|
296
|
+
commandAuthorized,
|
|
297
|
+
channelId,
|
|
298
|
+
accountId: account.accountId,
|
|
299
|
+
streamId: msg.streamId,
|
|
300
|
+
accStatus,
|
|
301
|
+
chatOpts: {
|
|
302
|
+
toAgent: msg.toAgent,
|
|
303
|
+
fromUser: msg.fromPerson,
|
|
304
|
+
botType: msg.botType,
|
|
305
|
+
contactId: msg.contactId,
|
|
306
|
+
relayId,
|
|
307
|
+
chatId: isGroup ? msg.chatId : msg.fromPerson.empId,
|
|
308
|
+
isGroup,
|
|
309
|
+
notifyUrl: msg.notifyUrl
|
|
310
|
+
}
|
|
311
|
+
}), 0, //1_800_000, //30分钟不设置超时
|
|
312
|
+
`[tag_error_processing_timed_out](streamId=${msg.streamId})`);
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
client.publishToServer({
|
|
316
|
+
streamId: msg.streamId,
|
|
317
|
+
content: `[tag_error_build_ctx]\n${e}`,
|
|
318
|
+
kind: 'block',
|
|
319
|
+
finish: true
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ChannelOutboundAdapter } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import type { createTextPairingAdapter } from "openclaw/plugin-sdk/channel-pairing";
|
|
3
|
+
type ChannelPairingAdapter = ReturnType<typeof createTextPairingAdapter>;
|
|
4
|
+
export declare const dlOutbound: () => ChannelOutboundAdapter;
|
|
5
|
+
export declare const dlPairing: (channelId: string) => ChannelPairingAdapter;
|
|
6
|
+
export {};
|