@izhimu/qq 0.1.1 → 0.2.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/dist/src/adapters/message.d.ts +1 -1
- package/dist/src/adapters/message.js +7 -7
- package/dist/src/channel.js +65 -198
- package/dist/src/core/dispatch.js +35 -34
- package/dist/src/types/index.d.ts +3 -1
- package/dist/src/utils/index.d.ts +3 -0
- package/dist/src/utils/index.js +59 -0
- package/package.json +1 -1
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* Optimized for maintainability with clear structure and minimal duplication.
|
|
5
5
|
*/
|
|
6
6
|
import type { NapCatMessage, OpenClawMessage } from '../types/index.js';
|
|
7
|
-
export declare function openClawToNapCatMessage(content: OpenClawMessage[]
|
|
7
|
+
export declare function openClawToNapCatMessage(content: OpenClawMessage[]): NapCatMessage[];
|
|
8
8
|
export declare function napCatToOpenClawMessage(segments: NapCatMessage[] | string): Promise<OpenClawMessage[]>;
|
|
@@ -117,20 +117,20 @@ function openClawSegmentToNapCat(content) {
|
|
|
117
117
|
return { type: 'image', data: { file: content.url, url: content.url } };
|
|
118
118
|
case 'reply':
|
|
119
119
|
return { type: 'reply', data: { id: content.messageId } };
|
|
120
|
+
case 'file':
|
|
121
|
+
return { type: 'file', data: { file: content.file, url: content.url, file_size: content.fileSize } };
|
|
120
122
|
case 'audio':
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
return {
|
|
124
|
+
type: 'record',
|
|
125
|
+
data: { file: content.file, path: content.path, url: content.url, file_size: content.fileSize }
|
|
126
|
+
};
|
|
124
127
|
default:
|
|
125
128
|
log.warn('adapters', `Unknown content type (outbound): ${content.type}`);
|
|
126
129
|
return null;
|
|
127
130
|
}
|
|
128
131
|
}
|
|
129
|
-
export function openClawToNapCatMessage(content
|
|
132
|
+
export function openClawToNapCatMessage(content) {
|
|
130
133
|
const segments = [];
|
|
131
|
-
if (replyToId) {
|
|
132
|
-
segments.push({ type: 'reply', data: { id: replyToId } });
|
|
133
|
-
}
|
|
134
134
|
for (const item of content) {
|
|
135
135
|
const segment = openClawSegmentToNapCat(item);
|
|
136
136
|
if (segment) {
|
package/dist/src/channel.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Main plugin entry point
|
|
4
4
|
*/
|
|
5
5
|
import { buildChannelConfigSchema, DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
6
|
-
import { messageIdToString, Logger as log } from "./utils/index.js";
|
|
6
|
+
import { messageIdToString, getFileType, getFileName, Logger as log } from "./utils/index.js";
|
|
7
7
|
import { setContext, setContextStatus, clearContext, setConnection, getConnection, clearConnection } from "./core/runtime.js";
|
|
8
8
|
import { ConnectionManager } from "./core/connection.js";
|
|
9
9
|
import { openClawToNapCatMessage } from "./adapters/message.js";
|
|
@@ -53,203 +53,8 @@ export const qqPlugin = {
|
|
|
53
53
|
},
|
|
54
54
|
outbound: {
|
|
55
55
|
deliveryMode: "direct",
|
|
56
|
-
sendText:
|
|
57
|
-
|
|
58
|
-
if (!accountId) {
|
|
59
|
-
return {
|
|
60
|
-
channel: CHANNEL_ID,
|
|
61
|
-
messageId: "",
|
|
62
|
-
error: new Error("accountId is required"),
|
|
63
|
-
deliveredAt: Date.now(),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
const account = resolveQQAccount({ cfg });
|
|
67
|
-
if (!account) {
|
|
68
|
-
return {
|
|
69
|
-
channel: CHANNEL_ID,
|
|
70
|
-
messageId: "",
|
|
71
|
-
error: new Error(`Account not found: ${accountId}`),
|
|
72
|
-
deliveredAt: Date.now(),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
const connection = getConnection();
|
|
76
|
-
if (!connection?.isConnected()) {
|
|
77
|
-
return {
|
|
78
|
-
channel: CHANNEL_ID,
|
|
79
|
-
messageId: "",
|
|
80
|
-
error: new Error(`Not connected for account: ${accountId}`),
|
|
81
|
-
deliveredAt: Date.now(),
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
// Parse target (format: private:xxx or group:xxx)
|
|
85
|
-
const parts = to.split(":");
|
|
86
|
-
const type = parts[0];
|
|
87
|
-
const id = parts[1];
|
|
88
|
-
const chatType = type === "group" ? "group" : "direct";
|
|
89
|
-
const chatId = id || to;
|
|
90
|
-
try {
|
|
91
|
-
const messageSegments = openClawToNapCatMessage([{ type: "text", text }], replyToId ?? undefined);
|
|
92
|
-
const response = await sendMsg({
|
|
93
|
-
message_type: chatType === "direct" ? "private" : "group",
|
|
94
|
-
user_id: chatType === "direct" ? chatId : undefined,
|
|
95
|
-
group_id: chatType === "group" ? chatId : undefined,
|
|
96
|
-
message: messageSegments,
|
|
97
|
-
});
|
|
98
|
-
// Update lastOutboundAt timestamp on successful send
|
|
99
|
-
if (response.status === "ok") {
|
|
100
|
-
setContextStatus({
|
|
101
|
-
lastOutboundAt: Date.now(),
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
if (response.status === "ok" && response.data) {
|
|
105
|
-
const data = response.data;
|
|
106
|
-
return {
|
|
107
|
-
channel: CHANNEL_ID,
|
|
108
|
-
messageId: messageIdToString(data.message_id),
|
|
109
|
-
deliveredAt: Date.now(),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
return {
|
|
114
|
-
channel: CHANNEL_ID,
|
|
115
|
-
messageId: "",
|
|
116
|
-
error: new Error(response.msg || "Send failed"),
|
|
117
|
-
deliveredAt: Date.now(),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
123
|
-
return {
|
|
124
|
-
channel: CHANNEL_ID,
|
|
125
|
-
messageId: "",
|
|
126
|
-
error: new Error(errorMessage),
|
|
127
|
-
deliveredAt: Date.now(),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
},
|
|
131
|
-
sendMedia: async (ctx) => {
|
|
132
|
-
const { to, mediaUrl, accountId, cfg, replyToId } = ctx;
|
|
133
|
-
log.debug("outbound", `sendMedia called - accountId: ${accountId}, to: ${to}, mediaUrl: ${mediaUrl ?? "null"}, replyToId: ${replyToId ?? "none"}`);
|
|
134
|
-
if (!accountId) {
|
|
135
|
-
log.warn("outbound", "sendMedia failed: accountId is required");
|
|
136
|
-
return {
|
|
137
|
-
channel: CHANNEL_ID,
|
|
138
|
-
messageId: "",
|
|
139
|
-
error: new Error("accountId is required"),
|
|
140
|
-
deliveredAt: Date.now(),
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
// Validate mediaUrl - check for null, undefined, empty string, or invalid URL
|
|
144
|
-
if (mediaUrl === null || mediaUrl === undefined || mediaUrl === "") {
|
|
145
|
-
log.warn("outbound", `sendMedia failed: mediaUrl is invalid (value: ${String(mediaUrl)})`);
|
|
146
|
-
return {
|
|
147
|
-
channel: CHANNEL_ID,
|
|
148
|
-
messageId: "",
|
|
149
|
-
error: new Error(`mediaUrl is required but received: ${mediaUrl === null ? "null" : mediaUrl === undefined ? "undefined" : "empty string"}`),
|
|
150
|
-
deliveredAt: Date.now(),
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
// Check if mediaUrl looks like a valid URL or file path
|
|
154
|
-
const trimmedUrl = String(mediaUrl).trim();
|
|
155
|
-
if (trimmedUrl === "" || trimmedUrl.length < 3) {
|
|
156
|
-
log.warn("outbound", `sendMedia failed: mediaUrl is too short or empty after trim`);
|
|
157
|
-
return {
|
|
158
|
-
channel: CHANNEL_ID,
|
|
159
|
-
messageId: "",
|
|
160
|
-
error: new Error(`mediaUrl appears to be invalid: "${trimmedUrl}"`),
|
|
161
|
-
deliveredAt: Date.now(),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
const account = resolveQQAccount({ cfg });
|
|
165
|
-
if (!account) {
|
|
166
|
-
log.warn("outbound", `sendMedia failed: Account not found: ${accountId}`);
|
|
167
|
-
return {
|
|
168
|
-
channel: CHANNEL_ID,
|
|
169
|
-
messageId: "",
|
|
170
|
-
error: new Error(`Account not found: ${accountId}`),
|
|
171
|
-
deliveredAt: Date.now(),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
const connection = getConnection();
|
|
175
|
-
if (!connection?.isConnected()) {
|
|
176
|
-
log.warn("outbound", `sendMedia failed: Not connected for account: ${accountId}`);
|
|
177
|
-
return {
|
|
178
|
-
channel: CHANNEL_ID,
|
|
179
|
-
messageId: "",
|
|
180
|
-
error: new Error(`Not connected for account: ${accountId}`),
|
|
181
|
-
deliveredAt: Date.now(),
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
// Parse target (format: private:xxx or group:xxx)
|
|
185
|
-
const parts = to.split(":");
|
|
186
|
-
const type = parts[0];
|
|
187
|
-
const id = parts[1];
|
|
188
|
-
const chatType = type === "group" ? "group" : "direct";
|
|
189
|
-
const chatId = id || to;
|
|
190
|
-
log.debug("outbound", `Sending media to ${chatType}:${chatId}, url: ${trimmedUrl.substring(0, 100)}${trimmedUrl.length > 100 ? "..." : ""}`);
|
|
191
|
-
try {
|
|
192
|
-
// Build media segment - NapCat requires 'file' field
|
|
193
|
-
// file can be: URL, file path, or base64
|
|
194
|
-
const mediaSegment = {
|
|
195
|
-
type: "image",
|
|
196
|
-
data: {
|
|
197
|
-
file: trimmedUrl,
|
|
198
|
-
url: trimmedUrl,
|
|
199
|
-
summary: "[图片]",
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
// Build message segments with optional reply
|
|
203
|
-
const messageSegments = [];
|
|
204
|
-
if (replyToId) {
|
|
205
|
-
messageSegments.push({ type: "reply", data: { id: replyToId } });
|
|
206
|
-
}
|
|
207
|
-
messageSegments.push(mediaSegment);
|
|
208
|
-
log.debug("outbound", `Message segments: ${JSON.stringify(messageSegments)}`);
|
|
209
|
-
const response = await sendMsg({
|
|
210
|
-
message_type: chatType === "direct" ? "private" : "group",
|
|
211
|
-
user_id: chatType === "direct" ? chatId : undefined,
|
|
212
|
-
group_id: chatType === "group" ? chatId : undefined,
|
|
213
|
-
message: messageSegments,
|
|
214
|
-
});
|
|
215
|
-
log.debug("outbound", `NapCat response - status: ${response.status}, retcode: ${response.retcode}, msg: ${response.msg ?? "none"}, data: ${JSON.stringify(response.data)}`);
|
|
216
|
-
// Update lastOutboundAt timestamp on successful send
|
|
217
|
-
if (response.status === "ok") {
|
|
218
|
-
setContextStatus({
|
|
219
|
-
lastOutboundAt: Date.now(),
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
if (response.status === "ok" && response.data) {
|
|
223
|
-
const data = response.data;
|
|
224
|
-
log.debug("outbound", `Media sent successfully, message_id: ${data.message_id}`);
|
|
225
|
-
return {
|
|
226
|
-
channel: CHANNEL_ID,
|
|
227
|
-
messageId: messageIdToString(data.message_id),
|
|
228
|
-
deliveredAt: Date.now(),
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
const errorMsg = response.msg || "Send media failed";
|
|
233
|
-
log.warn("outbound", `sendMedia failed - status: ${response.status}, retcode: ${response.retcode}, msg: ${errorMsg}`);
|
|
234
|
-
return {
|
|
235
|
-
channel: CHANNEL_ID,
|
|
236
|
-
messageId: "",
|
|
237
|
-
error: new Error(`NapCat error [${response.retcode ?? "unknown"}]: ${errorMsg}`),
|
|
238
|
-
deliveredAt: Date.now(),
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
catch (error) {
|
|
243
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
244
|
-
log.error("outbound", `sendMedia exception: ${errorMessage}`);
|
|
245
|
-
return {
|
|
246
|
-
channel: CHANNEL_ID,
|
|
247
|
-
messageId: "",
|
|
248
|
-
error: new Error(`sendMedia error: ${errorMessage}`),
|
|
249
|
-
deliveredAt: Date.now(),
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
},
|
|
56
|
+
sendText: outboundSend,
|
|
57
|
+
sendMedia: outboundSend,
|
|
253
58
|
},
|
|
254
59
|
status: {
|
|
255
60
|
buildAccountSnapshot: ({ account, runtime }) => {
|
|
@@ -310,3 +115,65 @@ export const qqPlugin = {
|
|
|
310
115
|
},
|
|
311
116
|
},
|
|
312
117
|
};
|
|
118
|
+
async function outboundSend(ctx) {
|
|
119
|
+
const { to, text, mediaUrl, accountId, replyToId } = ctx;
|
|
120
|
+
log.debug("outbound", `send called - accountId: ${accountId}, to: ${to}, mediaUrl: ${mediaUrl ?? "null"}, replyToId: ${replyToId ?? "none"}`);
|
|
121
|
+
// Parse target (format: private:xxx or group:xxx)
|
|
122
|
+
const parts = to.split(":");
|
|
123
|
+
const [type, id] = parts.length > 1 ? parts : ["private", to];
|
|
124
|
+
const chatType = type === "group" ? "group" : "private";
|
|
125
|
+
const chatId = id || to;
|
|
126
|
+
const content = [];
|
|
127
|
+
if (text) {
|
|
128
|
+
content.push({ type: "text", text });
|
|
129
|
+
}
|
|
130
|
+
if (mediaUrl) {
|
|
131
|
+
switch (getFileType(mediaUrl)) {
|
|
132
|
+
case "image":
|
|
133
|
+
content.push({ type: "image", url: mediaUrl.trim() });
|
|
134
|
+
break;
|
|
135
|
+
case "audio":
|
|
136
|
+
content.push({ type: "audio", path: mediaUrl.trim(), url: mediaUrl.trim(), file: getFileName(mediaUrl.trim()) });
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
content.push({ type: "file", url: mediaUrl.trim(), file: getFileName(mediaUrl.trim()) });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (replyToId) {
|
|
143
|
+
content.push({ type: "reply", messageId: replyToId });
|
|
144
|
+
}
|
|
145
|
+
if (content.length === 0) {
|
|
146
|
+
log.warn("outbound", `send called with no content - accountId: ${accountId}, to: ${to}, mediaUrl: ${mediaUrl ?? "null"}, replyToId: ${replyToId ?? "none"}`);
|
|
147
|
+
return {
|
|
148
|
+
channel: CHANNEL_ID,
|
|
149
|
+
messageId: "",
|
|
150
|
+
error: new Error(`No content to send`),
|
|
151
|
+
deliveredAt: Date.now(),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const response = await sendMsg({
|
|
155
|
+
message_type: chatType,
|
|
156
|
+
user_id: chatType === "private" ? chatId : undefined,
|
|
157
|
+
group_id: chatType === "group" ? chatId : undefined,
|
|
158
|
+
message: openClawToNapCatMessage(content),
|
|
159
|
+
});
|
|
160
|
+
if (response.status === "ok" && response.data) {
|
|
161
|
+
setContextStatus({ lastOutboundAt: Date.now() });
|
|
162
|
+
const data = response.data;
|
|
163
|
+
log.debug("outbound", `send successfully, messageId: ${data.message_id}`);
|
|
164
|
+
return {
|
|
165
|
+
channel: CHANNEL_ID,
|
|
166
|
+
messageId: messageIdToString(data.message_id),
|
|
167
|
+
deliveredAt: Date.now(),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
log.warn("outbound", `send failed, status: ${response.status}, retcode: ${response.retcode}, msg: ${response.msg ?? "none"}`);
|
|
172
|
+
return {
|
|
173
|
+
channel: CHANNEL_ID,
|
|
174
|
+
messageId: "",
|
|
175
|
+
error: new Error(response.msg || "Send failed"),
|
|
176
|
+
deliveredAt: Date.now(),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -18,7 +18,7 @@ async function contentToPlainText(content) {
|
|
|
18
18
|
.map((c) => {
|
|
19
19
|
switch (c.type) {
|
|
20
20
|
case 'text':
|
|
21
|
-
return c.text
|
|
21
|
+
return `[消息]\n${c.text}`;
|
|
22
22
|
case 'at':
|
|
23
23
|
return c.isAll ? '@全体成员' : `@${c.userId}`;
|
|
24
24
|
case 'json':
|
|
@@ -63,6 +63,7 @@ async function contextToMedia(content) {
|
|
|
63
63
|
}
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
|
+
// TODO弃用
|
|
66
67
|
async function contextToReply(content) {
|
|
67
68
|
const hasReply = content.some(c => c.type === 'reply');
|
|
68
69
|
if (!hasReply) {
|
|
@@ -86,6 +87,22 @@ async function contextToReply(content) {
|
|
|
86
87
|
sender: String(response.data?.sender.user_id)
|
|
87
88
|
};
|
|
88
89
|
}
|
|
90
|
+
async function sendText(isGroup, chatId, text) {
|
|
91
|
+
const cleanText = text.replace(/NO_REPLY\s*$/, '');
|
|
92
|
+
const messageSegments = [{ type: 'text', data: { text: markdownToText(cleanText) } }];
|
|
93
|
+
try {
|
|
94
|
+
await sendMsg({
|
|
95
|
+
message_type: isGroup ? 'group' : 'private',
|
|
96
|
+
group_id: isGroup ? chatId : undefined,
|
|
97
|
+
user_id: !isGroup ? chatId : undefined,
|
|
98
|
+
message: messageSegments,
|
|
99
|
+
});
|
|
100
|
+
log.info('dispatch', `Sent reply: ${text.slice(0, 100)}`);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
log.error('dispatch', `Send failed: ${error}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
89
106
|
/**
|
|
90
107
|
* Dispatch an incoming message to the AI for processing
|
|
91
108
|
*/
|
|
@@ -103,13 +120,7 @@ export async function dispatchMessage(params) {
|
|
|
103
120
|
}
|
|
104
121
|
const isGroup = chatType === 'group';
|
|
105
122
|
const peerId = isGroup ? `group:${chatId}` : senderId;
|
|
106
|
-
|
|
107
|
-
// 输入状态
|
|
108
|
-
await setInputStatus({
|
|
109
|
-
user_id: senderId,
|
|
110
|
-
event_type: 1
|
|
111
|
-
});
|
|
112
|
-
}
|
|
123
|
+
const fullContent = `${content}\n\nFrom QQ(${senderId}) - Nickname: ${senderName}`;
|
|
113
124
|
const route = runtime.channel.routing.resolveAgentRoute({
|
|
114
125
|
cfg: context.cfg,
|
|
115
126
|
channel: CHANNEL_ID,
|
|
@@ -122,7 +133,7 @@ export async function dispatchMessage(params) {
|
|
|
122
133
|
const body = runtime.channel.reply.formatInboundEnvelope({
|
|
123
134
|
channel: CHANNEL_ID,
|
|
124
135
|
from: senderName || senderId,
|
|
125
|
-
body:
|
|
136
|
+
body: fullContent,
|
|
126
137
|
timestamp,
|
|
127
138
|
chatType: isGroup ? 'group' : 'direct',
|
|
128
139
|
sender: {
|
|
@@ -135,8 +146,8 @@ export async function dispatchMessage(params) {
|
|
|
135
146
|
const toAddress = `qq:${route.accountId}`;
|
|
136
147
|
const ctxPayload = runtime.channel.reply.finalizeInboundContext({
|
|
137
148
|
Body: body,
|
|
138
|
-
RawBody:
|
|
139
|
-
CommandBody:
|
|
149
|
+
RawBody: fullContent,
|
|
150
|
+
CommandBody: fullContent,
|
|
140
151
|
From: fromAddress,
|
|
141
152
|
To: toAddress,
|
|
142
153
|
SessionKey: route.sessionKey,
|
|
@@ -159,24 +170,7 @@ export async function dispatchMessage(params) {
|
|
|
159
170
|
OriginatingTo: toAddress,
|
|
160
171
|
});
|
|
161
172
|
log.info('dispatch', `Dispatching to agent ${route.agentId}, session: ${route.sessionKey}`);
|
|
162
|
-
const sendReply = async (text) => {
|
|
163
|
-
const messageSegments = [{ type: 'text', data: { text: markdownToText(text) } }];
|
|
164
|
-
try {
|
|
165
|
-
await sendMsg({
|
|
166
|
-
message_type: isGroup ? 'group' : 'private',
|
|
167
|
-
group_id: isGroup ? chatId : undefined,
|
|
168
|
-
user_id: !isGroup ? chatId : undefined,
|
|
169
|
-
message: messageSegments,
|
|
170
|
-
});
|
|
171
|
-
log.info('dispatch', `Sent reply: ${text.slice(0, 100)}`);
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
log.error('dispatch', `Send failed: ${error}`);
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
173
|
const messagesConfig = runtime.channel.reply.resolveEffectiveMessagesConfig(context.cfg, route.agentId);
|
|
178
|
-
log.info('dispatch', `Messages config: ${JSON.stringify(messagesConfig)}`);
|
|
179
|
-
let hasResponse = false;
|
|
180
174
|
try {
|
|
181
175
|
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
182
176
|
ctx: ctxPayload,
|
|
@@ -186,22 +180,29 @@ export async function dispatchMessage(params) {
|
|
|
186
180
|
mode: "off"
|
|
187
181
|
},
|
|
188
182
|
responsePrefix: messagesConfig.responsePrefix,
|
|
183
|
+
onReplyStart: async () => {
|
|
184
|
+
if (!isGroup) {
|
|
185
|
+
// 输入状态
|
|
186
|
+
await setInputStatus({
|
|
187
|
+
user_id: senderId,
|
|
188
|
+
event_type: 1
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
},
|
|
189
192
|
deliver: async (payload, info) => {
|
|
190
|
-
hasResponse = true;
|
|
191
193
|
log.info('dispatch', `deliver(${info.kind}): ${JSON.stringify(payload)}`);
|
|
192
194
|
if (payload.text) {
|
|
193
|
-
await
|
|
195
|
+
await sendText(isGroup, chatId, payload.text);
|
|
194
196
|
}
|
|
195
197
|
},
|
|
196
198
|
onError: async (err) => {
|
|
197
|
-
hasResponse = true;
|
|
198
199
|
log.error('dispatch', `Dispatch error: ${err}`);
|
|
199
|
-
await
|
|
200
|
+
await sendText(isGroup, chatId, `[错误]\n${String(err)}`);
|
|
200
201
|
},
|
|
201
202
|
},
|
|
202
203
|
replyOptions: {},
|
|
203
204
|
});
|
|
204
|
-
log.info('dispatch', `Dispatch completed
|
|
205
|
+
log.info('dispatch', `Dispatch completed`);
|
|
205
206
|
}
|
|
206
207
|
catch (error) {
|
|
207
208
|
log.error('dispatch', `Message processing failed: ${error}`);
|
|
@@ -279,7 +280,7 @@ export async function handlePokeEvent(event) {
|
|
|
279
280
|
senderId: String(event.user_id),
|
|
280
281
|
senderName: String(event.user_id),
|
|
281
282
|
messageId: `poke_${event.user_id}_${Date.now()}`,
|
|
282
|
-
content: `[动作]
|
|
283
|
+
content: `[动作]\n${pokeMessage}`,
|
|
283
284
|
timestamp: Date.now(),
|
|
284
285
|
});
|
|
285
286
|
}
|
|
@@ -141,7 +141,9 @@ export interface OpenClawJsonContent {
|
|
|
141
141
|
}
|
|
142
142
|
export interface OpenClawFileContent {
|
|
143
143
|
type: 'file';
|
|
144
|
-
fileId
|
|
144
|
+
fileId?: string;
|
|
145
|
+
file?: string;
|
|
146
|
+
url?: string;
|
|
145
147
|
fileSize?: number;
|
|
146
148
|
}
|
|
147
149
|
export type OpenClawMessage = OpenClawTextContent | OpenClawAtContent | OpenClawImageContent | OpenClawReplyContent | OpenClawAudioContent | OpenClawJsonContent | OpenClawFileContent;
|
|
@@ -46,6 +46,9 @@ export declare function chunk<T>(array: T[], size: number): T[][];
|
|
|
46
46
|
* Get a human-readable message for a WebSocket close code
|
|
47
47
|
*/
|
|
48
48
|
export declare function getCloseCodeMessage(code: number): string;
|
|
49
|
+
export type FileCategory = 'image' | 'audio' | 'file';
|
|
50
|
+
export declare function getFileType(pathOrUrl: string): FileCategory;
|
|
51
|
+
export declare function getFileName(pathOrUrl: string): string;
|
|
49
52
|
export { CQCodeUtils, CQNode } from './cqcode.js';
|
|
50
53
|
export { Logger } from './log.js';
|
|
51
54
|
export { MarkdownToText, markdownToText, } from './markdown.js';
|
package/dist/src/utils/index.js
CHANGED
|
@@ -236,6 +236,65 @@ export function getCloseCodeMessage(code) {
|
|
|
236
236
|
};
|
|
237
237
|
return messages[code] ?? `Unknown close code: ${code}`;
|
|
238
238
|
}
|
|
239
|
+
const IMAGE_EXTENSIONS = new Set([
|
|
240
|
+
'jpg', 'jpeg', 'png', 'gif', 'bmp',
|
|
241
|
+
'webp', 'svg', 'tiff', 'ico', 'heic'
|
|
242
|
+
]);
|
|
243
|
+
const AUDIO_EXTENSIONS = new Set([
|
|
244
|
+
'mp3', // 最通用
|
|
245
|
+
'wav', // 无损/未压缩
|
|
246
|
+
'ogg', // 开源/Web常用
|
|
247
|
+
'm4a', // Apple/MPEG-4 音频
|
|
248
|
+
'aac', // 高级音频编码
|
|
249
|
+
'flac', // 无损压缩
|
|
250
|
+
'wma', // Windows Media
|
|
251
|
+
'aiff', // Apple Interchange
|
|
252
|
+
'amr', // 移动端录音
|
|
253
|
+
'opus' // 现代Web流媒体
|
|
254
|
+
]);
|
|
255
|
+
export function getFileType(pathOrUrl) {
|
|
256
|
+
if (!pathOrUrl)
|
|
257
|
+
return 'file';
|
|
258
|
+
try {
|
|
259
|
+
const cleanPath = pathOrUrl.split(/[?#]/)[0];
|
|
260
|
+
const lastDotIndex = cleanPath.lastIndexOf('.');
|
|
261
|
+
if (lastDotIndex === -1) {
|
|
262
|
+
return 'file';
|
|
263
|
+
}
|
|
264
|
+
const extension = cleanPath.substring(lastDotIndex + 1).toLowerCase();
|
|
265
|
+
if (IMAGE_EXTENSIONS.has(extension)) {
|
|
266
|
+
return 'image';
|
|
267
|
+
}
|
|
268
|
+
if (AUDIO_EXTENSIONS.has(extension)) {
|
|
269
|
+
return 'audio';
|
|
270
|
+
}
|
|
271
|
+
return 'file';
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return 'file';
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export function getFileName(pathOrUrl) {
|
|
278
|
+
if (!pathOrUrl)
|
|
279
|
+
return '';
|
|
280
|
+
try {
|
|
281
|
+
let cleanPath = pathOrUrl.split(/[?#]/)[0];
|
|
282
|
+
try {
|
|
283
|
+
cleanPath = decodeURIComponent(cleanPath);
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
}
|
|
287
|
+
cleanPath = cleanPath.replace(/\\/g, '/');
|
|
288
|
+
if (cleanPath.endsWith('/')) {
|
|
289
|
+
cleanPath = cleanPath.slice(0, -1);
|
|
290
|
+
}
|
|
291
|
+
const fileName = cleanPath.split('/').pop();
|
|
292
|
+
return fileName || '';
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
return '';
|
|
296
|
+
}
|
|
297
|
+
}
|
|
239
298
|
// =============================================================================
|
|
240
299
|
// CQ Code Utilities
|
|
241
300
|
// =============================================================================
|