openclaw-extension-typex 1.0.15 → 1.0.17
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 +227 -136
- 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 +195 -69
- 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 +5 -4
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,281 @@ 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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
content:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
throw new Error(`Send message failed: [${resJson.code}] ${resJson.message}`);
|
|
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;
|
|
128
101
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
message_id: `msg_${Date.now()}`,
|
|
102
|
+
payloadStr = JSON.stringify({
|
|
103
|
+
chat_id: to,
|
|
104
|
+
content: botContentObj,
|
|
105
|
+
msg_type: msgType,
|
|
106
|
+
reply_msg_id: options.replyMsgId || "0",
|
|
135
107
|
});
|
|
136
108
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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}`, "x-developer": "ryan" } : { Cookie: token }),
|
|
121
|
+
},
|
|
122
|
+
body: payloadStr,
|
|
123
|
+
});
|
|
124
|
+
const bodyText = await response.text();
|
|
125
|
+
let resJson;
|
|
126
|
+
try {
|
|
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
|
+
"x-developer": "ryan"
|
|
162
|
+
// Note: fetch will automatically set the Content-Type boundary
|
|
163
|
+
},
|
|
164
|
+
body: formData,
|
|
165
|
+
});
|
|
166
|
+
const resJson = await response.json();
|
|
167
|
+
if (resJson.code !== 0) {
|
|
168
|
+
throw new Error(`Upload resource failed: [${resJson.code}] ${resJson.msg || resJson.message}`);
|
|
143
169
|
}
|
|
170
|
+
return resJson.data;
|
|
144
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Fetch messages. Dispatches to user or bot endpoint based on mode.
|
|
174
|
+
*/
|
|
145
175
|
async fetchMessages(pos) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
176
|
+
return this.mode === "bot"
|
|
177
|
+
? this.fetchBotMessages()
|
|
178
|
+
: this.fetchUserMessages(pos);
|
|
179
|
+
}
|
|
180
|
+
/** Pull messages for a regular user account (sessionid cookie auth). */
|
|
181
|
+
async fetchUserMessages(pos) {
|
|
182
|
+
if (!this.accessToken)
|
|
151
183
|
return [];
|
|
152
|
-
}
|
|
153
184
|
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, {
|
|
185
|
+
const response = await fetch(`${TYPEX_DOMAIN}/open/claw/message`, {
|
|
159
186
|
method: "POST",
|
|
160
|
-
headers: {
|
|
161
|
-
|
|
162
|
-
"Content-Type": "application/json",
|
|
163
|
-
},
|
|
164
|
-
body: JSON.stringify({ pos: pos }),
|
|
187
|
+
headers: { Cookie: this.accessToken, "Content-Type": "application/json", 'x-developer': 'ryan' },
|
|
188
|
+
body: JSON.stringify({ pos }),
|
|
165
189
|
});
|
|
166
190
|
const resJson = await response.json();
|
|
167
|
-
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}`);
|
|
191
|
+
if (resJson.code !== 0)
|
|
172
192
|
return [];
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
193
|
+
return Array.isArray(resJson.data) ? resJson.data : [];
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
console.log(`Fetch messages error: ${e}`);
|
|
177
197
|
return [];
|
|
178
198
|
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Pull messages for a bot account (Bearer token auth).
|
|
202
|
+
* TODO: replace /open/bot/message with the actual endpoint path once confirmed.
|
|
203
|
+
*/
|
|
204
|
+
async fetchBotMessages() {
|
|
205
|
+
if (!this.accessToken)
|
|
206
|
+
return [];
|
|
207
|
+
try {
|
|
208
|
+
const response = await fetch(`${TYPEX_DOMAIN}/open/robot/message/pull`, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: { Authorization: `Bearer ${this.accessToken}`, "Content-Type": "application/json", 'x-developer': 'ryan' },
|
|
211
|
+
body: JSON.stringify({ limit: 5 }),
|
|
212
|
+
});
|
|
213
|
+
const resJson = await response.json();
|
|
214
|
+
if (resJson.code !== 0)
|
|
215
|
+
return [];
|
|
216
|
+
return Array.isArray(resJson.data?.messages) ? resJson.data.messages : [];
|
|
217
|
+
}
|
|
179
218
|
catch (e) {
|
|
180
|
-
|
|
181
|
-
prompter.note(`Fetch messages network error: ${e}`);
|
|
182
|
-
else
|
|
183
|
-
console.log(`Fetch messages network error: ${e}`);
|
|
219
|
+
console.log(`Bot fetch messages error: ${e}`);
|
|
184
220
|
return [];
|
|
185
221
|
}
|
|
186
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Fetch a single message by ID (used to resolve quoted/parent messages).
|
|
225
|
+
*/
|
|
226
|
+
async getMessage(messageId) {
|
|
227
|
+
if (!this.accessToken)
|
|
228
|
+
return null;
|
|
229
|
+
try {
|
|
230
|
+
const isBot = this.mode === "bot";
|
|
231
|
+
const response = await fetch(`${TYPEX_DOMAIN}/open/claw/message/${messageId}`, {
|
|
232
|
+
method: "GET",
|
|
233
|
+
headers: isBot
|
|
234
|
+
? { Authorization: `Bearer ${this.accessToken}`, "Content-Type": "application/json", 'x-developer': 'ryan' }
|
|
235
|
+
: { Cookie: this.accessToken, "Content-Type": "application/json" },
|
|
236
|
+
});
|
|
237
|
+
const resJson = await response.json();
|
|
238
|
+
return resJson.code === 0 && resJson.data ? resJson.data : null;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Fetch file binary stream from TypeX using object_key.
|
|
246
|
+
* Requires Bot Token authentication.
|
|
247
|
+
*/
|
|
248
|
+
async fetchFileBuffer(objectKey, size) {
|
|
249
|
+
if (!this.accessToken || this.mode !== "bot")
|
|
250
|
+
return null;
|
|
251
|
+
try {
|
|
252
|
+
const query = new URLSearchParams({ object_key: objectKey });
|
|
253
|
+
if (size)
|
|
254
|
+
query.append("size", size);
|
|
255
|
+
const url = `${TYPEX_DOMAIN}/open/robot/chat/file?${query.toString()}`;
|
|
256
|
+
const response = await fetch(url, {
|
|
257
|
+
method: "GET",
|
|
258
|
+
headers: { Authorization: `Bearer ${this.accessToken}`, 'x-developer': 'ryan' },
|
|
259
|
+
});
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
console.log(`fetchFileBuffer failed with status: ${response.status} ${response.statusText} for url: ${url}`);
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
265
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
266
|
+
const mimeType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
267
|
+
return { buffer, mimeType };
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
console.log(`fetchFileBuffer error: ${e}`);
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
187
274
|
}
|
|
188
275
|
exports.TypeXClient = TypeXClient;
|
|
189
276
|
function getTypeXClient(accountId, manualOptions) {
|
|
190
277
|
const typexCfg = (manualOptions?.typexCfg ?? {});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
prompter = clawPrompter;
|
|
194
|
-
}
|
|
278
|
+
if (manualOptions?.prompter)
|
|
279
|
+
prompter = manualOptions.prompter;
|
|
195
280
|
let token = manualOptions?.token;
|
|
281
|
+
let mode = manualOptions?.mode ?? "user";
|
|
196
282
|
if (accountId && typexCfg.accounts?.[accountId]) {
|
|
197
|
-
|
|
283
|
+
const acctCfg = typexCfg.accounts[accountId];
|
|
284
|
+
token = token ?? acctCfg.token;
|
|
285
|
+
if (acctCfg.mode === "bot" || acctCfg.mode === "user")
|
|
286
|
+
mode = acctCfg.mode;
|
|
198
287
|
}
|
|
288
|
+
// Config check: outbound sends should fail only when we truly lack credentials.
|
|
289
|
+
// Historically this was a stub that always threw, which broke outbound delivery.
|
|
199
290
|
if (!manualOptions?.skipConfigCheck) {
|
|
200
|
-
|
|
291
|
+
if (!token?.trim()) {
|
|
292
|
+
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");
|
|
293
|
+
}
|
|
201
294
|
}
|
|
202
|
-
return new TypeXClient({
|
|
203
|
-
token: token,
|
|
204
|
-
});
|
|
295
|
+
return new TypeXClient({ token, mode });
|
|
205
296
|
}
|
|
@@ -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>;
|