@youdu/openclaw-youdu 2026.5.8-3 → 2026.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/channel.js +21 -0
- package/dist/src/media.d.ts +26 -2
- package/dist/src/media.js +73 -18
- package/dist/src/message-sender.d.ts +28 -1
- package/dist/src/message-sender.js +78 -3
- package/dist/src/monitor.d.ts +6 -6
- package/dist/src/monitor.js +14 -30
- package/dist/src/sdk/ws-client.d.ts +1 -0
- package/dist/src/sdk/ws-client.js +7 -3
- package/dist/src/sdk/ws-manager.js +4 -4
- package/dist/src/state-manager.d.ts +8 -0
- package/dist/src/state-manager.js +35 -1
- package/package.json +1 -1
package/dist/src/channel.js
CHANGED
|
@@ -4,6 +4,7 @@ import { TEXT_CHUNK_LIMIT, CHANNEL_ID, DEFAULT_DM_POLICY } from './const.js';
|
|
|
4
4
|
import { youduSetupAdapter, youduSetupWizard } from './onboarding.js';
|
|
5
5
|
import { DEFAULT_ACCOUNT_ID, formatPairingApproveHint } from './openclaw-compat.js';
|
|
6
6
|
import { getYouDuRuntime } from './runtime.js';
|
|
7
|
+
import { sendYouDuMediaMessage, sendYouDuTextMessage } from './message-sender.js';
|
|
7
8
|
|
|
8
9
|
/*
|
|
9
10
|
* Licensed under the MIT License.
|
|
@@ -147,6 +148,26 @@ const youduPlugin = {
|
|
|
147
148
|
deliveryMode: "gateway",
|
|
148
149
|
chunker: (text, limit) => getYouDuRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
149
150
|
textChunkLimit: TEXT_CHUNK_LIMIT,
|
|
151
|
+
sendText: async ({ to, text, accountId }) => {
|
|
152
|
+
return sendYouDuTextMessage({
|
|
153
|
+
to,
|
|
154
|
+
text,
|
|
155
|
+
accountId,
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId }) => {
|
|
159
|
+
if (!mediaUrl) {
|
|
160
|
+
throw new Error("No media URL provided");
|
|
161
|
+
}
|
|
162
|
+
return sendYouDuMediaMessage({
|
|
163
|
+
cfg,
|
|
164
|
+
to,
|
|
165
|
+
text,
|
|
166
|
+
mediaUrl,
|
|
167
|
+
mediaLocalRoots,
|
|
168
|
+
accountId,
|
|
169
|
+
});
|
|
170
|
+
},
|
|
150
171
|
},
|
|
151
172
|
status: {
|
|
152
173
|
defaultRuntime: {
|
package/dist/src/media.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ResolvedYouDuAccount, YouDuConfig } from './utils.js';
|
|
|
3
3
|
import { WSClient, WsFrame } from "./sdk/ws-client";
|
|
4
4
|
import { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
|
5
5
|
export declare namespace Youdu {
|
|
6
|
-
const
|
|
6
|
+
const requestTimeoutMs = 30000;
|
|
7
7
|
type MediaKind = 'image' | 'file' | 'audio' | 'video';
|
|
8
8
|
interface MediaBody {
|
|
9
9
|
mediaId?: string;
|
|
@@ -34,8 +34,26 @@ export declare namespace Youdu {
|
|
|
34
34
|
headers: IncomingHttpHeaders;
|
|
35
35
|
body: Buffer;
|
|
36
36
|
}
|
|
37
|
+
interface UploadMediaResponse {
|
|
38
|
+
kind?: string;
|
|
39
|
+
mediaId?: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
contentType?: string;
|
|
42
|
+
size?: number;
|
|
43
|
+
width?: number;
|
|
44
|
+
height?: number;
|
|
45
|
+
duration?: number;
|
|
46
|
+
data?: {
|
|
47
|
+
mediaId?: string;
|
|
48
|
+
name?: string;
|
|
49
|
+
contentType?: string;
|
|
50
|
+
size?: number;
|
|
51
|
+
width?: number;
|
|
52
|
+
height?: number;
|
|
53
|
+
duration?: number;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
37
56
|
}
|
|
38
|
-
export declare const YouDuRequestTimeoutMs = 30000;
|
|
39
57
|
export declare function inferInboundMessageText(body: {
|
|
40
58
|
text?: {
|
|
41
59
|
content?: string;
|
|
@@ -49,6 +67,7 @@ export declare function inferInboundMessageText(body: {
|
|
|
49
67
|
video?: Youdu.MediaBody;
|
|
50
68
|
}): string;
|
|
51
69
|
export declare function extractInboundMedia(body: {
|
|
70
|
+
msgType?: string;
|
|
52
71
|
media?: Array<Youdu.MediaBody & {
|
|
53
72
|
kind?: string;
|
|
54
73
|
}>;
|
|
@@ -73,3 +92,8 @@ export declare function downloadInboundMedia(params: {
|
|
|
73
92
|
config: YouDuConfig;
|
|
74
93
|
token?: string;
|
|
75
94
|
}): Promise<Array<Youdu.DownloadedInboundMedia>>;
|
|
95
|
+
/**
|
|
96
|
+
* 统一构建有度发送媒体时可访问的本地路径白名单。
|
|
97
|
+
* 这样无论是回复链路还是官方 message send 链路,都能走同一套目录规则。
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolveYouDuMediaLocalRoots(config?: YouDuConfig): Promise<string[]>;
|
package/dist/src/media.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { loadOutboundMediaFromUrl } from './openclaw-compat.js';
|
|
1
|
+
import { getDefaultMediaLocalRoots, resolveStateDir, loadOutboundMediaFromUrl } from './openclaw-compat.js';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as https from 'node:https';
|
|
4
4
|
import { getYouDuRuntime } from './runtime.js';
|
|
5
|
-
import { DEFAULT_MEDIA_MAX_SIZE, DOWNLOAD_MEDIA_URL, HTTP_SCHEME, PORT
|
|
5
|
+
import { UPLOAD_MEDIA_URL, DEFAULT_MEDIA_MAX_SIZE, DOWNLOAD_MEDIA_URL, HTTP_SCHEME, PORT } from './const.js';
|
|
6
6
|
import { sendYouDuMediaReply } from './message-sender.js';
|
|
7
7
|
import { buildYouDuTlsConnectOptions } from './tls.js';
|
|
8
|
+
import os from 'os';
|
|
8
9
|
|
|
9
10
|
/*
|
|
10
11
|
* Licensed under the MIT License.
|
|
11
12
|
*/
|
|
12
13
|
var Youdu;
|
|
13
14
|
(function (Youdu) {
|
|
14
|
-
Youdu.
|
|
15
|
+
Youdu.requestTimeoutMs = 30000;
|
|
15
16
|
})(Youdu || (Youdu = {}));
|
|
16
17
|
function getMediaAuthHeaders(token) {
|
|
17
18
|
const headers = {};
|
|
@@ -77,8 +78,8 @@ function request(url, options) {
|
|
|
77
78
|
});
|
|
78
79
|
});
|
|
79
80
|
req.on("error", reject);
|
|
80
|
-
req.setTimeout(Youdu.
|
|
81
|
-
req.destroy(new Error(`Youdu request timed out after ${Youdu.
|
|
81
|
+
req.setTimeout(Youdu.requestTimeoutMs, () => {
|
|
82
|
+
req.destroy(new Error(`Youdu request timed out after ${Youdu.requestTimeoutMs}ms`));
|
|
82
83
|
});
|
|
83
84
|
if (options.body) {
|
|
84
85
|
req.write(options.body);
|
|
@@ -102,6 +103,10 @@ function fallbackFileNameFromUrl(mediaUrl) {
|
|
|
102
103
|
}
|
|
103
104
|
function kindFromContentType(contentType) {
|
|
104
105
|
const normalized = String(contentType ?? '').toLowerCase();
|
|
106
|
+
// SVG 更适合作为文件发送,避免被客户端按图片类型处理后无法正常展示或接收。
|
|
107
|
+
if (normalized === 'image/svg+xml') {
|
|
108
|
+
return 'file';
|
|
109
|
+
}
|
|
105
110
|
if (normalized.startsWith('image/')) {
|
|
106
111
|
return 'image';
|
|
107
112
|
}
|
|
@@ -125,6 +130,15 @@ function kindLabel(kind) {
|
|
|
125
130
|
return '[文件]';
|
|
126
131
|
}
|
|
127
132
|
}
|
|
133
|
+
function readUploadedMediaField(payload, key) {
|
|
134
|
+
// 兼容两种上传返回格式:
|
|
135
|
+
// 1. 当前协议文档里的根级字段;
|
|
136
|
+
// 2. 旧实现里包在 data 下的字段。
|
|
137
|
+
if (payload[key] !== undefined && payload[key] !== null) {
|
|
138
|
+
return payload[key];
|
|
139
|
+
}
|
|
140
|
+
return payload.data?.[key];
|
|
141
|
+
}
|
|
128
142
|
function inferInboundMessageText(body) {
|
|
129
143
|
const text = body.text?.content?.trim();
|
|
130
144
|
if (text) {
|
|
@@ -139,6 +153,24 @@ function inferInboundMessageText(body) {
|
|
|
139
153
|
}
|
|
140
154
|
function extractInboundMedia(body) {
|
|
141
155
|
const items = [];
|
|
156
|
+
// 某些回调里 media[] 只有 mediaId/name/size,没有显式 kind,
|
|
157
|
+
// 这里根据顶层 msgType 或具名媒体节点做一次回退推断。
|
|
158
|
+
const fallbackKindFromBody = (() => {
|
|
159
|
+
const normalizedMsgType = String(body.msgType ?? "").toLowerCase();
|
|
160
|
+
if (normalizedMsgType === "image" || body.image?.mediaId) {
|
|
161
|
+
return "image";
|
|
162
|
+
}
|
|
163
|
+
if (normalizedMsgType === "audio" || body.audio?.mediaId) {
|
|
164
|
+
return "audio";
|
|
165
|
+
}
|
|
166
|
+
if (normalizedMsgType === "video" || body.video?.mediaId) {
|
|
167
|
+
return "video";
|
|
168
|
+
}
|
|
169
|
+
if (normalizedMsgType === "file" || body.file?.mediaId) {
|
|
170
|
+
return "file";
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
})();
|
|
142
174
|
const push = (kind, node) => {
|
|
143
175
|
if (!node?.mediaId) {
|
|
144
176
|
return;
|
|
@@ -163,6 +195,10 @@ function extractInboundMedia(body) {
|
|
|
163
195
|
push(kind, media);
|
|
164
196
|
continue;
|
|
165
197
|
}
|
|
198
|
+
if (!kind && fallbackKindFromBody) {
|
|
199
|
+
push(fallbackKindFromBody, media);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
166
202
|
push('file', media);
|
|
167
203
|
}
|
|
168
204
|
if (items.length > 0) {
|
|
@@ -215,21 +251,21 @@ async function uploadOutboundMedia(params) {
|
|
|
215
251
|
if (response.statusCode >= 400) {
|
|
216
252
|
throw new Error(`upload media failed: ${response.statusCode} ${response.body.toString("utf8")}`);
|
|
217
253
|
}
|
|
254
|
+
// 上传接口返回成功后,统一抽取 mediaId 和媒体元数据,供 send_msg/respond_msg 复用。
|
|
218
255
|
const payload = JSON.parse(response.body.toString("utf8"));
|
|
219
|
-
const
|
|
220
|
-
const mediaId = String(data.mediaId ?? '');
|
|
256
|
+
const mediaId = String(readUploadedMediaField(payload, "mediaId") ?? "");
|
|
221
257
|
if (!mediaId) {
|
|
222
258
|
throw new Error('upload media failed: mediaId is missing');
|
|
223
259
|
}
|
|
224
260
|
return {
|
|
225
261
|
kind,
|
|
226
262
|
mediaId: mediaId,
|
|
227
|
-
name: String(
|
|
228
|
-
size: Number(
|
|
229
|
-
width: Number(
|
|
230
|
-
height: Number(
|
|
231
|
-
duration: Number(
|
|
232
|
-
contentType: String(
|
|
263
|
+
name: String(readUploadedMediaField(payload, "name") ?? fileName),
|
|
264
|
+
size: Number(readUploadedMediaField(payload, "size") ?? media.buffer.length),
|
|
265
|
+
width: Number(readUploadedMediaField(payload, "width") ?? 0) || undefined,
|
|
266
|
+
height: Number(readUploadedMediaField(payload, "height") ?? 0) || undefined,
|
|
267
|
+
duration: Number(readUploadedMediaField(payload, "duration") ?? 0) || undefined,
|
|
268
|
+
contentType: String(readUploadedMediaField(payload, "contentType") ?? media.contentType ?? ''),
|
|
233
269
|
};
|
|
234
270
|
}
|
|
235
271
|
catch (error) {
|
|
@@ -248,13 +284,13 @@ function buildMediaMessageBody(media) {
|
|
|
248
284
|
};
|
|
249
285
|
switch (media.kind) {
|
|
250
286
|
case 'image':
|
|
251
|
-
return {
|
|
287
|
+
return { msgType: 'image', image: payload };
|
|
252
288
|
case 'audio':
|
|
253
|
-
return {
|
|
289
|
+
return { msgType: 'audio', audio: payload };
|
|
254
290
|
case 'video':
|
|
255
|
-
return {
|
|
291
|
+
return { msgType: 'video', video: payload };
|
|
256
292
|
default:
|
|
257
|
-
return {
|
|
293
|
+
return { msgType: 'file', file: payload };
|
|
258
294
|
}
|
|
259
295
|
}
|
|
260
296
|
async function downloadInboundMedia(params) {
|
|
@@ -291,5 +327,24 @@ async function downloadInboundMedia(params) {
|
|
|
291
327
|
}
|
|
292
328
|
return mediaList;
|
|
293
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* 统一构建有度发送媒体时可访问的本地路径白名单。
|
|
332
|
+
* 这样无论是回复链路还是官方 message send 链路,都能走同一套目录规则。
|
|
333
|
+
*/
|
|
334
|
+
async function resolveYouDuMediaLocalRoots(config) {
|
|
335
|
+
const defaults = await getDefaultMediaLocalRoots();
|
|
336
|
+
const roots = new Set();
|
|
337
|
+
for (const root of defaults) {
|
|
338
|
+
roots.add(path.resolve(root));
|
|
339
|
+
}
|
|
340
|
+
roots.add(path.resolve(resolveStateDir()));
|
|
341
|
+
if (config?.mediaLocalRoots) {
|
|
342
|
+
for (const root of config.mediaLocalRoots) {
|
|
343
|
+
const resolved = path.resolve(root.replace(/^~(?=\/|$)/, os.homedir()));
|
|
344
|
+
roots.add(resolved);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return Array.from(roots);
|
|
348
|
+
}
|
|
294
349
|
|
|
295
|
-
export { Youdu, buildMediaMessageBody, downloadInboundMedia, extractInboundMedia, inferInboundMessageText, uploadAndSendMedia, uploadOutboundMedia };
|
|
350
|
+
export { Youdu, buildMediaMessageBody, downloadInboundMedia, extractInboundMedia, inferInboundMessageText, resolveYouDuMediaLocalRoots, uploadAndSendMedia, uploadOutboundMedia };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RuntimeEnv } from 'openclaw/plugin-sdk/runtime-env';
|
|
2
|
-
import { WSClient, WsFrame } from './sdk/ws-client';
|
|
3
2
|
import { type Youdu } from "./media";
|
|
3
|
+
import { WSClient, WsFrame } from './sdk/ws-client';
|
|
4
4
|
/**
|
|
5
5
|
* 发送 YouDu 回复
|
|
6
6
|
*/
|
|
@@ -16,3 +16,30 @@ export declare function sendYouDuMediaReply(params: {
|
|
|
16
16
|
mediaAttachment: Youdu.InboundMediaAttachment;
|
|
17
17
|
runtime: RuntimeEnv;
|
|
18
18
|
}): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* 主动发送有度文本消息。
|
|
21
|
+
*/
|
|
22
|
+
export declare function sendYouDuTextMessage(params: {
|
|
23
|
+
to: string;
|
|
24
|
+
text: string;
|
|
25
|
+
accountId?: string | null;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
channel: string;
|
|
28
|
+
messageId: string;
|
|
29
|
+
chatId: string;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* 主动发送有度媒体消息。
|
|
33
|
+
*/
|
|
34
|
+
export declare function sendYouDuMediaMessage(params: {
|
|
35
|
+
cfg: any;
|
|
36
|
+
to: string;
|
|
37
|
+
mediaUrl: string;
|
|
38
|
+
text?: string;
|
|
39
|
+
mediaLocalRoots?: readonly string[];
|
|
40
|
+
accountId?: string | null;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
channel: string;
|
|
43
|
+
messageId: string;
|
|
44
|
+
chatId: string;
|
|
45
|
+
}>;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID } from './openclaw-compat.js';
|
|
2
|
+
import { getDirectChatTarget, getYouDuWebSocket } from './state-manager.js';
|
|
3
|
+
import { CHANNEL_ID } from './const.js';
|
|
4
|
+
import { resolveYouDuMediaLocalRoots, uploadOutboundMedia, buildMediaMessageBody } from './media.js';
|
|
5
|
+
import { resolveYouDuAccount } from './utils.js';
|
|
2
6
|
|
|
3
7
|
/*
|
|
4
8
|
* Licensed under the MIT License.
|
|
@@ -10,7 +14,7 @@ async function sendYouDuReply(params) {
|
|
|
10
14
|
const { wsClient, frame, text, runtime } = params;
|
|
11
15
|
try {
|
|
12
16
|
await wsClient.reply(frame, {
|
|
13
|
-
|
|
17
|
+
msgType: 'text',
|
|
14
18
|
text: { content: text },
|
|
15
19
|
});
|
|
16
20
|
}
|
|
@@ -31,5 +35,76 @@ async function sendYouDuMediaReply(params) {
|
|
|
31
35
|
throw err;
|
|
32
36
|
}
|
|
33
37
|
}
|
|
38
|
+
function getOutboundWsClient(accountId) {
|
|
39
|
+
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
40
|
+
const wsClient = getYouDuWebSocket(resolvedAccountId);
|
|
41
|
+
if (!wsClient) {
|
|
42
|
+
throw new Error(`WSClient not connected for account ${resolvedAccountId}`);
|
|
43
|
+
}
|
|
44
|
+
return wsClient;
|
|
45
|
+
}
|
|
46
|
+
function resolveOutboundChatId(target, accountId) {
|
|
47
|
+
const normalizedTarget = target.replace(new RegExp(`^${CHANNEL_ID}:`, "i"), "").trim();
|
|
48
|
+
if (/^\d+$/.test(normalizedTarget)) {
|
|
49
|
+
return normalizedTarget;
|
|
50
|
+
}
|
|
51
|
+
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
52
|
+
// 私聊主动发送优先复用运行期学习到的 account -> chatId 映射,
|
|
53
|
+
// 这样 message send 可以直接使用账号字符串作为目标。
|
|
54
|
+
const rememberedChatId = getDirectChatTarget(resolvedAccountId, normalizedTarget);
|
|
55
|
+
if (rememberedChatId) {
|
|
56
|
+
return rememberedChatId;
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`Unknown YouDu direct target: ${normalizedTarget}. Send a message from that account to the bot first so the plugin can learn its numeric chatId.`);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 主动发送有度文本消息。
|
|
62
|
+
*/
|
|
63
|
+
async function sendYouDuTextMessage(params) {
|
|
64
|
+
const { to, text, accountId } = params;
|
|
65
|
+
const chatId = resolveOutboundChatId(to, accountId);
|
|
66
|
+
const wsClient = getOutboundWsClient(accountId);
|
|
67
|
+
const ack = await wsClient.sendMessage(chatId, {
|
|
68
|
+
msgType: "text",
|
|
69
|
+
text: { content: text },
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
channel: CHANNEL_ID,
|
|
73
|
+
messageId: ack.headers?.reqId ?? `youdu-${Date.now()}`,
|
|
74
|
+
chatId,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 主动发送有度媒体消息。
|
|
79
|
+
*/
|
|
80
|
+
async function sendYouDuMediaMessage(params) {
|
|
81
|
+
const { cfg, to, mediaUrl, text, mediaLocalRoots, accountId } = params;
|
|
82
|
+
const chatId = resolveOutboundChatId(to, accountId);
|
|
83
|
+
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
84
|
+
const wsClient = getOutboundWsClient(resolvedAccountId);
|
|
85
|
+
const account = resolveYouDuAccount(cfg);
|
|
86
|
+
const effectiveMediaLocalRoots = mediaLocalRoots && mediaLocalRoots.length > 0
|
|
87
|
+
? [...mediaLocalRoots]
|
|
88
|
+
: await resolveYouDuMediaLocalRoots(account.config);
|
|
89
|
+
// 先走 CGI 上传拿到 mediaId,再通过 send_msg 发送正式媒体消息。
|
|
90
|
+
const mediaAttachment = await uploadOutboundMedia({
|
|
91
|
+
account,
|
|
92
|
+
mediaUrl,
|
|
93
|
+
mediaLocalRoots: effectiveMediaLocalRoots,
|
|
94
|
+
token: wsClient.getToken(),
|
|
95
|
+
});
|
|
96
|
+
if (text?.trim()) {
|
|
97
|
+
await wsClient.sendMessage(chatId, {
|
|
98
|
+
msgType: "text",
|
|
99
|
+
text: { content: text.trim() },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const ack = await wsClient.sendMessage(chatId, buildMediaMessageBody(mediaAttachment));
|
|
103
|
+
return {
|
|
104
|
+
channel: CHANNEL_ID,
|
|
105
|
+
messageId: ack.headers?.reqId ?? `youdu-${Date.now()}`,
|
|
106
|
+
chatId,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
34
109
|
|
|
35
|
-
export { sendYouDuMediaReply, sendYouDuReply };
|
|
110
|
+
export { sendYouDuMediaMessage, sendYouDuMediaReply, sendYouDuReply, sendYouDuTextMessage };
|
package/dist/src/monitor.d.ts
CHANGED
|
@@ -6,17 +6,17 @@ import { type Youdu } from './media.js';
|
|
|
6
6
|
* 消息体类型(待用户完善)
|
|
7
7
|
*/
|
|
8
8
|
export interface YouDuMessageBody {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
msgId: string;
|
|
10
|
+
chatType: 'single' | 'group';
|
|
11
|
+
chatId: string;
|
|
12
12
|
from: {
|
|
13
13
|
userId: string;
|
|
14
14
|
account: string;
|
|
15
15
|
name?: string;
|
|
16
16
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
createTime?: number;
|
|
18
|
+
responseUrl?: string;
|
|
19
|
+
msgType: string;
|
|
20
20
|
text?: {
|
|
21
21
|
content: string;
|
|
22
22
|
};
|
package/dist/src/monitor.js
CHANGED
|
@@ -3,15 +3,12 @@ import { getYouDuRuntime } from './runtime.js';
|
|
|
3
3
|
import { resolveYouDuAccount } from './utils.js';
|
|
4
4
|
import { WS_MAX_RECONNECT_ATTEMPTS, WS_HEARTBEAT_INTERVAL_MS, PORT, THINKING_MESSAGE, MESSAGE_PROCESS_TIMEOUT_MS, CHANNEL_ID, MEDIA_IMAGE_PLACEHOLDER, MEDIA_DOCUMENT_PLACEHOLDER } from './const.js';
|
|
5
5
|
import { buildYouDuTlsConnectOptions } from './tls.js';
|
|
6
|
-
import { cleanupAccount, startMessageStateCleanup, setYouDuWebSocket, stopMessageStateCleanup, setMessageState, deleteMessageState } from './state-manager.js';
|
|
6
|
+
import { cleanupAccount, startMessageStateCleanup, setYouDuWebSocket, stopMessageStateCleanup, rememberDirectChatTarget, setMessageState, deleteMessageState } from './state-manager.js';
|
|
7
7
|
import { checkDmPolicy } from './dm-policy.js';
|
|
8
8
|
import { checkGroupPolicy } from './group-policy.js';
|
|
9
|
-
import { extractInboundMedia, inferInboundMessageText, downloadInboundMedia, uploadAndSendMedia } from './media.js';
|
|
9
|
+
import { extractInboundMedia, inferInboundMessageText, downloadInboundMedia, uploadAndSendMedia, resolveYouDuMediaLocalRoots } from './media.js';
|
|
10
10
|
import { sendYouDuReply } from './message-sender.js';
|
|
11
11
|
import { generateReqId } from './sdk/ws-utils.js';
|
|
12
|
-
import path__default from 'path';
|
|
13
|
-
import os from 'os';
|
|
14
|
-
import { getDefaultMediaLocalRoots, resolveStateDir } from './openclaw-compat.js';
|
|
15
12
|
|
|
16
13
|
/*
|
|
17
14
|
* Licensed under the MIT License.
|
|
@@ -23,8 +20,8 @@ function buildMessageContext(frame, account, config, text, mediaList) {
|
|
|
23
20
|
const senderAccount = String(body.from.account).trim();
|
|
24
21
|
const senderName = String(body.from.name ?? "").trim();
|
|
25
22
|
const senderDisplay = senderName ? `${senderAccount} (${senderName})` : senderAccount;
|
|
26
|
-
const chatId = body.
|
|
27
|
-
const chatType = body.
|
|
23
|
+
const chatId = body.chatId || senderAccount;
|
|
24
|
+
const chatType = body.chatType === 'group' ? 'group' : 'direct';
|
|
28
25
|
const route = core.channel.routing.resolveAgentRoute({
|
|
29
26
|
cfg: config,
|
|
30
27
|
channel: CHANNEL_ID,
|
|
@@ -49,7 +46,7 @@ function buildMessageContext(frame, account, config, text, mediaList) {
|
|
|
49
46
|
Body: messageBody,
|
|
50
47
|
RawBody: messageBody,
|
|
51
48
|
CommandBody: messageBody,
|
|
52
|
-
MessageSid: body.
|
|
49
|
+
MessageSid: body.msgId,
|
|
53
50
|
From: chatType === 'group' ? `${CHANNEL_ID}:group:${chatId}` : `${CHANNEL_ID}:${senderId}`,
|
|
54
51
|
To: `${CHANNEL_ID}:${chatId}`,
|
|
55
52
|
SenderId: senderId,
|
|
@@ -58,14 +55,14 @@ function buildMessageContext(frame, account, config, text, mediaList) {
|
|
|
58
55
|
AccountId: account.accountId,
|
|
59
56
|
ChatType: chatType,
|
|
60
57
|
ConversationLabel: fromLabel,
|
|
61
|
-
Timestamp: Date.now(),
|
|
58
|
+
Timestamp: body.createTime || Date.now(),
|
|
62
59
|
Provider: CHANNEL_ID,
|
|
63
60
|
Surface: CHANNEL_ID,
|
|
64
61
|
OriginatingChannel: CHANNEL_ID,
|
|
65
62
|
OriginatingTo: `${CHANNEL_ID}:${chatId}`,
|
|
66
63
|
YouDuFrame: frame,
|
|
67
64
|
CommandAuthorized: true,
|
|
68
|
-
ResponseUrl: body.
|
|
65
|
+
ResponseUrl: body.responseUrl,
|
|
69
66
|
ReqId: frame.headers.reqId,
|
|
70
67
|
MediaPath: mediaList[0]?.path,
|
|
71
68
|
MediaType: mediaList[0]?.contentType,
|
|
@@ -83,11 +80,14 @@ function buildMessageContext(frame, account, config, text, mediaList) {
|
|
|
83
80
|
async function processYouDuMessage(params) {
|
|
84
81
|
const { frame, account, config, runtime, wsClient } = params;
|
|
85
82
|
const body = frame.body;
|
|
86
|
-
const chatId = body.
|
|
83
|
+
const chatId = body.chatId;
|
|
87
84
|
const youduAccount = String(body.from.account).trim();
|
|
88
|
-
const chatType = body.
|
|
89
|
-
const messageId = body.
|
|
85
|
+
const chatType = body.chatType === 'group' ? 'group' : 'direct';
|
|
86
|
+
const messageId = body.msgId;
|
|
90
87
|
const youduConfig = resolveYouDuAccount(config).config;
|
|
88
|
+
if (chatType === "direct" && chatId && youduAccount) {
|
|
89
|
+
rememberDirectChatTarget(account.accountId, youduAccount, chatId);
|
|
90
|
+
}
|
|
91
91
|
let text = '';
|
|
92
92
|
const inboundMedia = extractInboundMedia(body);
|
|
93
93
|
text = inferInboundMessageText(body);
|
|
@@ -335,23 +335,7 @@ function createSdkLogger(runtime, accountId) {
|
|
|
335
335
|
};
|
|
336
336
|
}
|
|
337
337
|
async function getExtendedMediaLocalRoots(config) {
|
|
338
|
-
|
|
339
|
-
const defaults = await getDefaultMediaLocalRoots();
|
|
340
|
-
const roots = [...defaults];
|
|
341
|
-
const stateDir = path__default.resolve(resolveStateDir());
|
|
342
|
-
if (!roots.includes(stateDir)) {
|
|
343
|
-
roots.push(stateDir);
|
|
344
|
-
}
|
|
345
|
-
// 合并用户在 YouDu 中配置的自定义路径
|
|
346
|
-
if (config?.mediaLocalRoots) {
|
|
347
|
-
for (const r of config.mediaLocalRoots) {
|
|
348
|
-
const resolved = path__default.resolve(r.replace(/^~(?=\/|$)/, os.homedir()));
|
|
349
|
-
if (!roots.includes(resolved)) {
|
|
350
|
-
roots.push(resolved);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return roots;
|
|
338
|
+
return resolveYouDuMediaLocalRoots(config);
|
|
355
339
|
}
|
|
356
340
|
/**
|
|
357
341
|
* 监听 YouDu WebSocket 连接(主函数)
|
|
@@ -14,6 +14,7 @@ export declare class WSClient extends EventEmitter {
|
|
|
14
14
|
connect(): this;
|
|
15
15
|
disconnect(): void;
|
|
16
16
|
reply(frame: WsFrame, body: any, cmd?: string): Promise<WsFrame>;
|
|
17
|
+
sendMessage(chatId: string, body: any): Promise<WsFrame>;
|
|
17
18
|
getToken(): string;
|
|
18
19
|
}
|
|
19
20
|
export type { Logger, WsFrame, WSClientOptions, WSClientEventMap, } from "./types";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import { WsConnectionManager } from './ws-manager.js';
|
|
3
|
-
import { DefaultLogger } from './ws-utils.js';
|
|
4
|
-
export {
|
|
3
|
+
import { DefaultLogger, generateReqId, WsCmd } from './ws-utils.js';
|
|
4
|
+
export { WsErrCode } from './ws-utils.js';
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
7
|
* Licensed under the MIT License.
|
|
@@ -72,9 +72,13 @@ class WSClient extends EventEmitter {
|
|
|
72
72
|
const reqId = frame.headers?.reqId || "";
|
|
73
73
|
return this.wsManager.sendReply(reqId, body, cmd);
|
|
74
74
|
}
|
|
75
|
+
sendMessage(chatId, body) {
|
|
76
|
+
const reqId = generateReqId(WsCmd.SEND_MSG);
|
|
77
|
+
return this.wsManager.sendReply(reqId, { chatId, ...body }, WsCmd.SEND_MSG);
|
|
78
|
+
}
|
|
75
79
|
getToken() {
|
|
76
80
|
return this.wsManager.getToken();
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
export { DefaultLogger, WSClient };
|
|
84
|
+
export { DefaultLogger, WSClient, WsCmd };
|
|
@@ -106,11 +106,11 @@ class WsConnectionManager extends EventEmitter {
|
|
|
106
106
|
cmd: WsCmd.CALLBACK,
|
|
107
107
|
headers: { reqId: generateReqId("raw_text") },
|
|
108
108
|
body: {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
msgId: `msg_${Date.now()}`,
|
|
110
|
+
chatId: "default_chat",
|
|
111
|
+
chatType: "single",
|
|
112
112
|
from: { userid: "default_user" },
|
|
113
|
-
|
|
113
|
+
msgType: "text",
|
|
114
114
|
text: { content: raw },
|
|
115
115
|
},
|
|
116
116
|
};
|
|
@@ -22,6 +22,14 @@ export declare function setYouDuWebSocket(accountId: string, wsClient: WSClient)
|
|
|
22
22
|
* 获取账户的 WebSocket 客户端
|
|
23
23
|
*/
|
|
24
24
|
export declare function getYouDuWebSocket(accountId: string): WSClient | null;
|
|
25
|
+
/**
|
|
26
|
+
* 记录私聊账号对应的数值 chatId。
|
|
27
|
+
*/
|
|
28
|
+
export declare function rememberDirectChatTarget(accountId: string, account: string, chatId: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* 读取私聊账号对应的数值 chatId。
|
|
31
|
+
*/
|
|
32
|
+
export declare function getDirectChatTarget(accountId: string, account: string): string | undefined;
|
|
25
33
|
/**
|
|
26
34
|
* 设置消息状态
|
|
27
35
|
*/
|
|
@@ -9,6 +9,8 @@ const wsClients = new Map();
|
|
|
9
9
|
const messageStates = new Map();
|
|
10
10
|
/** chatId 到 reqId 的映射 */
|
|
11
11
|
const reqIdStore = new Map();
|
|
12
|
+
/** 私聊账号到数值 chatId 的运行时映射 */
|
|
13
|
+
const directChatStore = new Map();
|
|
12
14
|
/** 清理定时器 */
|
|
13
15
|
let cleanupTimer = null;
|
|
14
16
|
/**
|
|
@@ -21,6 +23,33 @@ function setYouDuWebSocket(accountId, wsClient) {
|
|
|
21
23
|
}
|
|
22
24
|
wsClients.set(accountId, wsClient);
|
|
23
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* 获取账户的 WebSocket 客户端
|
|
28
|
+
*/
|
|
29
|
+
function getYouDuWebSocket(accountId) {
|
|
30
|
+
return wsClients.get(accountId) ?? null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 记录私聊账号对应的数值 chatId。
|
|
34
|
+
*/
|
|
35
|
+
function rememberDirectChatTarget(accountId, account, chatId) {
|
|
36
|
+
const normalizedAccount = String(account).trim().toLowerCase();
|
|
37
|
+
const normalizedChatId = String(chatId).trim();
|
|
38
|
+
if (!normalizedAccount || !normalizedChatId) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
directChatStore.set(`${accountId}:${normalizedAccount}`, normalizedChatId);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 读取私聊账号对应的数值 chatId。
|
|
45
|
+
*/
|
|
46
|
+
function getDirectChatTarget(accountId, account) {
|
|
47
|
+
const normalizedAccount = String(account).trim().toLowerCase();
|
|
48
|
+
if (!normalizedAccount) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
return directChatStore.get(`${accountId}:${normalizedAccount}`);
|
|
52
|
+
}
|
|
24
53
|
/**
|
|
25
54
|
* 设置消息状态
|
|
26
55
|
*/
|
|
@@ -78,6 +107,11 @@ async function cleanupAccount(accountId, expectedClient) {
|
|
|
78
107
|
reqIdStore.delete(key);
|
|
79
108
|
}
|
|
80
109
|
}
|
|
110
|
+
for (const key of directChatStore.keys()) {
|
|
111
|
+
if (key.startsWith(`${accountId}:`)) {
|
|
112
|
+
directChatStore.delete(key);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
81
115
|
}
|
|
82
116
|
|
|
83
|
-
export { cleanupAccount, deleteMessageState, setMessageState, setYouDuWebSocket, startMessageStateCleanup, stopMessageStateCleanup };
|
|
117
|
+
export { cleanupAccount, deleteMessageState, getDirectChatTarget, getYouDuWebSocket, rememberDirectChatTarget, setMessageState, setYouDuWebSocket, startMessageStateCleanup, stopMessageStateCleanup };
|