zapry-openclaw-plugin 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -0
- package/index.ts +345 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +22 -0
- package/skills/zapry/SKILL.md +337 -0
- package/src/actions.ts +2241 -0
- package/src/api-client.ts +357 -0
- package/src/channel.ts +268 -0
- package/src/config.ts +68 -0
- package/src/inbound.ts +3494 -0
- package/src/monitor.ts +144 -0
- package/src/profile-sync.ts +195 -0
- package/src/runtime.ts +66 -0
- package/src/send.ts +257 -0
- package/src/types.ts +175 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ZapryApiResponse,
|
|
3
|
+
SetMyProfilePayload,
|
|
4
|
+
SetMyProfileResponse,
|
|
5
|
+
FeedListResponse,
|
|
6
|
+
CreatePostResponse,
|
|
7
|
+
ChatHistoryResponse,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
|
|
10
|
+
type SetMySoulPayload = {
|
|
11
|
+
soulMd: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
source?: string;
|
|
14
|
+
agentKey?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type SetMySkillsPayload = {
|
|
18
|
+
skills: Array<Record<string, unknown>>;
|
|
19
|
+
version?: string;
|
|
20
|
+
source?: string;
|
|
21
|
+
agentKey?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class ZapryApiClient {
|
|
25
|
+
constructor(
|
|
26
|
+
private baseUrl: string,
|
|
27
|
+
private botToken: string,
|
|
28
|
+
private options?: {
|
|
29
|
+
defaultHeaders?: Record<string, string>;
|
|
30
|
+
},
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
private buildHeaders(extraHeaders?: Record<string, string>): Record<string, string> {
|
|
34
|
+
return {
|
|
35
|
+
...(this.options?.defaultHeaders ?? {}),
|
|
36
|
+
...(extraHeaders ?? {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getRequestHeaders(extraHeaders?: Record<string, string>): Record<string, string> {
|
|
41
|
+
return this.buildHeaders(extraHeaders);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async request<T = unknown>(opts: {
|
|
45
|
+
methodPath: string;
|
|
46
|
+
method: "GET" | "POST";
|
|
47
|
+
body?: Record<string, unknown>;
|
|
48
|
+
}): Promise<ZapryApiResponse<T>> {
|
|
49
|
+
const { methodPath, method, body } = opts;
|
|
50
|
+
const url = `${this.baseUrl}/${this.botToken}/${methodPath}`;
|
|
51
|
+
const resp = await fetch(url, {
|
|
52
|
+
method,
|
|
53
|
+
headers: this.buildHeaders({ "Content-Type": "application/json" }),
|
|
54
|
+
body: method === "POST" ? JSON.stringify(body ?? {}) : undefined,
|
|
55
|
+
});
|
|
56
|
+
if (!resp.ok) {
|
|
57
|
+
let errorBody = "";
|
|
58
|
+
try { errorBody = await resp.text(); } catch {}
|
|
59
|
+
const errorJson = (() => { try { return JSON.parse(errorBody); } catch { return null; } })();
|
|
60
|
+
const desc = errorJson?.description ?? errorBody.slice(0, 200) ?? resp.statusText;
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error_code: resp.status,
|
|
64
|
+
description: `HTTP ${resp.status} ${resp.statusText}${desc ? ` — ${desc}` : ""}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return (await resp.json()) as ZapryApiResponse<T>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async post<T = unknown>(
|
|
71
|
+
methodPath: string,
|
|
72
|
+
body?: Record<string, unknown>,
|
|
73
|
+
): Promise<ZapryApiResponse<T>> {
|
|
74
|
+
return this.request({ methodPath, method: "POST", body });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async get<T = unknown>(methodPath: string): Promise<ZapryApiResponse<T>> {
|
|
78
|
+
return this.request({ methodPath, method: "GET" });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Messaging ──
|
|
82
|
+
|
|
83
|
+
async sendMessage(
|
|
84
|
+
chatId: string,
|
|
85
|
+
text: string,
|
|
86
|
+
opts?: {
|
|
87
|
+
replyToMessageId?: string;
|
|
88
|
+
messageThreadId?: string;
|
|
89
|
+
replyMarkup?: unknown;
|
|
90
|
+
},
|
|
91
|
+
) {
|
|
92
|
+
return this.post("sendMessage", {
|
|
93
|
+
chat_id: chatId,
|
|
94
|
+
text,
|
|
95
|
+
reply_to_message_id: opts?.replyToMessageId,
|
|
96
|
+
message_thread_id: opts?.messageThreadId,
|
|
97
|
+
reply_markup: opts?.replyMarkup,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async sendPhoto(chatId: string, photo: string) {
|
|
102
|
+
return this.post("sendPhoto", { chat_id: chatId, photo });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async sendVideo(chatId: string, video: string) {
|
|
106
|
+
return this.post("sendVideo", { chat_id: chatId, video });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async sendDocument(chatId: string, document: string) {
|
|
110
|
+
return this.post("sendDocument", { chat_id: chatId, document });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async sendAudio(chatId: string, audio: string) {
|
|
114
|
+
return this.post("sendAudio", { chat_id: chatId, audio });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async sendVoice(chatId: string, voice: string) {
|
|
118
|
+
return this.post("sendVoice", { chat_id: chatId, voice });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async sendAnimation(chatId: string, animation: string) {
|
|
122
|
+
return this.post("sendAnimation", { chat_id: chatId, animation });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async sendChatAction(chatId: string, action: string) {
|
|
126
|
+
return this.post("sendChatAction", { chat_id: chatId, action });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async deleteMessage(chatId: string, messageId: string) {
|
|
130
|
+
return this.post("deleteMessage", { chat_id: chatId, message_id: messageId });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async answerCallbackQuery(
|
|
134
|
+
chatId: string,
|
|
135
|
+
callbackQueryId: string,
|
|
136
|
+
opts?: {
|
|
137
|
+
text?: string;
|
|
138
|
+
showAlert?: boolean;
|
|
139
|
+
},
|
|
140
|
+
) {
|
|
141
|
+
return this.post("answerCallbackQuery", {
|
|
142
|
+
chat_id: chatId,
|
|
143
|
+
callback_query_id: callbackQueryId,
|
|
144
|
+
text: opts?.text,
|
|
145
|
+
show_alert: opts?.showAlert,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Receive / Webhook ──
|
|
150
|
+
|
|
151
|
+
async getUpdates(offset?: number, limit?: number, timeout?: number) {
|
|
152
|
+
return this.post("getUpdates", { offset, limit, timeout });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getFile(fileId: string) {
|
|
156
|
+
return this.post("getFile", { file_id: fileId });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async setWebhook(url: string) {
|
|
160
|
+
return this.post("setWebhook", { url });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async getWebhookInfo() {
|
|
164
|
+
return this.post("getWebhookInfo");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async deleteWebhook() {
|
|
168
|
+
return this.post("deleteWebhook");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Skills ──
|
|
172
|
+
|
|
173
|
+
async setMySoul(payload: SetMySoulPayload) {
|
|
174
|
+
return this.post("setMySoul", payload);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async getMySoul() {
|
|
178
|
+
return this.get("getMySoul");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async setMySkills(payload: SetMySkillsPayload) {
|
|
182
|
+
return this.post("setMySkills", payload);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async getMySkills() {
|
|
186
|
+
return this.get("getMySkills");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async getMyProfile() {
|
|
190
|
+
return this.post("getMyProfile");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async setMyProfile(payload: SetMyProfilePayload): Promise<ZapryApiResponse<SetMyProfileResponse>> {
|
|
194
|
+
return this.post("setMyProfile", payload as unknown as Record<string, unknown>);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Group Query & Moderation ──
|
|
198
|
+
|
|
199
|
+
async getMyGroups(page?: number, pageSize?: number) {
|
|
200
|
+
return this.post("getMyGroups", { page, page_size: pageSize });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async getMyChats(page?: number, pageSize?: number) {
|
|
204
|
+
return this.post("getMyChats", { page, page_size: pageSize });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async getChatMember(chatId: string, userId: string) {
|
|
208
|
+
return this.post("getChatMember", { chat_id: chatId, user_id: userId });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async getChatMembers(chatId: string, opts?: { page?: number; pageSize?: number; keyword?: string }) {
|
|
212
|
+
return this.post("getChatMembers", {
|
|
213
|
+
chat_id: chatId,
|
|
214
|
+
page: opts?.page,
|
|
215
|
+
page_size: opts?.pageSize,
|
|
216
|
+
keyword: opts?.keyword,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async getChatMemberCount(chatId: string) {
|
|
221
|
+
return this.post("getChatMemberCount", { chat_id: chatId });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async getChatHistory(chatId: string, limit?: number): Promise<ZapryApiResponse<ChatHistoryResponse>> {
|
|
225
|
+
return this.post("getChatHistory", { chat_id: chatId, limit: limit ?? 50 });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async getChatAdministrators(chatId: string) {
|
|
229
|
+
return this.post("getChatAdministrators", { chat_id: chatId });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async muteChatMember(chatId: string, userId: string, mute: boolean) {
|
|
233
|
+
return this.post("muteChatMember", { chat_id: chatId, user_id: userId, mute });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async kickChatMember(chatId: string, userId: string) {
|
|
237
|
+
return this.post("kickChatMember", { chat_id: chatId, user_id: userId });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async inviteChatMember(chatId: string, userId: string) {
|
|
241
|
+
return this.post("inviteChatMember", { chat_id: chatId, user_id: userId });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async setChatTitle(chatId: string, title: string) {
|
|
245
|
+
return this.post("setChatTitle", { chat_id: chatId, title });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async setChatDescription(chatId: string, description: string) {
|
|
249
|
+
return this.post("setChatDescription", { chat_id: chatId, description });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Agent Self Management ──
|
|
253
|
+
|
|
254
|
+
async getMe() {
|
|
255
|
+
return this.get("getMe");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async getUserProfilePhotos(userId?: string) {
|
|
259
|
+
return this.post("getUserProfilePhotos", { user_id: userId });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async setMyWalletAddress(walletAddress: string) {
|
|
263
|
+
return this.post("setMyWalletAddress", { wallet_address: walletAddress });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async setMyFriendVerify(needVerify: boolean) {
|
|
267
|
+
return this.post("setMyFriendVerify", { need_verify: needVerify });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async getMyContacts(page?: number, pageSize?: number) {
|
|
271
|
+
return this.post("getMyContacts", { page, page_size: pageSize });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async getMyFriendRequests(pendingOnly?: boolean) {
|
|
275
|
+
return this.post("getMyFriendRequests", { pending_only: pendingOnly });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async acceptFriendRequest(userId: string) {
|
|
279
|
+
return this.post("acceptFriendRequest", { user_id: userId });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async rejectFriendRequest(userId: string) {
|
|
283
|
+
return this.post("rejectFriendRequest", { user_id: userId });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async addFriend(userId: string, message?: string, remark?: string) {
|
|
287
|
+
return this.post("addFriend", { user_id: userId, message, remark });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async deleteFriend(userId: string) {
|
|
291
|
+
return this.post("deleteFriend", { user_id: userId });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async setMyName(name: string) {
|
|
295
|
+
return this.post("setMyName", { name });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async setMyDescription(description: string) {
|
|
299
|
+
return this.post("setMyDescription", { description });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Feed ──
|
|
303
|
+
|
|
304
|
+
async getTrendingPosts(page?: number, pageSize?: number): Promise<ZapryApiResponse<FeedListResponse>> {
|
|
305
|
+
return this.post("getTrendingPosts", { page, page_size: pageSize });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async getLatestPosts(page?: number, pageSize?: number): Promise<ZapryApiResponse<FeedListResponse>> {
|
|
309
|
+
return this.post("getLatestPosts", { page, page_size: pageSize });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async getMyPosts(page?: number, pageSize?: number): Promise<ZapryApiResponse<FeedListResponse>> {
|
|
313
|
+
return this.post("getMyPosts", { page, page_size: pageSize });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async searchPosts(keyword: string, page?: number, pageSize?: number): Promise<ZapryApiResponse<FeedListResponse>> {
|
|
317
|
+
return this.post("searchPosts", { keyword, page, page_size: pageSize });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async createPost(content: string, images?: string[]): Promise<ZapryApiResponse<CreatePostResponse>> {
|
|
321
|
+
return this.post("createPost", { content, images });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async deletePost(dynamicId: number) {
|
|
325
|
+
return this.post("deletePost", { dynamic_id: dynamicId });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async commentPost(dynamicId: number, content: string) {
|
|
329
|
+
return this.post("commentPost", { dynamic_id: dynamicId, content });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async likePost(dynamicId: number) {
|
|
333
|
+
return this.post("likePost", { dynamic_id: dynamicId });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async sharePost(dynamicId: number) {
|
|
337
|
+
return this.post("sharePost", { dynamic_id: dynamicId });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Club ──
|
|
341
|
+
|
|
342
|
+
async getMyClubs(page?: number, pageSize?: number) {
|
|
343
|
+
return this.post("getMyClubs", { page, page_size: pageSize });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async createClub(name: string, desc?: string, avatar?: string) {
|
|
347
|
+
return this.post("createClub", { name, desc, avatar });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async updateClub(clubId: number, name?: string, desc?: string, avatar?: string) {
|
|
351
|
+
return this.post("updateClub", { club_id: clubId, name, desc, avatar });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async setMyPresence(online: boolean) {
|
|
355
|
+
return this.post("setMyPresence", { online });
|
|
356
|
+
}
|
|
357
|
+
}
|
package/src/channel.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listZapryAccountIds,
|
|
3
|
+
resolveDefaultZapryAccountId,
|
|
4
|
+
resolveZapryAccount,
|
|
5
|
+
} from "./config.js";
|
|
6
|
+
import { sendMessageZapry } from "./send.js";
|
|
7
|
+
import { monitorZapryProvider } from "./monitor.js";
|
|
8
|
+
import { ZapryApiClient } from "./api-client.js";
|
|
9
|
+
import { DEFAULT_ACCOUNT_ID } from "./types.js";
|
|
10
|
+
import type { ResolvedZapryAccount } from "./types.js";
|
|
11
|
+
import { syncProfileToZapry } from "./profile-sync.js";
|
|
12
|
+
|
|
13
|
+
const IS_STANDARD_CHAT_ID = /^[gup]_\d+$/;
|
|
14
|
+
|
|
15
|
+
async function resolveOutboundTarget(account: ResolvedZapryAccount, to: string): Promise<string> {
|
|
16
|
+
const trimmed = to.trim();
|
|
17
|
+
if (IS_STANDARD_CHAT_ID.test(trimmed) || trimmed.startsWith("chat:") || /^\d+$/.test(trimmed)) {
|
|
18
|
+
return trimmed;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const client = new ZapryApiClient(account.config.apiBaseUrl, account.botToken);
|
|
22
|
+
const resp = await client.getMyGroups(1, 100);
|
|
23
|
+
if (!resp.ok) return trimmed;
|
|
24
|
+
const raw = (resp as any).result;
|
|
25
|
+
const groups: any[] = Array.isArray(raw) ? raw : raw?.items ?? raw?.groups ?? [];
|
|
26
|
+
for (const g of groups) {
|
|
27
|
+
const info = g.info ?? g;
|
|
28
|
+
const gName = info.group_name ?? info.name ?? info.title ?? "";
|
|
29
|
+
const gId = info.chat_id ?? info.group_id ?? info.id ?? "";
|
|
30
|
+
if (String(gName) === trimmed && gId) return String(gId);
|
|
31
|
+
}
|
|
32
|
+
for (const g of groups) {
|
|
33
|
+
const info = g.info ?? g;
|
|
34
|
+
const gName = String(info.group_name ?? info.name ?? info.title ?? "");
|
|
35
|
+
const gId = info.chat_id ?? info.group_id ?? info.id ?? "";
|
|
36
|
+
if (gName.includes(trimmed) && gId) return String(gId);
|
|
37
|
+
}
|
|
38
|
+
} catch {}
|
|
39
|
+
return trimmed;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const zapryPlugin = {
|
|
43
|
+
id: "zapry",
|
|
44
|
+
meta: {
|
|
45
|
+
id: "zapry",
|
|
46
|
+
name: "Zapry",
|
|
47
|
+
emoji: "⚡",
|
|
48
|
+
description: "Zapry social platform — messaging, groups, feed, clubs",
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
capabilities: {
|
|
52
|
+
chatTypes: ["direct", "channel", "group"] as const,
|
|
53
|
+
polls: false,
|
|
54
|
+
reactions: false,
|
|
55
|
+
threads: false,
|
|
56
|
+
media: true,
|
|
57
|
+
nativeCommands: true,
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
streaming: {
|
|
61
|
+
blockStreamingCoalesceDefaults: { minChars: 1000, idleMs: 800 },
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
reload: { configPrefixes: ["channels.zapry"] },
|
|
65
|
+
|
|
66
|
+
config: {
|
|
67
|
+
listAccountIds: (cfg: any) => listZapryAccountIds(cfg),
|
|
68
|
+
resolveAccount: (cfg: any, accountId?: string) => resolveZapryAccount(cfg, accountId),
|
|
69
|
+
defaultAccountId: (cfg: any) => resolveDefaultZapryAccountId(cfg),
|
|
70
|
+
setAccountEnabled: ({ cfg, accountId, enabled }: any) => {
|
|
71
|
+
const next = structuredClone(cfg);
|
|
72
|
+
const zapry = next.channels?.zapry;
|
|
73
|
+
if (!zapry) return next;
|
|
74
|
+
if (zapry.accounts?.[accountId]) {
|
|
75
|
+
zapry.accounts[accountId].enabled = enabled;
|
|
76
|
+
} else if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
77
|
+
zapry.enabled = enabled;
|
|
78
|
+
}
|
|
79
|
+
return next;
|
|
80
|
+
},
|
|
81
|
+
deleteAccount: ({ cfg, accountId }: any) => {
|
|
82
|
+
const next = structuredClone(cfg);
|
|
83
|
+
const zapry = next.channels?.zapry;
|
|
84
|
+
if (!zapry) return next;
|
|
85
|
+
if (zapry.accounts?.[accountId]) {
|
|
86
|
+
delete zapry.accounts[accountId];
|
|
87
|
+
}
|
|
88
|
+
return next;
|
|
89
|
+
},
|
|
90
|
+
isConfigured: (account: any) => Boolean(account.botToken?.trim()),
|
|
91
|
+
describeAccount: (account: any) => ({
|
|
92
|
+
accountId: account.accountId,
|
|
93
|
+
name: account.name,
|
|
94
|
+
enabled: account.enabled,
|
|
95
|
+
configured: Boolean(account.botToken?.trim()),
|
|
96
|
+
tokenSource: account.tokenSource,
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
security: {
|
|
101
|
+
resolveDmPolicy: ({ account }: any) => ({
|
|
102
|
+
policy: account.config.dm?.policy ?? "open",
|
|
103
|
+
allowFrom: account.config.dm?.allowFrom ?? [],
|
|
104
|
+
allowFromPath: `channels.zapry.dm.`,
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
messaging: {
|
|
109
|
+
normalizeTarget: (to: string) => to.replace(/^(chat|zapry):/i, "").trim(),
|
|
110
|
+
targetResolver: {
|
|
111
|
+
looksLikeId: (input: string) => /^(g_)?\d+$/.test(input.replace(/^(chat|zapry):/i, "").trim()),
|
|
112
|
+
hint: "<chatId|chat:ID>",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
agentPrompt: {
|
|
117
|
+
messageToolHints: () => [
|
|
118
|
+
`For ALL Zapry operations, prefer "zapry_action" tool. ` +
|
|
119
|
+
`To send text messages: use "zapry_action" action="send-message" with chat_id and text — supports group names (auto-resolved). ` +
|
|
120
|
+
`To send photos: use "zapry_action" action="send-photo" with "prompt" parameter to auto-generate images (e.g. prompt="cute cat"). ` +
|
|
121
|
+
`To send video/audio/document: use "zapry_action" with send-video/send-audio/send-document. ` +
|
|
122
|
+
`IMPORTANT: Do NOT use "message" tool for Zapry group chats — it cannot resolve group names. Always use zapry_action instead. ` +
|
|
123
|
+
`To publish to feed (广场), use "zapry_post" with content and optional images.`,
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
actions: {
|
|
128
|
+
listActions: () => [
|
|
129
|
+
"send",
|
|
130
|
+
],
|
|
131
|
+
extractToolSend: (ctx: any) => {
|
|
132
|
+
if (ctx.action === "send" || ctx.action === "send-message") {
|
|
133
|
+
const result: Record<string, any> = {
|
|
134
|
+
to: ctx.params?.to ?? ctx.params?.chatId ?? ctx.params?.chat_id,
|
|
135
|
+
text: ctx.params?.message ?? ctx.params?.text,
|
|
136
|
+
};
|
|
137
|
+
const mediaUrl =
|
|
138
|
+
ctx.params?.photo ?? ctx.params?.video ?? ctx.params?.animation ??
|
|
139
|
+
ctx.params?.document ?? ctx.params?.audio ?? ctx.params?.voice ??
|
|
140
|
+
ctx.params?.mediaUrl ?? ctx.params?.media_url ?? ctx.params?.media;
|
|
141
|
+
if (mediaUrl) result.mediaUrl = mediaUrl;
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
outbound: {
|
|
149
|
+
deliveryMode: "direct" as const,
|
|
150
|
+
chunker: null,
|
|
151
|
+
textChunkLimit: 4096,
|
|
152
|
+
resolveTarget: ({ to }: { to: string }) => to.replace(/^(chat|zapry):/i, "").trim(),
|
|
153
|
+
sendText: async ({ to, text, accountId, deps, replyToId }: any) => {
|
|
154
|
+
const cfg = deps?.cfg;
|
|
155
|
+
const account = resolveZapryAccount(cfg, accountId);
|
|
156
|
+
const resolvedTo = await resolveOutboundTarget(account, to);
|
|
157
|
+
const result = await sendMessageZapry(account, resolvedTo, text, { replyTo: replyToId });
|
|
158
|
+
return { channel: "zapry", ...result };
|
|
159
|
+
},
|
|
160
|
+
sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId }: any) => {
|
|
161
|
+
const cfg = deps?.cfg;
|
|
162
|
+
const account = resolveZapryAccount(cfg, accountId);
|
|
163
|
+
const resolvedTo = await resolveOutboundTarget(account, to);
|
|
164
|
+
const result = await sendMessageZapry(account, resolvedTo, text || "", { mediaUrl, replyTo: replyToId });
|
|
165
|
+
return { channel: "zapry", ...result };
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
status: {
|
|
170
|
+
defaultRuntime: {
|
|
171
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
172
|
+
running: false,
|
|
173
|
+
lastStartAt: null,
|
|
174
|
+
lastStopAt: null,
|
|
175
|
+
lastError: null,
|
|
176
|
+
lastInboundAt: null,
|
|
177
|
+
lastOutboundAt: null,
|
|
178
|
+
},
|
|
179
|
+
probeAccount: async ({ account, timeoutMs }: any) => {
|
|
180
|
+
try {
|
|
181
|
+
const client = new ZapryApiClient(account.config.apiBaseUrl, account.botToken);
|
|
182
|
+
const resp = await client.getMe();
|
|
183
|
+
if (!resp.ok) {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
errorCode: resp.error_code,
|
|
187
|
+
error: resp.description ?? "getMe failed",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return { ok: true, bot: resp.result };
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return { ok: false, error: String(err) };
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
buildAccountSnapshot: ({ account, runtime, probe }: any) => ({
|
|
196
|
+
accountId: account.accountId,
|
|
197
|
+
name: account.name,
|
|
198
|
+
enabled: account.enabled,
|
|
199
|
+
configured: Boolean(account.botToken?.trim()),
|
|
200
|
+
tokenSource: account.tokenSource,
|
|
201
|
+
running: runtime?.running ?? false,
|
|
202
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
203
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
204
|
+
lastError: runtime?.lastError ?? null,
|
|
205
|
+
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
206
|
+
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
207
|
+
probe,
|
|
208
|
+
}),
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
gateway: {
|
|
212
|
+
startAccount: async (ctx: any) => {
|
|
213
|
+
const { account } = ctx;
|
|
214
|
+
ctx.log?.info(`[${account.accountId}] starting Zapry provider (${account.config.mode} mode)`);
|
|
215
|
+
|
|
216
|
+
const client = new ZapryApiClient(account.config.apiBaseUrl, account.botToken);
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const probe = await client.getMe();
|
|
220
|
+
if (probe.ok) {
|
|
221
|
+
const bot = probe.result as any;
|
|
222
|
+
ctx.log?.info(`[${account.accountId}] bot: ${bot?.name ?? bot?.username ?? "unknown"}`);
|
|
223
|
+
ctx.setStatus?.({ accountId: account.accountId, bot });
|
|
224
|
+
} else {
|
|
225
|
+
ctx.log?.warn(
|
|
226
|
+
`[${account.accountId}] getMe probe failed: ` +
|
|
227
|
+
`${probe.error_code ?? "unknown"}:${probe.description ?? "unknown"}`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
// probe is best-effort
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
client.setMyPresence(true).catch(() => {});
|
|
235
|
+
ctx.log?.info(`[${account.accountId}] presence set to online`);
|
|
236
|
+
|
|
237
|
+
const projectRoot =
|
|
238
|
+
ctx.runtime?.projectRoot ?? ctx.runtime?.config?.projectRoot ?? process.cwd();
|
|
239
|
+
syncProfileToZapry(account, { projectRoot, log: ctx.log }).catch((err) => {
|
|
240
|
+
ctx.log?.warn?.(
|
|
241
|
+
`[${account.accountId}] profile sync failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
ctx.abortSignal?.addEventListener("abort", () => {
|
|
246
|
+
client.setMyPresence(false).catch(() => {});
|
|
247
|
+
ctx.log?.info(`[${account.accountId}] presence set to offline`);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const effectiveRuntime = ctx.channelRuntime
|
|
251
|
+
? { ...ctx.runtime, channel: ctx.channelRuntime }
|
|
252
|
+
: ctx.runtime;
|
|
253
|
+
|
|
254
|
+
return monitorZapryProvider({
|
|
255
|
+
account,
|
|
256
|
+
cfg: ctx.cfg,
|
|
257
|
+
runtime: effectiveRuntime,
|
|
258
|
+
abortSignal: ctx.abortSignal,
|
|
259
|
+
statusSink: (patch) => {
|
|
260
|
+
if (typeof ctx.setStatus === "function") {
|
|
261
|
+
ctx.setStatus({ accountId: ctx.accountId, ...patch });
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
log: ctx.log,
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
};
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ACCOUNT_ID,
|
|
3
|
+
DEFAULT_API_BASE_URL,
|
|
4
|
+
type ResolvedZapryAccount,
|
|
5
|
+
type ZapryChannelConfig,
|
|
6
|
+
} from "./types.js";
|
|
7
|
+
|
|
8
|
+
function getZapryConfig(cfg: any): ZapryChannelConfig | undefined {
|
|
9
|
+
return cfg?.channels?.zapry;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function listZapryAccountIds(cfg: any): string[] {
|
|
13
|
+
const zapry = getZapryConfig(cfg);
|
|
14
|
+
if (!zapry) return [];
|
|
15
|
+
if (zapry.accounts && Object.keys(zapry.accounts).length > 0) {
|
|
16
|
+
return Object.keys(zapry.accounts);
|
|
17
|
+
}
|
|
18
|
+
if (zapry.botToken) return [DEFAULT_ACCOUNT_ID];
|
|
19
|
+
const envToken = process.env.ZAPRY_BOT_TOKEN;
|
|
20
|
+
if (envToken) return [DEFAULT_ACCOUNT_ID];
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveZapryAccount(
|
|
25
|
+
cfg: any,
|
|
26
|
+
accountId?: string,
|
|
27
|
+
): ResolvedZapryAccount {
|
|
28
|
+
const zapry = getZapryConfig(cfg) ?? {};
|
|
29
|
+
const resolvedId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
30
|
+
|
|
31
|
+
const acct = zapry.accounts?.[resolvedId];
|
|
32
|
+
if (acct) {
|
|
33
|
+
return {
|
|
34
|
+
accountId: resolvedId,
|
|
35
|
+
name: acct.name,
|
|
36
|
+
enabled: acct.enabled !== false,
|
|
37
|
+
botToken: acct.botToken ?? "",
|
|
38
|
+
tokenSource: "config",
|
|
39
|
+
config: {
|
|
40
|
+
apiBaseUrl: acct.apiBaseUrl ?? zapry.apiBaseUrl ?? DEFAULT_API_BASE_URL,
|
|
41
|
+
mode: acct.mode ?? zapry.mode ?? "polling",
|
|
42
|
+
webhookUrl: acct.webhookUrl ?? zapry.webhookUrl,
|
|
43
|
+
dm: acct.dm ?? zapry.dm,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const envToken = process.env.ZAPRY_BOT_TOKEN;
|
|
49
|
+
const token = zapry.botToken ?? envToken ?? "";
|
|
50
|
+
return {
|
|
51
|
+
accountId: resolvedId,
|
|
52
|
+
name: undefined,
|
|
53
|
+
enabled: zapry.enabled !== false,
|
|
54
|
+
botToken: token,
|
|
55
|
+
tokenSource: zapry.botToken ? "config" : "env",
|
|
56
|
+
config: {
|
|
57
|
+
apiBaseUrl: zapry.apiBaseUrl ?? DEFAULT_API_BASE_URL,
|
|
58
|
+
mode: zapry.mode ?? "polling",
|
|
59
|
+
webhookUrl: zapry.webhookUrl,
|
|
60
|
+
dm: zapry.dm,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveDefaultZapryAccountId(cfg: any): string {
|
|
66
|
+
const ids = listZapryAccountIds(cfg);
|
|
67
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
68
|
+
}
|