dlinkclaw-openclaw 2026.8.24-alpha.1
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/README.en.md +46 -0
- package/README.md +25 -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 +53 -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 +99 -0
- package/dist/src/client/dl-client.js +535 -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 +1204 -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 +224 -0
- package/dist/src/client/types.js +1 -0
- package/dist/src/monitor.d.ts +17 -0
- package/dist/src/monitor.js +320 -0
- package/dist/src/outbound.d.ts +4 -0
- package/dist/src/outbound.js +83 -0
- package/dist/src/policy.d.ts +42 -0
- package/dist/src/policy.js +205 -0
- package/dist/src/runtime.d.ts +9 -0
- package/dist/src/runtime.js +65 -0
- package/dist/src/services/index.d.ts +41 -0
- package/dist/src/services/index.js +225 -0
- package/dist/src/services/services.d.ts +152 -0
- package/dist/src/services/services.js +1066 -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 +174 -0
- package/dist/src/services/ws-client.js +866 -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 +280 -0
- package/ext_capabilities/workrelay-speak/install.sh.bak-20260814 +376 -0
- package/ext_capabilities/workrelay-speak/manifest.json +49 -0
- package/ext_capabilities/workrelay-speak/patch-kokoro-en.py +85 -0
- package/ext_capabilities/workrelay-speak/status.sh +90 -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 +282 -0
- package/ext_capabilities/workrelay-speak-luxtts/manifest.json +49 -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 +94 -0
- package/ext_capabilities/workrelay-speak-luxtts/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 +207 -0
- package/ext_capabilities/workrelay-transcribe/manifest.json +42 -0
- package/ext_capabilities/workrelay-transcribe/status.sh +62 -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,83 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { getDLLogger, getDLRuntime } from "./runtime.js";
|
|
3
|
+
import { TEXT_CHUNK_LIMIT } from "./client/const.js";
|
|
4
|
+
import { getDLClient } from "./client/client-manager.js";
|
|
5
|
+
const log_prefix = "[Outbound.ts]";
|
|
6
|
+
const getClient = (accountId) => {
|
|
7
|
+
const dlClient = getDLClient(accountId);
|
|
8
|
+
if (!dlClient) {
|
|
9
|
+
throw new Error(`${log_prefix} DLClient account[${accountId}] is null!`);
|
|
10
|
+
}
|
|
11
|
+
return dlClient;
|
|
12
|
+
};
|
|
13
|
+
export const dlOutbound = () => {
|
|
14
|
+
const logger = getDLLogger();
|
|
15
|
+
return {
|
|
16
|
+
deliveryMode: "direct",
|
|
17
|
+
chunker: (text, limit) => getDLRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
18
|
+
chunkerMode: "markdown",
|
|
19
|
+
textChunkLimit: TEXT_CHUNK_LIMIT,
|
|
20
|
+
async sendMedia(ctx) {
|
|
21
|
+
const { to, text, accountId, replyToId, threadId, mediaUrl } = ctx;
|
|
22
|
+
const accId = accountId || DEFAULT_ACCOUNT_ID;
|
|
23
|
+
logger.info(`${log_prefix} Notify[sendMedia] to[${to}], accountId=${accId}, replyToId=${replyToId}, threadId=${threadId}, text=${text}`);
|
|
24
|
+
logger.info(`${log_prefix} Media files = [${mediaUrl}]..`);
|
|
25
|
+
return await getClient(accId).autoReply({
|
|
26
|
+
to,
|
|
27
|
+
text,
|
|
28
|
+
mediaFiles: mediaUrl && mediaUrl.length > 0 ? [mediaUrl] : []
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
async sendPayload(ctx) {
|
|
32
|
+
//主动出站(文本与媒体文件)
|
|
33
|
+
const { to, accountId, replyToId, threadId, payload } = ctx;
|
|
34
|
+
const accId = accountId || DEFAULT_ACCOUNT_ID;
|
|
35
|
+
logger.info(`${log_prefix} Notify[sendPayload] to[${to}], accountId=${accId}, replyToId=${replyToId}, threadId=${threadId}, text=${payload.text}`);
|
|
36
|
+
const mediaFile = [];
|
|
37
|
+
if (payload.mediaUrl) {
|
|
38
|
+
mediaFile.push(payload.mediaUrl);
|
|
39
|
+
}
|
|
40
|
+
if (payload.mediaUrls && payload.mediaUrls.length > 0) {
|
|
41
|
+
payload.mediaUrls.map(item => mediaFile.push(item));
|
|
42
|
+
}
|
|
43
|
+
const text = payload.text ?? '';
|
|
44
|
+
const dlClient = getClient(accId);
|
|
45
|
+
return await dlClient.autoReply({
|
|
46
|
+
to,
|
|
47
|
+
text,
|
|
48
|
+
mediaFiles: [...mediaFile]
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
async sendText(ctx) {
|
|
52
|
+
//主动出站(文本)
|
|
53
|
+
const { cfg, to, text, accountId, replyToId, threadId } = ctx;
|
|
54
|
+
const accId = accountId || DEFAULT_ACCOUNT_ID;
|
|
55
|
+
logger.info(`${log_prefix} Notify[sendText] to[${to}], accountId=${accId}, replyToId=${replyToId}, threadId=${threadId}, text=${text}`);
|
|
56
|
+
const dlClient = getClient(accId);
|
|
57
|
+
return await dlClient.autoReply({
|
|
58
|
+
to,
|
|
59
|
+
text
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
export const dlPairing = (channelId) => {
|
|
65
|
+
const logger = getDLLogger();
|
|
66
|
+
return {
|
|
67
|
+
idLabel: channelId,
|
|
68
|
+
normalizeAllowEntry: (entry) => {
|
|
69
|
+
const trimmed = entry?.trim();
|
|
70
|
+
logger.info(`${log_prefix} NormalizeAllowEntry: ${trimmed}`);
|
|
71
|
+
return trimmed.replace(new RegExp(`^(${channelId}|user):`, "i"), "").trim();
|
|
72
|
+
},
|
|
73
|
+
notifyApproval: async ({ cfg, accountId }) => {
|
|
74
|
+
const accId = accountId || DEFAULT_ACCOUNT_ID;
|
|
75
|
+
logger.info('对码成功回调通知...');
|
|
76
|
+
const dlClient = getDLClient(accId);
|
|
77
|
+
if (dlClient) {
|
|
78
|
+
await dlClient.approved(true);
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`${channelId} account[${accId}] DLClient is null!`);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { DLConfig, DLResolvedAccount } from "./client/types.js";
|
|
2
|
+
import { DLClient } from "./client/dl-client.js";
|
|
3
|
+
import { OpenClawConfig, PluginLogger, PluginRuntime } from "openclaw/plugin-sdk";
|
|
4
|
+
export type DLGroupPolicyCheckResult = {
|
|
5
|
+
allowed: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type DmPolicyCheckResult = {
|
|
8
|
+
allowed: boolean;
|
|
9
|
+
allowFrom?: Array<string | number>;
|
|
10
|
+
};
|
|
11
|
+
export declare function checkGroupPolicy(params: {
|
|
12
|
+
chatId: string;
|
|
13
|
+
senderId: string;
|
|
14
|
+
account: DLResolvedAccount;
|
|
15
|
+
channelId: string;
|
|
16
|
+
logger: PluginLogger;
|
|
17
|
+
}): DLGroupPolicyCheckResult;
|
|
18
|
+
/**
|
|
19
|
+
* 检查发送者是否在允许列表中(通用)
|
|
20
|
+
*/
|
|
21
|
+
export declare function isSenderAllowed(senderId: string, allowFrom: string[], channelId: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* 检查 DM Policy 访问控制
|
|
24
|
+
* @returns 检查结果,包含是否允许继续处理
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkDmPolicy(params: {
|
|
27
|
+
core: PluginRuntime;
|
|
28
|
+
senderId: string;
|
|
29
|
+
account: DLResolvedAccount;
|
|
30
|
+
client: DLClient;
|
|
31
|
+
channelId: string;
|
|
32
|
+
streamId: string;
|
|
33
|
+
logger: PluginLogger;
|
|
34
|
+
}): Promise<DmPolicyCheckResult>;
|
|
35
|
+
/**
|
|
36
|
+
* 设置 dmPolicy
|
|
37
|
+
*/
|
|
38
|
+
export declare function setDmPolicy(cfg: OpenClawConfig, dmPolicy: "pairing" | "allowlist" | "open" | "disabled", channelId: string): OpenClawConfig;
|
|
39
|
+
/**
|
|
40
|
+
* 设置企业微信账户配置
|
|
41
|
+
*/
|
|
42
|
+
export declare function setAccount(cfg: OpenClawConfig, account: Partial<DLConfig>, channelId: string): OpenClawConfig;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { resolveDLAccount } from "./account.js";
|
|
2
|
+
import { resolveAllowlistMatchSimple } from "openclaw/plugin-sdk/allow-from";
|
|
3
|
+
import { addWildcardAllowFrom } from "openclaw/plugin-sdk/setup";
|
|
4
|
+
/**
|
|
5
|
+
* 检查群组是否在允许列表中
|
|
6
|
+
*/
|
|
7
|
+
function isInGroupAllowed(params) {
|
|
8
|
+
const { channelId, groupPolicy } = params;
|
|
9
|
+
if (groupPolicy === "disabled") {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (groupPolicy === "open") {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
// allowlist 模式:检查群组是否在允许列表中
|
|
16
|
+
const normalizedAllowFrom = params.allowFrom.map((entry) => String(entry).replace(new RegExp(`^${channelId}:`, "i"), "").trim());
|
|
17
|
+
if (normalizedAllowFrom.includes("*")) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
const normalizedGroupId = params.groupId.trim();
|
|
21
|
+
return normalizedAllowFrom.some((entry) => entry === normalizedGroupId || entry.toLowerCase() === normalizedGroupId.toLowerCase());
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 检查群组内发送者是否在允许列表中
|
|
25
|
+
*/
|
|
26
|
+
function isGroupSenderAllowed(params) {
|
|
27
|
+
const { senderId, groupId, channelId, config } = params;
|
|
28
|
+
const groups = config.groups;
|
|
29
|
+
//不配置默认允许
|
|
30
|
+
if (!groups) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
const groupConfig = groups[groupId];
|
|
34
|
+
if (!groupConfig) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
const perGroupSenderAllowFrom = (groupConfig.allowFrom ?? []).map((v) => String(v));
|
|
38
|
+
//不配置默认允许
|
|
39
|
+
if (perGroupSenderAllowFrom.length === 0) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (perGroupSenderAllowFrom.includes("*") || perGroupSenderAllowFrom.includes(senderId)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return perGroupSenderAllowFrom.some((entry) => {
|
|
46
|
+
const normalized = entry.replace(new RegExp(`^${channelId}:`, "i"), "").trim();
|
|
47
|
+
return normalized === senderId || normalized === `user:${senderId}`;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function checkGroupPolicy(params) {
|
|
51
|
+
const { channelId, chatId, senderId, account, logger } = params;
|
|
52
|
+
const { accountId } = account;
|
|
53
|
+
const groupPolicy = account.config.groupPolicy ?? "open";
|
|
54
|
+
const groupAllowFrom = account.config.groupAllowFrom ?? [];
|
|
55
|
+
const groupAllowed = isInGroupAllowed({
|
|
56
|
+
channelId,
|
|
57
|
+
groupPolicy,
|
|
58
|
+
allowFrom: groupAllowFrom,
|
|
59
|
+
groupId: chatId,
|
|
60
|
+
});
|
|
61
|
+
if (!groupAllowed) {
|
|
62
|
+
logger.warn(`account[${accountId}] Group ${chatId} not allowed (groupPolicy=${groupPolicy})`);
|
|
63
|
+
return { allowed: false };
|
|
64
|
+
}
|
|
65
|
+
const senderAllowed = isGroupSenderAllowed({
|
|
66
|
+
channelId,
|
|
67
|
+
senderId,
|
|
68
|
+
groupId: chatId,
|
|
69
|
+
config: account.config,
|
|
70
|
+
});
|
|
71
|
+
if (!senderAllowed) {
|
|
72
|
+
logger.warn(`account[${accountId}] Sender ${senderId} not in group ${chatId} sender allowlist`);
|
|
73
|
+
return { allowed: false };
|
|
74
|
+
}
|
|
75
|
+
return { allowed: true };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 检查发送者是否在允许列表中(通用)
|
|
79
|
+
*/
|
|
80
|
+
export function isSenderAllowed(senderId, allowFrom, channelId) {
|
|
81
|
+
if (allowFrom.includes("*")) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return allowFrom.some((entry) => {
|
|
85
|
+
const normalized = entry.replace(new RegExp(`^${channelId}:`, "i"), "").trim();
|
|
86
|
+
return normalized === senderId || normalized === `user:${senderId}`;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 检查 DM Policy 访问控制
|
|
91
|
+
* @returns 检查结果,包含是否允许继续处理
|
|
92
|
+
*/
|
|
93
|
+
export async function checkDmPolicy(params) {
|
|
94
|
+
const { core, senderId, account, client, logger, channelId, streamId } = params;
|
|
95
|
+
const logPreTag = `account[${account.accountId}],Policy.ts[checkDmPolicy],`;
|
|
96
|
+
const dmPolicy = account.config.dmPolicy ?? "pairing"; //默认需要配对
|
|
97
|
+
logger.warn(`${logPreTag}Blocked DM from [${senderId}] (dmPolicy=${dmPolicy})`);
|
|
98
|
+
const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
|
|
99
|
+
// 如果 dmPolicy 是 disabled直接拒绝
|
|
100
|
+
if (dmPolicy === "disabled") {
|
|
101
|
+
logger.warn(`未配对,直接拒绝...`);
|
|
102
|
+
return { allowed: false };
|
|
103
|
+
}
|
|
104
|
+
// 如果是 open 模式,允许所有人
|
|
105
|
+
if (dmPolicy === "open") {
|
|
106
|
+
return { allowed: true, allowFrom: [...configAllowFrom] };
|
|
107
|
+
}
|
|
108
|
+
// Compatibility fallback for newer OpenClaw implementations.
|
|
109
|
+
const storeAllowFrom = await core.channel.pairing
|
|
110
|
+
.readAllowFromStore({ channel: channelId, accountId: account.accountId })
|
|
111
|
+
.catch((e) => {
|
|
112
|
+
logger.error(`${logPreTag}get allow from store, error=[${String(e)}]`);
|
|
113
|
+
return [];
|
|
114
|
+
});
|
|
115
|
+
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
|
116
|
+
const senderAllowedResult = resolveAllowlistMatchSimple({
|
|
117
|
+
allowFrom: effectiveAllowFrom,
|
|
118
|
+
senderId: senderId,
|
|
119
|
+
allowNameMatching: false
|
|
120
|
+
});
|
|
121
|
+
logger.info(`${logPreTag}senderAllowedResult = ${JSON.stringify(senderAllowedResult)}, effectiveAllowFrom=[${effectiveAllowFrom.join(',')}]`);
|
|
122
|
+
if (senderAllowedResult && senderAllowedResult.allowed) {
|
|
123
|
+
return { allowed: true, allowFrom: [...effectiveAllowFrom] };
|
|
124
|
+
}
|
|
125
|
+
// 处理未授权用户
|
|
126
|
+
if (dmPolicy === "pairing") {
|
|
127
|
+
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
|
128
|
+
channel: channelId,
|
|
129
|
+
id: senderId,
|
|
130
|
+
accountId: account.accountId,
|
|
131
|
+
meta: { name: senderId },
|
|
132
|
+
});
|
|
133
|
+
if (created) {
|
|
134
|
+
logger.info(`${logPreTag}pairing request created for sender=${senderId}`);
|
|
135
|
+
const replayContent = core.channel.pairing.buildPairingReply({
|
|
136
|
+
channel: channelId,
|
|
137
|
+
idLine: `Your account[${account.accountId}] sedner=${senderId}`,
|
|
138
|
+
code
|
|
139
|
+
});
|
|
140
|
+
await client.publishToServer({
|
|
141
|
+
streamId,
|
|
142
|
+
content: replayContent,
|
|
143
|
+
kind: 'block',
|
|
144
|
+
finish: true
|
|
145
|
+
});
|
|
146
|
+
// 对码请求已创建,通过MQTT通知客户端可一键对码
|
|
147
|
+
client.publishEventToDLClient({
|
|
148
|
+
code, // 对码验证码
|
|
149
|
+
channelId, // 通道ID
|
|
150
|
+
senderId, // 发送者ID,实质是relayBotId
|
|
151
|
+
relayType: "OpenClaw"
|
|
152
|
+
}, 'onPairingRequest', '0').catch(err => logger.error(`${logPreTag}发送对码事件失败:${String(err)}`));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
logger.error(`${logPreTag}blocked unauthorized sender=${senderId} (dmPolicy=${dmPolicy})`);
|
|
156
|
+
//再发送对验证码要求对码
|
|
157
|
+
client.publishEventToDLClient({
|
|
158
|
+
code, // 对码验证码
|
|
159
|
+
channelId, // 通道ID
|
|
160
|
+
senderId, // 发送者ID,实质是relayBotId
|
|
161
|
+
relayType: "OpenClaw"
|
|
162
|
+
}, 'onPairingRequest', '0').catch(err => logger.error(`${logPreTag}发送对码事件失败:${String(err)}`));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
logger.warn(`account[${account.accountId}] sender=${senderId} access denied`);
|
|
166
|
+
return { allowed: false };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 设置 dmPolicy
|
|
170
|
+
*/
|
|
171
|
+
export function setDmPolicy(cfg, dmPolicy, channelId) {
|
|
172
|
+
const account = resolveDLAccount({ cfg }, channelId);
|
|
173
|
+
const existingAllowFrom = account.config.allowFrom ?? [];
|
|
174
|
+
const allowFrom = dmPolicy === "open"
|
|
175
|
+
? addWildcardAllowFrom(existingAllowFrom.map((x) => String(x)))
|
|
176
|
+
: existingAllowFrom.map((x) => String(x));
|
|
177
|
+
return setAccount(cfg, {
|
|
178
|
+
dmPolicy,
|
|
179
|
+
allowFrom,
|
|
180
|
+
}, channelId);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 设置企业微信账户配置
|
|
184
|
+
*/
|
|
185
|
+
export function setAccount(cfg, account, channelId) {
|
|
186
|
+
const existing = (cfg.channels?.[channelId] ?? {});
|
|
187
|
+
const merged = {
|
|
188
|
+
enabled: account.enabled ?? existing?.enabled ?? true,
|
|
189
|
+
apiKey: account.apiKey ?? existing?.apiKey ?? "",
|
|
190
|
+
secret: account.secret ?? existing?.secret ?? "",
|
|
191
|
+
baseUrl: account.baseUrl ?? existing?.baseUrl ?? "",
|
|
192
|
+
allowFrom: account.allowFrom ?? existing?.allowFrom,
|
|
193
|
+
dmPolicy: account.dmPolicy ?? existing?.dmPolicy,
|
|
194
|
+
...(account.accountName || existing?.accountName
|
|
195
|
+
? { name: account.accountName ?? existing?.accountName }
|
|
196
|
+
: {})
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
...cfg,
|
|
200
|
+
channels: {
|
|
201
|
+
...cfg.channels,
|
|
202
|
+
[channelId]: merged,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PluginLogger, PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
export declare function setDLRuntime(next: PluginRuntime): void;
|
|
3
|
+
export declare function getDLRuntime(): PluginRuntime;
|
|
4
|
+
export declare function setDLLogger(coreLogger: PluginLogger, msgPrefix: string): void;
|
|
5
|
+
export declare function getDLLogger(): PluginLogger;
|
|
6
|
+
/** 记录一个已停止的 runId(客户端点停止后由 services.ts 调用) */
|
|
7
|
+
export declare function markRunStopped(runId: string): void;
|
|
8
|
+
/** 检查 runId 是否处于已停止状态(before_tool_call 钩子调用) */
|
|
9
|
+
export declare function isRunStopped(runId?: string): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
let runtime = null;
|
|
2
|
+
let logger = null;
|
|
3
|
+
export function setDLRuntime(next) {
|
|
4
|
+
runtime = next;
|
|
5
|
+
}
|
|
6
|
+
export function getDLRuntime() {
|
|
7
|
+
if (!runtime) {
|
|
8
|
+
throw new Error("DLink runtime not initialized");
|
|
9
|
+
}
|
|
10
|
+
return runtime;
|
|
11
|
+
}
|
|
12
|
+
export function setDLLogger(coreLogger, msgPrefix) {
|
|
13
|
+
const log = {
|
|
14
|
+
info: (msg) => {
|
|
15
|
+
coreLogger.info(`${msgPrefix} ${msg}`);
|
|
16
|
+
},
|
|
17
|
+
warn: (msg) => {
|
|
18
|
+
coreLogger.warn(`${msgPrefix} ${msg}`);
|
|
19
|
+
},
|
|
20
|
+
error: (msg) => {
|
|
21
|
+
coreLogger.error(`${msgPrefix} ${msg}`);
|
|
22
|
+
},
|
|
23
|
+
debug: (msg) => {
|
|
24
|
+
if (coreLogger.debug) {
|
|
25
|
+
coreLogger.debug(`${msgPrefix} ${msg}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (process.env.NODE_ENV === 'dev') {
|
|
29
|
+
coreLogger.info(`DEBUG-${msgPrefix} ${msg}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
logger = log;
|
|
34
|
+
}
|
|
35
|
+
export function getDLLogger() {
|
|
36
|
+
if (!logger) {
|
|
37
|
+
return console;
|
|
38
|
+
}
|
|
39
|
+
return logger;
|
|
40
|
+
}
|
|
41
|
+
// 2026-08-24 停止标记集合:客户端点“停止”触发 chat.abort 命令时记录 runId,
|
|
42
|
+
// before_tool_call 钩子据此 block 该 run 的后续工具调用,实现“立即终止”。
|
|
43
|
+
// 用 TTL 过期清理,防止 runId 无限堆积(runId 为网关唯一 ID,过期后不可能再命中)。
|
|
44
|
+
const stoppedRuns = new Map();
|
|
45
|
+
const STOPPED_RUN_TTL_MS = 10 * 60 * 1000; // 10 分钟过期
|
|
46
|
+
/** 记录一个已停止的 runId(客户端点停止后由 services.ts 调用) */
|
|
47
|
+
export function markRunStopped(runId) {
|
|
48
|
+
if (!runId)
|
|
49
|
+
return;
|
|
50
|
+
stoppedRuns.set(runId, Date.now());
|
|
51
|
+
getDLLogger().info(`markRunStopped: ${runId}`);
|
|
52
|
+
}
|
|
53
|
+
/** 检查 runId 是否处于已停止状态(before_tool_call 钩子调用) */
|
|
54
|
+
export function isRunStopped(runId) {
|
|
55
|
+
if (!runId)
|
|
56
|
+
return false;
|
|
57
|
+
const ts = stoppedRuns.get(runId);
|
|
58
|
+
if (!ts)
|
|
59
|
+
return false;
|
|
60
|
+
if (Date.now() - ts > STOPPED_RUN_TTL_MS) {
|
|
61
|
+
stoppedRuns.delete(runId); // 过期自动清理
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMD 消息处理器
|
|
3
|
+
* 集成到 duolian 的消息处理流程中
|
|
4
|
+
*/
|
|
5
|
+
import { DLClient } from "../client/dl-client.js";
|
|
6
|
+
import { ClawServiceOptions, CmdMessage, CmdResponse } from "./types.js";
|
|
7
|
+
import { PluginLogger } from "openclaw/plugin-sdk";
|
|
8
|
+
/**
|
|
9
|
+
* CMD 消息处理器
|
|
10
|
+
*/
|
|
11
|
+
export declare class CmdHandler {
|
|
12
|
+
private readonly dlClient;
|
|
13
|
+
private services;
|
|
14
|
+
private isInitialized;
|
|
15
|
+
constructor(config: ClawServiceOptions, dlClient: DLClient, logger: PluginLogger);
|
|
16
|
+
private log;
|
|
17
|
+
/**
|
|
18
|
+
* 初始化处理器
|
|
19
|
+
*/
|
|
20
|
+
initialize(): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* 处理 CMD 消息
|
|
23
|
+
*/
|
|
24
|
+
handleCmdMessage(rawMessage: CmdMessage): Promise<CmdResponse>;
|
|
25
|
+
/**
|
|
26
|
+
* 生成 CMD 响应消息(用于MQTT回复)
|
|
27
|
+
*/
|
|
28
|
+
createCmdResponse(response: CmdResponse): any;
|
|
29
|
+
/**
|
|
30
|
+
* 清理资源
|
|
31
|
+
*/
|
|
32
|
+
cleanup(): void;
|
|
33
|
+
/**
|
|
34
|
+
* 检查处理器是否就绪
|
|
35
|
+
*/
|
|
36
|
+
isReady(): boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 创建 CMD 处理器
|
|
40
|
+
*/
|
|
41
|
+
export declare function createCmdHandler(config: ClawServiceOptions, dlClient: DLClient, logger: PluginLogger): CmdHandler;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMD 消息处理器
|
|
3
|
+
* 集成到 duolian 的消息处理流程中
|
|
4
|
+
*/
|
|
5
|
+
import ClawServices from "./services.js";
|
|
6
|
+
const AGENT_RELAY_MAP = new Map();
|
|
7
|
+
//const BATCH_COUNT = 10;
|
|
8
|
+
//const BATCH_TRAJECTORY_MAP: Map<string, Array<any>> = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* CMD 消息处理器
|
|
11
|
+
*/
|
|
12
|
+
export class CmdHandler {
|
|
13
|
+
dlClient;
|
|
14
|
+
services;
|
|
15
|
+
isInitialized = false;
|
|
16
|
+
constructor(config, dlClient, logger) {
|
|
17
|
+
this.dlClient = dlClient;
|
|
18
|
+
this.services = new ClawServices(config, logger);
|
|
19
|
+
}
|
|
20
|
+
log(msg, type) {
|
|
21
|
+
this.services.log(msg, type);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 初始化处理器
|
|
25
|
+
*/
|
|
26
|
+
async initialize() {
|
|
27
|
+
try {
|
|
28
|
+
this.services.on('onTick', (payload) => {
|
|
29
|
+
this.dlClient.publishEventToDLClient(payload, 'onTick', '0');
|
|
30
|
+
//this.dlClient.publishEventToServer({ "connected": true }, "onConnectState")
|
|
31
|
+
});
|
|
32
|
+
this.services.on('onHeartbeat', (_payload) => {
|
|
33
|
+
this.dlClient.publishEventToServer({ "connected": true }, "onConnectState");
|
|
34
|
+
});
|
|
35
|
+
this.services.on('onHealth', (payload) => {
|
|
36
|
+
this.dlClient.publishEventToDLClient(payload, 'onHealth', '0');
|
|
37
|
+
});
|
|
38
|
+
this.services.on('onHandshaked', (payload) => {
|
|
39
|
+
this.dlClient.publishEventToServer(payload, 'onHandshaked', '0');
|
|
40
|
+
});
|
|
41
|
+
this.services.on('onAgentReply', (payload) => {
|
|
42
|
+
const message = payload;
|
|
43
|
+
if (message.stream === 'lifecycle') {
|
|
44
|
+
if (message.data.phase && message.data.phase === 'start') {
|
|
45
|
+
AGENT_RELAY_MAP.set(message.runId, '');
|
|
46
|
+
}
|
|
47
|
+
else if (message.data.phase === 'end') {
|
|
48
|
+
const content = AGENT_RELAY_MAP.get(message.runId);
|
|
49
|
+
if (content && content.length > 0) {
|
|
50
|
+
setTimeout(() => this.dlClient.publishEventToServer({
|
|
51
|
+
runId: message.runId,
|
|
52
|
+
sessionKey: message.sessionKey, //运行的agent SessionKey
|
|
53
|
+
content,
|
|
54
|
+
spawnedBy: message.spawnedBy //指派任务的agent SessionKey(通过消息协议入站执行agent.ask接口的该值为空)
|
|
55
|
+
}, 'onAgentReply', '0'), 1000);
|
|
56
|
+
}
|
|
57
|
+
AGENT_RELAY_MAP.delete(message.runId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (message.stream === 'assistant') {
|
|
61
|
+
if (AGENT_RELAY_MAP.has(message.runId)) {
|
|
62
|
+
if (message.data.text && message.data.text.length > 0) {
|
|
63
|
+
AGENT_RELAY_MAP.set(message.runId, message.data.text);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
// this.services.on('onAgentChat', (payload) => { 这个不需要了,会与onAgentReply上面重复
|
|
69
|
+
// if (!payload) return;
|
|
70
|
+
// if (payload.state === 'final') {
|
|
71
|
+
// setTimeout(() => this.dlClient.publishEventToServer({
|
|
72
|
+
// runId: payload.runId,
|
|
73
|
+
// sessionKey: payload.sessionKey,
|
|
74
|
+
// message: payload.message
|
|
75
|
+
// }, 'onAgentChat', '0'), 1000);
|
|
76
|
+
// }
|
|
77
|
+
// });
|
|
78
|
+
this.services.on('onSessionsChanged', (_payload) => {
|
|
79
|
+
//this.log(`sessions.changed >>> ${JSON.stringify(payload)}`, 'info'); //暂时不需要处理
|
|
80
|
+
});
|
|
81
|
+
this.services.on('onSessionMessage', (payload) => {
|
|
82
|
+
if (!payload) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const sessionKey = payload.sessionKey;
|
|
87
|
+
const message = payload.message;
|
|
88
|
+
const role = message.role;
|
|
89
|
+
if (role !== 'assistant') {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
//this.log(`onSessionMessage >>> ${JSON.stringify(payload)}`, 'warn');
|
|
93
|
+
const stopReason = message.stopReason;
|
|
94
|
+
const content = message.content ?? [];
|
|
95
|
+
const result_contents = content.map(item => {
|
|
96
|
+
if (item.type === 'thinking') {
|
|
97
|
+
return item.thinking;
|
|
98
|
+
}
|
|
99
|
+
else if (item.type === 'text' && stopReason != 'stop') { //stop应该是最终回复了,被免重复推送这里过滤掉(最终回复已在别处处理)
|
|
100
|
+
return item.text;
|
|
101
|
+
}
|
|
102
|
+
return '';
|
|
103
|
+
});
|
|
104
|
+
if (result_contents.length > 0) {
|
|
105
|
+
const label = payload.origin.label || undefined;
|
|
106
|
+
if (!label) {
|
|
107
|
+
this.log(`不能StreamId...${label}`, 'error');
|
|
108
|
+
this.dlClient.publishEventToServer({
|
|
109
|
+
sessionKey,
|
|
110
|
+
content: result_contents.join(),
|
|
111
|
+
}, 'onAgentReply', '0');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (label.includes('_')) {
|
|
115
|
+
const streamId = label.split('_')[1];
|
|
116
|
+
this.dlClient.publishProcessingState({
|
|
117
|
+
streamId,
|
|
118
|
+
content: result_contents.join(''),
|
|
119
|
+
kind: 'plan',
|
|
120
|
+
status: 'read'
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
this.log(`onSessionMessage 发生错误 >>> ${String(e)}`, 'info');
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
this.services.on('onCron', (payload) => {
|
|
130
|
+
this.dlClient.publishEventToServer(payload, 'onCron', '0');
|
|
131
|
+
});
|
|
132
|
+
this.services.on('onShutdown', (_payload) => {
|
|
133
|
+
//TODO
|
|
134
|
+
});
|
|
135
|
+
this.services.on('onConnectState', (connected) => {
|
|
136
|
+
this.dlClient.publishEventToServer({ "connected": connected }, "onConnectState");
|
|
137
|
+
});
|
|
138
|
+
this.services.on('onError', (error) => {
|
|
139
|
+
this.dlClient.publishEventToServer({ "error": error }, "onError");
|
|
140
|
+
});
|
|
141
|
+
this.services.on('onPairingResult', data => this.dlClient.publishEventToServer(data, 'onPairingResult', '0'));
|
|
142
|
+
await this.services.initialize();
|
|
143
|
+
this.isInitialized = true;
|
|
144
|
+
this.log('CMD 处理器初始化成功!', 'info');
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
this.log('CMD 处理器初始化失败:' + String(error), 'error');
|
|
149
|
+
this.isInitialized = false;
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 处理 CMD 消息
|
|
155
|
+
*/
|
|
156
|
+
async handleCmdMessage(rawMessage) {
|
|
157
|
+
if (!this.isInitialized || !this.services?.isReady()) {
|
|
158
|
+
this.log('CMD 处理器未初始化,尝试重新初始化...', 'warn');
|
|
159
|
+
const initialized = await this.initialize();
|
|
160
|
+
if (!initialized) {
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
error: 'CMD 处理器未初始化',
|
|
164
|
+
timestamp: Date.now()
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (rawMessage.params?.message) {
|
|
169
|
+
const logMessage = {
|
|
170
|
+
...rawMessage,
|
|
171
|
+
params: {
|
|
172
|
+
...rawMessage.params,
|
|
173
|
+
message: '...'
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
this.log(`🔄CMD 指令:${rawMessage.cmd}, params=${JSON.stringify(logMessage)}`, 'info');
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
this.log(`🔄CMD 指令:${rawMessage.cmd}, params=${JSON.stringify(rawMessage)}`, 'info');
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
// 调用服务层处理
|
|
183
|
+
return await this.services.handleCmdMessage(rawMessage);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
return {
|
|
187
|
+
success: false,
|
|
188
|
+
error: `cmd:[${rawMessage.cmd}]${error instanceof Error ? error.message : '处理失败'}`,
|
|
189
|
+
timestamp: Date.now()
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 生成 CMD 响应消息(用于MQTT回复)
|
|
195
|
+
*/
|
|
196
|
+
createCmdResponse(response) {
|
|
197
|
+
return {
|
|
198
|
+
type: 'cmd_response',
|
|
199
|
+
requestId: response.requestId,
|
|
200
|
+
success: response.success,
|
|
201
|
+
data: response.data,
|
|
202
|
+
error: response.error,
|
|
203
|
+
timestamp: response.timestamp
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* 清理资源
|
|
208
|
+
*/
|
|
209
|
+
cleanup() {
|
|
210
|
+
this.services.cleanup();
|
|
211
|
+
this.isInitialized = false;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 检查处理器是否就绪
|
|
215
|
+
*/
|
|
216
|
+
isReady() {
|
|
217
|
+
return this.isInitialized && (this.services?.isReady() || false);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* 创建 CMD 处理器
|
|
222
|
+
*/
|
|
223
|
+
export function createCmdHandler(config, dlClient, logger) {
|
|
224
|
+
return new CmdHandler(config, dlClient, logger);
|
|
225
|
+
}
|