openclaw-extension-typex 1.0.16 → 1.0.18
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/client/client.d.ts +41 -3
- package/dist/client/client.js +241 -131
- package/dist/client/message-helpers.d.ts +24 -0
- package/dist/client/message-helpers.js +125 -0
- package/dist/client/message.d.ts +10 -3
- package/dist/client/message.js +340 -61
- package/dist/client/monitor.d.ts +1 -1
- package/dist/client/monitor.js +172 -73
- package/dist/client/outbound.js +8 -6
- package/dist/client/send.d.ts +15 -1
- package/dist/client/send.js +75 -10
- package/dist/client/types.d.ts +52 -3
- package/dist/client/types.js +11 -0
- package/dist/index.d.ts +2 -2
- package/dist/onboarding.js +76 -19
- package/dist/plugin.d.ts +2 -2
- package/dist/plugin.js +7 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/client/client.d.ts
CHANGED
|
@@ -1,14 +1,52 @@
|
|
|
1
|
-
import { TypeXMessageEnum, type TypeXClientOptions } from "./types.js";
|
|
1
|
+
import { TypeXMessageEnum, type TypeXClientOptions, type TypeXMessageEntry } from "./types.js";
|
|
2
2
|
export declare class TypeXClient {
|
|
3
3
|
private options;
|
|
4
4
|
private accessToken?;
|
|
5
5
|
private userId?;
|
|
6
6
|
constructor(options: TypeXClientOptions);
|
|
7
|
+
get mode(): "user" | "bot";
|
|
7
8
|
getAccessToken(): Promise<string>;
|
|
8
9
|
getCurUserId(): Promise<string>;
|
|
9
10
|
fetchQrcodeUrl(): Promise<any>;
|
|
10
11
|
checkLoginStatus(qrcodeId: string): Promise<boolean>;
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Send a message to a specific chat (group or DM).
|
|
14
|
+
* @param to chat_id to send to
|
|
15
|
+
* @param content message text or object
|
|
16
|
+
*/
|
|
17
|
+
sendMessage(to: string, content: string | object, msgType?: TypeXMessageEnum, options?: {
|
|
18
|
+
replyMsgId?: string;
|
|
19
|
+
}): Promise<any>;
|
|
20
|
+
/**
|
|
21
|
+
* Upload resource for the robot to send.
|
|
22
|
+
* @param fileName Name of the file
|
|
23
|
+
* @param fileType "image" | "audio" | "video" | "application"
|
|
24
|
+
* @param fileContent Buffer or Blob containing the file data
|
|
25
|
+
* @param chatId Optional chat_id
|
|
26
|
+
*/
|
|
27
|
+
uploadResource(fileName: string, fileType: "image" | "audio" | "video" | "application", fileContent: Buffer | Blob, chatId?: string): Promise<any>;
|
|
28
|
+
/**
|
|
29
|
+
* Fetch messages. Dispatches to user or bot endpoint based on mode.
|
|
30
|
+
*/
|
|
31
|
+
fetchMessages(pos: number): Promise<TypeXMessageEntry[]>;
|
|
32
|
+
/** Pull messages for a regular user account (sessionid cookie auth). */
|
|
33
|
+
private fetchUserMessages;
|
|
34
|
+
/**
|
|
35
|
+
* Pull messages for a bot account (Bearer token auth).
|
|
36
|
+
* TODO: replace /open/bot/message with the actual endpoint path once confirmed.
|
|
37
|
+
*/
|
|
38
|
+
private fetchBotMessages;
|
|
39
|
+
/**
|
|
40
|
+
* Fetch a single message by ID (used to resolve quoted/parent messages).
|
|
41
|
+
*/
|
|
42
|
+
getMessage(messageId: string): Promise<TypeXMessageEntry | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Fetch file binary stream from TypeX using object_key.
|
|
45
|
+
* Requires Bot Token authentication.
|
|
46
|
+
*/
|
|
47
|
+
fetchFileBuffer(objectKey: string, size?: string): Promise<{
|
|
48
|
+
buffer: Buffer;
|
|
49
|
+
mimeType: string;
|
|
50
|
+
} | null>;
|
|
13
51
|
}
|
|
14
52
|
export declare function getTypeXClient(accountId?: string, manualOptions?: TypeXClientOptions): TypeXClient;
|
package/dist/client/client.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TypeXClient = void 0;
|
|
4
4
|
exports.getTypeXClient = getTypeXClient;
|
|
5
|
+
const types_js_1 = require("./types.js");
|
|
5
6
|
const TYPEX_DOMAIN = "https://api-coco.typex.im";
|
|
6
7
|
// const TYPEX_DOMAIN = "https://api-tx.bossjob.net.cn";
|
|
7
8
|
let prompter;
|
|
@@ -15,191 +16,300 @@ class TypeXClient {
|
|
|
15
16
|
this.accessToken = options.token;
|
|
16
17
|
}
|
|
17
18
|
}
|
|
19
|
+
get mode() {
|
|
20
|
+
return this.options.mode ?? "user";
|
|
21
|
+
}
|
|
18
22
|
async getAccessToken() {
|
|
19
|
-
|
|
20
|
-
return this.accessToken;
|
|
21
|
-
}
|
|
22
|
-
return "";
|
|
23
|
+
return this.accessToken ?? "";
|
|
23
24
|
}
|
|
24
25
|
async getCurUserId() {
|
|
25
|
-
|
|
26
|
-
return this.userId;
|
|
27
|
-
}
|
|
28
|
-
return "";
|
|
26
|
+
return this.userId ?? "";
|
|
29
27
|
}
|
|
30
28
|
async fetchQrcodeUrl() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
throw new Error(`Failed to get QR code: ${qrResponse.statusText}`);
|
|
39
|
-
}
|
|
40
|
-
const qrResult = await qrResponse.json();
|
|
41
|
-
if (qrResult.code !== 0 || !qrResult.data) {
|
|
42
|
-
throw new Error(`Failed to get QR code: ${qrResult.msg}`);
|
|
43
|
-
}
|
|
44
|
-
return qrResult.data;
|
|
29
|
+
const qrResponse = await fetch(`${TYPEX_DOMAIN}/user/qrcode?login_type=open`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({}),
|
|
33
|
+
});
|
|
34
|
+
if (!qrResponse.ok) {
|
|
35
|
+
throw new Error(`Failed to get QR code: ${qrResponse.statusText}`);
|
|
45
36
|
}
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
const qrResult = await qrResponse.json();
|
|
38
|
+
if (qrResult.code !== 0 || !qrResult.data) {
|
|
39
|
+
throw new Error(`Failed to get QR code: ${qrResult.msg}`);
|
|
48
40
|
}
|
|
41
|
+
return qrResult.data;
|
|
49
42
|
}
|
|
50
43
|
async checkLoginStatus(qrcodeId) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const setCookieHeader = checkRes.headers.get("set-cookie");
|
|
62
|
-
if (setCookieHeader) {
|
|
63
|
-
const match = setCookieHeader.match(/(sessionid=[^;]+)/);
|
|
64
|
-
if (match && match[1]) {
|
|
65
|
-
this.accessToken = match[1];
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const checkData = await checkRes.json();
|
|
69
|
-
if (checkData.code === 0) {
|
|
70
|
-
const { user_id } = checkData.data;
|
|
71
|
-
this.userId = user_id;
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
else if (checkData.code === 10001) {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
44
|
+
const checkRes = await fetch(`${TYPEX_DOMAIN}/open/qrcode/check_auth`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
body: JSON.stringify({ qr_code_id: qrcodeId }),
|
|
48
|
+
});
|
|
49
|
+
const setCookieHeader = checkRes.headers.get("set-cookie");
|
|
50
|
+
if (setCookieHeader) {
|
|
51
|
+
const match = setCookieHeader.match(/(sessionid=[^;]+)/);
|
|
52
|
+
if (match?.[1])
|
|
53
|
+
this.accessToken = match[1];
|
|
80
54
|
}
|
|
81
|
-
|
|
82
|
-
|
|
55
|
+
const checkData = await checkRes.json();
|
|
56
|
+
if (checkData.code === 0) {
|
|
57
|
+
this.userId = checkData.data.user_id;
|
|
58
|
+
return true;
|
|
83
59
|
}
|
|
60
|
+
return false;
|
|
84
61
|
}
|
|
85
|
-
|
|
86
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Send a message to a specific chat (group or DM).
|
|
64
|
+
* @param to chat_id to send to
|
|
65
|
+
* @param content message text or object
|
|
66
|
+
*/
|
|
67
|
+
async sendMessage(to, content, msgType = 0, options = {}) {
|
|
87
68
|
const token = this.accessToken;
|
|
88
|
-
if (!token)
|
|
69
|
+
if (!token)
|
|
89
70
|
throw new Error("TypeXClient: Not authenticated.");
|
|
90
|
-
}
|
|
91
71
|
let finalContent = content;
|
|
92
72
|
if (typeof content === "object") {
|
|
93
73
|
try {
|
|
94
74
|
finalContent = JSON.stringify(content);
|
|
95
75
|
}
|
|
96
|
-
catch
|
|
97
|
-
if (e instanceof Error) {
|
|
98
|
-
if (prompter)
|
|
99
|
-
prompter.note("Failed to stringify message content");
|
|
100
|
-
else
|
|
101
|
-
console.log("Failed to stringify message content");
|
|
102
|
-
}
|
|
76
|
+
catch {
|
|
103
77
|
finalContent = String(content);
|
|
104
78
|
}
|
|
105
79
|
}
|
|
106
80
|
if (prompter)
|
|
107
|
-
prompter.note(`TypeXClient sending
|
|
81
|
+
prompter.note(`TypeXClient sending to ${to}: ${String(finalContent).slice(0, 80)}`);
|
|
108
82
|
else
|
|
109
|
-
console.log(`TypeXClient sending
|
|
83
|
+
console.log(`TypeXClient sending to ${to}: ${String(finalContent).slice(0, 80)}`);
|
|
84
|
+
const isBot = this.mode === "bot";
|
|
85
|
+
const endpoint = isBot ? "/open/robot/send_message" : "/open/claw/send_message";
|
|
86
|
+
let payloadStr;
|
|
87
|
+
if (isBot) {
|
|
88
|
+
let botContentObj;
|
|
89
|
+
if (msgType === types_js_1.TypeXMessageEnum.text || msgType === types_js_1.TypeXMessageEnum.richText) {
|
|
90
|
+
// According to docs, text type content format: {"text":"test"}
|
|
91
|
+
// Assuming content or finalContent holds the actual string text.
|
|
92
|
+
botContentObj = {
|
|
93
|
+
text: typeof content === "string" ? content : (typeof finalContent === "string" ? finalContent : JSON.stringify(content))
|
|
94
|
+
};
|
|
95
|
+
// Ensure msgType is 0 when sending to `/open/robot/send_message` since 8 might not be supported natively by robot API
|
|
96
|
+
msgType = types_js_1.TypeXMessageEnum.text;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// Image or File object payload for bot
|
|
100
|
+
botContentObj = typeof finalContent === "string" ? { text: finalContent } : content;
|
|
101
|
+
}
|
|
102
|
+
payloadStr = JSON.stringify({
|
|
103
|
+
chat_id: to,
|
|
104
|
+
content: botContentObj,
|
|
105
|
+
msg_type: msgType,
|
|
106
|
+
reply_msg_id: options.replyMsgId || "0",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
payloadStr = JSON.stringify({
|
|
111
|
+
chat_id: to,
|
|
112
|
+
content: { text: finalContent },
|
|
113
|
+
msg_type: msgType,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const response = await fetch(`${TYPEX_DOMAIN}${endpoint}`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: {
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
...(isBot ? { Authorization: `Bearer ${token}` } : { Cookie: token }),
|
|
121
|
+
},
|
|
122
|
+
body: payloadStr,
|
|
123
|
+
});
|
|
124
|
+
const bodyText = await response.text();
|
|
125
|
+
let resJson;
|
|
110
126
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
127
|
+
resJson = JSON.parse(bodyText);
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
throw new Error(`Send message failed (invalid JSON): HTTP ${response.status} - ${bodyText}`);
|
|
131
|
+
}
|
|
132
|
+
if (resJson.code !== 0) {
|
|
133
|
+
throw new Error(`Send message failed: [${resJson.code}] ${resJson.msg || resJson.message}`);
|
|
134
|
+
}
|
|
135
|
+
return resJson.data || { message_id: `msg_${Date.now()}` };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Upload resource for the robot to send.
|
|
139
|
+
* @param fileName Name of the file
|
|
140
|
+
* @param fileType "image" | "audio" | "video" | "application"
|
|
141
|
+
* @param fileContent Buffer or Blob containing the file data
|
|
142
|
+
* @param chatId Optional chat_id
|
|
143
|
+
*/
|
|
144
|
+
async uploadResource(fileName, fileType, fileContent, chatId) {
|
|
145
|
+
if (this.mode !== "bot" || !this.accessToken) {
|
|
146
|
+
throw new Error("TypeXClient: uploadResource requires bot mode and an access token.");
|
|
147
|
+
}
|
|
148
|
+
const formData = new FormData();
|
|
149
|
+
if (chatId)
|
|
150
|
+
formData.append("chat_id", chatId);
|
|
151
|
+
formData.append("file_name", fileName);
|
|
152
|
+
formData.append("file_type", fileType);
|
|
153
|
+
// Node.js fetch implementation of FormData requires a Blob-like object for files.
|
|
154
|
+
// By providing a Blob we ensure it correctly adds boundaries and content types per form part.
|
|
155
|
+
const blob = fileContent instanceof Buffer ? new Blob([fileContent]) : fileContent;
|
|
156
|
+
formData.append("file_content", blob, fileName);
|
|
157
|
+
const response = await fetch(`${TYPEX_DOMAIN}/open/robot/upload`, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: {
|
|
160
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
161
|
+
// Note: fetch will automatically set the Content-Type boundary
|
|
162
|
+
},
|
|
163
|
+
body: formData,
|
|
164
|
+
});
|
|
165
|
+
const resJson = await response.json();
|
|
166
|
+
if (resJson.code !== 0) {
|
|
167
|
+
throw new Error(`Upload resource failed: [${resJson.code}] ${resJson.msg || resJson.message}`);
|
|
168
|
+
}
|
|
169
|
+
return resJson.data;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Fetch messages. Dispatches to user or bot endpoint based on mode.
|
|
173
|
+
*/
|
|
174
|
+
async fetchMessages(pos) {
|
|
175
|
+
return this.mode === "bot"
|
|
176
|
+
? this.fetchBotMessages()
|
|
177
|
+
: this.fetchUserMessages(pos);
|
|
178
|
+
}
|
|
179
|
+
/** Pull messages for a regular user account (sessionid cookie auth). */
|
|
180
|
+
async fetchUserMessages(pos) {
|
|
181
|
+
if (!this.accessToken)
|
|
182
|
+
return [];
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch(`${TYPEX_DOMAIN}/open/claw/message`, {
|
|
113
185
|
method: "POST",
|
|
114
|
-
headers: {
|
|
115
|
-
|
|
116
|
-
Cookie: token,
|
|
117
|
-
},
|
|
118
|
-
body: JSON.stringify({
|
|
119
|
-
content: {
|
|
120
|
-
text: finalContent,
|
|
121
|
-
},
|
|
122
|
-
msg_type: msgType,
|
|
123
|
-
}),
|
|
186
|
+
headers: { Cookie: this.accessToken, "Content-Type": "application/json" },
|
|
187
|
+
body: JSON.stringify({ pos }),
|
|
124
188
|
});
|
|
189
|
+
if (response.status === 401) {
|
|
190
|
+
throw new Error("Unauthorized: 401 Token is invalid or expired.");
|
|
191
|
+
}
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
throw new Error(`HTTP Error: ${response.status}`);
|
|
194
|
+
}
|
|
125
195
|
const resJson = await response.json();
|
|
126
196
|
if (resJson.code !== 0) {
|
|
127
|
-
throw new Error(`
|
|
197
|
+
throw new Error(`API Error: code ${resJson.code}, message: ${resJson.msg}`);
|
|
128
198
|
}
|
|
129
|
-
|
|
130
|
-
prompter.note("Message sent successfully", resJson.data);
|
|
131
|
-
else
|
|
132
|
-
console.log("Message sent successfully", JSON.stringify(resJson.data));
|
|
133
|
-
return (resJson.data || {
|
|
134
|
-
message_id: `msg_${Date.now()}`,
|
|
135
|
-
});
|
|
199
|
+
return Array.isArray(resJson.data) ? resJson.data : [];
|
|
136
200
|
}
|
|
137
|
-
catch (
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
201
|
+
catch (e) {
|
|
202
|
+
if (e instanceof Error && e.message.includes("Unauthorized")) {
|
|
203
|
+
throw e;
|
|
204
|
+
}
|
|
205
|
+
console.log(`Fetch messages error: ${e}`);
|
|
206
|
+
return [];
|
|
143
207
|
}
|
|
144
208
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Pull messages for a bot account (Bearer token auth).
|
|
211
|
+
* TODO: replace /open/bot/message with the actual endpoint path once confirmed.
|
|
212
|
+
*/
|
|
213
|
+
async fetchBotMessages() {
|
|
214
|
+
if (!this.accessToken)
|
|
151
215
|
return [];
|
|
152
|
-
}
|
|
153
216
|
try {
|
|
154
|
-
const
|
|
155
|
-
if (prompter)
|
|
156
|
-
prompter.note(`Fetching messages from pos: ${pos}`);
|
|
157
|
-
// else console.log(`Fetching messages from pos: ${pos}`);
|
|
158
|
-
const response = await fetch(url, {
|
|
217
|
+
const response = await fetch(`${TYPEX_DOMAIN}/open/robot/message/pull`, {
|
|
159
218
|
method: "POST",
|
|
160
|
-
headers: {
|
|
161
|
-
|
|
162
|
-
"Content-Type": "application/json",
|
|
163
|
-
},
|
|
164
|
-
body: JSON.stringify({ pos: pos }),
|
|
219
|
+
headers: { Authorization: `Bearer ${this.accessToken}`, "Content-Type": "application/json" },
|
|
220
|
+
body: JSON.stringify({ limit: 5 }),
|
|
165
221
|
});
|
|
222
|
+
if (response.status === 401) {
|
|
223
|
+
throw new Error("Unauthorized: 401 Bot Token is invalid or expired.");
|
|
224
|
+
}
|
|
225
|
+
if (!response.ok) {
|
|
226
|
+
throw new Error(`HTTP Error: ${response.status}`);
|
|
227
|
+
}
|
|
166
228
|
const resJson = await response.json();
|
|
167
229
|
if (resJson.code !== 0) {
|
|
168
|
-
if (prompter)
|
|
169
|
-
prompter.note(`Fetch failed with code ${resJson.code}: ${resJson.message}`);
|
|
170
|
-
else
|
|
171
|
-
console.log(`Fetch failed with code ${resJson.code}: ${resJson.message}`);
|
|
172
230
|
return [];
|
|
173
231
|
}
|
|
174
|
-
|
|
175
|
-
|
|
232
|
+
return Array.isArray(resJson.data?.messages) ? resJson.data.messages : [];
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
if (e instanceof Error && e.message.includes("Unauthorized")) {
|
|
236
|
+
throw e;
|
|
176
237
|
}
|
|
238
|
+
console.log(`Bot fetch messages error: ${e}`);
|
|
177
239
|
return [];
|
|
178
240
|
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Fetch a single message by ID (used to resolve quoted/parent messages).
|
|
244
|
+
*/
|
|
245
|
+
async getMessage(messageId) {
|
|
246
|
+
if (!this.accessToken)
|
|
247
|
+
return null;
|
|
248
|
+
try {
|
|
249
|
+
const isBot = this.mode === "bot";
|
|
250
|
+
const response = await fetch(`${TYPEX_DOMAIN}/open/claw/message/${messageId}`, {
|
|
251
|
+
method: "GET",
|
|
252
|
+
headers: isBot
|
|
253
|
+
? { Authorization: `Bearer ${this.accessToken}`, "Content-Type": "application/json" }
|
|
254
|
+
: { Cookie: this.accessToken, "Content-Type": "application/json" },
|
|
255
|
+
});
|
|
256
|
+
const resJson = await response.json();
|
|
257
|
+
return resJson.code === 0 && resJson.data ? resJson.data : null;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Fetch file binary stream from TypeX using object_key.
|
|
265
|
+
* Requires Bot Token authentication.
|
|
266
|
+
*/
|
|
267
|
+
async fetchFileBuffer(objectKey, size) {
|
|
268
|
+
if (!this.accessToken || this.mode !== "bot")
|
|
269
|
+
return null;
|
|
270
|
+
try {
|
|
271
|
+
const query = new URLSearchParams({ object_key: objectKey });
|
|
272
|
+
if (size)
|
|
273
|
+
query.append("size", size);
|
|
274
|
+
const url = `${TYPEX_DOMAIN}/open/robot/chat/file?${query.toString()}`;
|
|
275
|
+
const response = await fetch(url, {
|
|
276
|
+
method: "GET",
|
|
277
|
+
headers: { Authorization: `Bearer ${this.accessToken}` },
|
|
278
|
+
});
|
|
279
|
+
if (!response.ok) {
|
|
280
|
+
console.log(`fetchFileBuffer failed with status: ${response.status} ${response.statusText} for url: ${url}`);
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
284
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
285
|
+
const mimeType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
286
|
+
return { buffer, mimeType };
|
|
287
|
+
}
|
|
179
288
|
catch (e) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
else
|
|
183
|
-
console.log(`Fetch messages network error: ${e}`);
|
|
184
|
-
return [];
|
|
289
|
+
console.log(`fetchFileBuffer error: ${e}`);
|
|
290
|
+
return null;
|
|
185
291
|
}
|
|
186
292
|
}
|
|
187
293
|
}
|
|
188
294
|
exports.TypeXClient = TypeXClient;
|
|
189
295
|
function getTypeXClient(accountId, manualOptions) {
|
|
190
296
|
const typexCfg = (manualOptions?.typexCfg ?? {});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
prompter = clawPrompter;
|
|
194
|
-
}
|
|
297
|
+
if (manualOptions?.prompter)
|
|
298
|
+
prompter = manualOptions.prompter;
|
|
195
299
|
let token = manualOptions?.token;
|
|
300
|
+
let mode = manualOptions?.mode ?? "user";
|
|
196
301
|
if (accountId && typexCfg.accounts?.[accountId]) {
|
|
197
|
-
|
|
302
|
+
const acctCfg = typexCfg.accounts[accountId];
|
|
303
|
+
token = token ?? acctCfg.token;
|
|
304
|
+
if (acctCfg.mode === "bot" || acctCfg.mode === "user")
|
|
305
|
+
mode = acctCfg.mode;
|
|
198
306
|
}
|
|
307
|
+
// Config check: outbound sends should fail only when we truly lack credentials.
|
|
308
|
+
// Historically this was a stub that always threw, which broke outbound delivery.
|
|
199
309
|
if (!manualOptions?.skipConfigCheck) {
|
|
200
|
-
|
|
310
|
+
if (!token?.trim()) {
|
|
311
|
+
throw new Error("TypeX not configured: missing token. Run the TypeX onboarding (QR login / bot token) or set channels.openclaw-extension-typex.accounts.<accountId>.token");
|
|
312
|
+
}
|
|
201
313
|
}
|
|
202
|
-
return new TypeXClient({
|
|
203
|
-
token: token,
|
|
204
|
-
});
|
|
314
|
+
return new TypeXClient({ token, mode });
|
|
205
315
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* message-helpers.ts
|
|
3
|
+
*
|
|
4
|
+
* Pure, stateless helper functions for TypeX message processing.
|
|
5
|
+
* All functions are free of side-effects and easy to unit-test.
|
|
6
|
+
*/
|
|
7
|
+
import { type TypeXMessageEntry, type TypeXMention } from "./types.js";
|
|
8
|
+
export declare function normalizeMessageToText(msg: TypeXMessageEntry): string;
|
|
9
|
+
export declare function checkBotMentioned(msg: TypeXMessageEntry, botId?: string): boolean;
|
|
10
|
+
export declare function stripBotMention(text: string, mentions?: TypeXMention[]): string;
|
|
11
|
+
export type TypeXGroupConfig = {
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
requireMention?: boolean;
|
|
14
|
+
allowFrom?: Array<string | number>;
|
|
15
|
+
};
|
|
16
|
+
export declare function resolveGroupConfig(typexCfg: Record<string, any>, groupId: string): TypeXGroupConfig | undefined;
|
|
17
|
+
export declare function normalizeAllowEntry(raw: string): string;
|
|
18
|
+
export declare function isAllowedBySenderId(allowFrom: Array<string | number>, senderId: string): boolean;
|
|
19
|
+
export declare function buildAgentBody(params: {
|
|
20
|
+
messageId: string;
|
|
21
|
+
senderLabel: string;
|
|
22
|
+
content: string;
|
|
23
|
+
quotedContent?: string;
|
|
24
|
+
}): string;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* message-helpers.ts
|
|
4
|
+
*
|
|
5
|
+
* Pure, stateless helper functions for TypeX message processing.
|
|
6
|
+
* All functions are free of side-effects and easy to unit-test.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.normalizeMessageToText = normalizeMessageToText;
|
|
10
|
+
exports.checkBotMentioned = checkBotMentioned;
|
|
11
|
+
exports.stripBotMention = stripBotMention;
|
|
12
|
+
exports.resolveGroupConfig = resolveGroupConfig;
|
|
13
|
+
exports.normalizeAllowEntry = normalizeAllowEntry;
|
|
14
|
+
exports.isAllowedBySenderId = isAllowedBySenderId;
|
|
15
|
+
exports.buildAgentBody = buildAgentBody;
|
|
16
|
+
const types_js_1 = require("./types.js");
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Message content normalisation
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
function normalizeMessageToText(msg) {
|
|
21
|
+
const typeNum = Number(msg.msg_type);
|
|
22
|
+
switch (typeNum) {
|
|
23
|
+
case types_js_1.TypeXMessageEnum.text:
|
|
24
|
+
case types_js_1.TypeXMessageEnum.richText: {
|
|
25
|
+
const parsedContent = typeof msg.content === "string" ? JSON.parse(msg.content) : msg.content;
|
|
26
|
+
return parsedContent.text ?? parsedContent.preview_text ?? "";
|
|
27
|
+
}
|
|
28
|
+
case types_js_1.TypeXMessageEnum.image:
|
|
29
|
+
case types_js_1.TypeXMessageEnum.photoCollageMsg: {
|
|
30
|
+
const parsedContent = typeof msg.content === "string" ? JSON.parse(msg.content) : msg.content;
|
|
31
|
+
const text = (parsedContent.text ?? parsedContent.preview_text ?? "").trim();
|
|
32
|
+
return text ? `${text} <media:image>` : "<media:image>";
|
|
33
|
+
}
|
|
34
|
+
case types_js_1.TypeXMessageEnum.file:
|
|
35
|
+
case types_js_1.TypeXMessageEnum.fileGroup:
|
|
36
|
+
return msg.content.file_name ? `<media:file:${msg.content.file_name}>` : "<media:file>";
|
|
37
|
+
case types_js_1.TypeXMessageEnum.video:
|
|
38
|
+
return "<media:video>";
|
|
39
|
+
case types_js_1.TypeXMessageEnum.emoji:
|
|
40
|
+
return "<media:sticker>";
|
|
41
|
+
case types_js_1.TypeXMessageEnum.newCard: {
|
|
42
|
+
const card = msg.content.card;
|
|
43
|
+
if (!card)
|
|
44
|
+
return "[Card message]";
|
|
45
|
+
try {
|
|
46
|
+
const raw = typeof card === "string" ? card : JSON.stringify(card);
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
const title = parsed.header?.title ?? parsed.title ?? "";
|
|
49
|
+
const body = parsed.body ?? parsed.content ?? parsed.text ?? "";
|
|
50
|
+
const parts = [title, body].map((p) => String(p ?? "").trim()).filter(Boolean);
|
|
51
|
+
return parts.length > 0 ? parts.join("\n") : "[Card message]";
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return "[Card message]";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
case types_js_1.TypeXMessageEnum.forward: {
|
|
58
|
+
const items = msg.content.items ?? [];
|
|
59
|
+
if (items.length === 0)
|
|
60
|
+
return "[Merged and Forwarded Messages]";
|
|
61
|
+
const lines = ["[Merged and Forwarded Messages]"];
|
|
62
|
+
const limit = Math.min(items.length, 50);
|
|
63
|
+
for (let i = 0; i < limit; i++) {
|
|
64
|
+
const item = items[i];
|
|
65
|
+
const itemText = item.content?.text?.trim() ?? `[type: ${item.msg_type ?? "unknown"}]`;
|
|
66
|
+
const sender = item.sender?.name ?? item.sender?.id ?? "unknown";
|
|
67
|
+
lines.push(`- ${sender}: ${itemText}`);
|
|
68
|
+
}
|
|
69
|
+
if (items.length > 50)
|
|
70
|
+
lines.push(`... and ${items.length - 50} more messages`);
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
}
|
|
73
|
+
case types_js_1.TypeXMessageEnum.mentioned:
|
|
74
|
+
case types_js_1.TypeXMessageEnum.custom:
|
|
75
|
+
case types_js_1.TypeXMessageEnum.via:
|
|
76
|
+
return msg.content.text ?? `[Message type: ${typeNum}]`;
|
|
77
|
+
default:
|
|
78
|
+
return `[Unsupported message type: ${msg.msg_type}]`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// @mention helpers
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
function checkBotMentioned(msg, botId) {
|
|
85
|
+
if (!botId)
|
|
86
|
+
return false;
|
|
87
|
+
return (msg.mentions ?? []).some((m) => m.id.open_id === botId || m.id.user_id === botId);
|
|
88
|
+
}
|
|
89
|
+
function stripBotMention(text, mentions) {
|
|
90
|
+
if (!mentions || mentions.length === 0)
|
|
91
|
+
return text;
|
|
92
|
+
let result = text;
|
|
93
|
+
for (const mention of mentions) {
|
|
94
|
+
result = result.replace(new RegExp(`@${escapeRegExp(mention.name)}\\s*`, "g"), "");
|
|
95
|
+
if (mention.key)
|
|
96
|
+
result = result.replace(new RegExp(escapeRegExp(mention.key), "g"), "");
|
|
97
|
+
}
|
|
98
|
+
return result.trim();
|
|
99
|
+
}
|
|
100
|
+
function escapeRegExp(s) {
|
|
101
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
102
|
+
}
|
|
103
|
+
function resolveGroupConfig(typexCfg, groupId) {
|
|
104
|
+
const groups = typexCfg?.groups ?? {};
|
|
105
|
+
return groups[groupId] ?? groups["*"];
|
|
106
|
+
}
|
|
107
|
+
function normalizeAllowEntry(raw) {
|
|
108
|
+
return raw.trim().toLowerCase().replace(/^typex:/i, "");
|
|
109
|
+
}
|
|
110
|
+
function isAllowedBySenderId(allowFrom, senderId) {
|
|
111
|
+
const norm = allowFrom.map((e) => normalizeAllowEntry(String(e))).filter(Boolean);
|
|
112
|
+
if (norm.includes("*"))
|
|
113
|
+
return true;
|
|
114
|
+
return norm.includes(normalizeAllowEntry(senderId));
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Agent message body builder
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
function buildAgentBody(params) {
|
|
120
|
+
const { messageId, senderLabel, content, quotedContent } = params;
|
|
121
|
+
let body = content;
|
|
122
|
+
if (quotedContent)
|
|
123
|
+
body = `[Replying to: "${quotedContent}"]\n\n${body}`;
|
|
124
|
+
return `[message_id: ${messageId}]\n${senderLabel}: ${body}`;
|
|
125
|
+
}
|
package/dist/client/message.d.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* message.ts
|
|
3
|
+
*
|
|
4
|
+
* Main message dispatch orchestrator for the TypeX standalone plugin.
|
|
5
|
+
* Pure helpers live in ./message-helpers.ts.
|
|
6
|
+
*/
|
|
7
|
+
import type { HistoryEntry, OpenClawConfig } from "openclaw/plugin-sdk";
|
|
1
8
|
import type { TypeXClient } from "./client.js";
|
|
2
9
|
import { type TypeXMessageEntry } from "./types.js";
|
|
3
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
4
10
|
export type ProcessTypeXMessageOptions = {
|
|
5
|
-
/** Full OpenClaw gateway config (needed for bindings/routing). */
|
|
6
11
|
cfg?: OpenClawConfig;
|
|
7
12
|
accountId?: string;
|
|
8
13
|
botName?: string;
|
|
9
14
|
typexCfg?: Record<string, any>;
|
|
15
|
+
chatHistories?: Map<string, HistoryEntry[]>;
|
|
10
16
|
logger?: {
|
|
11
17
|
warn: (msg: string) => void;
|
|
12
18
|
info: (msg: string) => void;
|
|
13
19
|
error: (msg: string) => void;
|
|
14
|
-
}
|
|
20
|
+
};
|
|
15
21
|
};
|
|
22
|
+
export { buildAgentBody, checkBotMentioned, normalizeMessageToText, stripBotMention } from "./message-helpers.js";
|
|
16
23
|
export declare function processTypeXMessage(client: TypeXClient, payload: TypeXMessageEntry, appId: string, options?: ProcessTypeXMessageOptions): Promise<void>;
|