polycopy 0.1.0 → 0.1.3
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/cli.js +1 -1
- package/dist/config.js +1 -1
- package/dist/index.js +559 -86
- package/dist/{init-OIFlNHFx.js → init-C9dmq3BS.js} +2 -396
- package/package.json +4 -2
package/dist/cli.js
CHANGED
package/dist/config.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const init = require("./init-
|
|
2
|
+
const init = require("./init-C9dmq3BS.js");
|
|
3
3
|
const grammy = require("grammy");
|
|
4
4
|
const wallet = require("@ethersproject/wallet");
|
|
5
5
|
const clobClient = require("@polymarket/clob-client");
|
|
@@ -32,23 +32,445 @@ function _interopNamespace(e) {
|
|
|
32
32
|
}
|
|
33
33
|
const setPromiseInterval__default = /* @__PURE__ */ _interopDefault(setPromiseInterval);
|
|
34
34
|
const fs__namespace = /* @__PURE__ */ _interopNamespace(fs);
|
|
35
|
-
|
|
35
|
+
function parseSignalMetadata(messageText) {
|
|
36
|
+
const match = messageText.match(/🤖\s*({.*})/s);
|
|
37
|
+
if (!match) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const jsonStr = match[1];
|
|
42
|
+
const metadata = JSON.parse(jsonStr);
|
|
43
|
+
if (!metadata.eventId || !metadata.assetId || !metadata.outcome || !metadata.endTime) {
|
|
44
|
+
init.logger.error("解析失败: 缺少必需字段", {
|
|
45
|
+
hasEventId: !!metadata.eventId,
|
|
46
|
+
hasAssetId: !!metadata.assetId,
|
|
47
|
+
hasOutcome: !!metadata.outcome,
|
|
48
|
+
hasEndTime: !!metadata.endTime
|
|
49
|
+
});
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (metadata.outcome !== "Up" && metadata.outcome !== "Down") {
|
|
53
|
+
init.logger.error("解析失败: outcome 值无效", {
|
|
54
|
+
outcome: metadata.outcome,
|
|
55
|
+
expected: "Up 或 Down"
|
|
56
|
+
});
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (typeof metadata.endTime !== "number" || metadata.endTime <= 0) {
|
|
60
|
+
init.logger.error("解析失败: endTime 值无效", {
|
|
61
|
+
endTime: metadata.endTime,
|
|
62
|
+
expected: "毫秒级时间戳"
|
|
63
|
+
});
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return metadata;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
init.logger.error("解析信号元数据失败:", error);
|
|
69
|
+
if (error instanceof Error) {
|
|
70
|
+
init.logger.line("", `错误详情: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
class TelegramListener {
|
|
76
|
+
bot;
|
|
77
|
+
onSignalCallback;
|
|
78
|
+
onChatIdsChangedCallback;
|
|
79
|
+
targetChatIds;
|
|
80
|
+
// 运行时监听的频道 ID 集合(自动管理)
|
|
81
|
+
constructor(bot, targetChatIds = []) {
|
|
82
|
+
this.bot = bot;
|
|
83
|
+
this.targetChatIds = new Set(targetChatIds);
|
|
84
|
+
this.setupHandlers();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 设置消息处理器
|
|
88
|
+
*/
|
|
89
|
+
setupHandlers() {
|
|
90
|
+
this.bot.on("channel_post", async (ctx) => {
|
|
91
|
+
await this.handleChannelPost(ctx);
|
|
92
|
+
});
|
|
93
|
+
this.bot.on("edited_channel_post", async (ctx) => {
|
|
94
|
+
await this.handleChannelPost(ctx);
|
|
95
|
+
});
|
|
96
|
+
this.bot.on("my_chat_member", async (ctx) => {
|
|
97
|
+
await this.handleMyChatMemberUpdate(ctx);
|
|
98
|
+
});
|
|
99
|
+
this.bot.catch((err) => {
|
|
100
|
+
init.logger.error("Telegram Bot 错误:", err);
|
|
101
|
+
if (err.error) {
|
|
102
|
+
init.logger.line("", `错误详情: ${err.error}`);
|
|
103
|
+
}
|
|
104
|
+
if (err.ctx) {
|
|
105
|
+
init.logger.line("", `上下文信息:`, {
|
|
106
|
+
chatId: err.ctx.chat?.id,
|
|
107
|
+
messageId: err.ctx.message?.message_id
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 处理 Bot 自己的成员状态变更
|
|
114
|
+
*/
|
|
115
|
+
async handleMyChatMemberUpdate(ctx) {
|
|
116
|
+
const update = ctx.myChatMember;
|
|
117
|
+
if (!update) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const chatId = update.chat.id;
|
|
121
|
+
const chatTitle = update.chat.title || "未知频道";
|
|
122
|
+
const oldStatus = update.old_chat_member.status;
|
|
123
|
+
const newStatus = update.new_chat_member.status;
|
|
124
|
+
const chatType = update.chat.type;
|
|
125
|
+
if (chatType !== "channel") {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const isAdmin = newStatus === "administrator" || newStatus === "creator";
|
|
129
|
+
const wasAdmin = oldStatus === "administrator" || oldStatus === "creator";
|
|
130
|
+
const isRemoved = newStatus === "left" || newStatus === "kicked";
|
|
131
|
+
const wasRemoved = oldStatus === "left" || oldStatus === "kicked";
|
|
132
|
+
if (oldStatus !== newStatus) {
|
|
133
|
+
let action = "";
|
|
134
|
+
let canListen = false;
|
|
135
|
+
let reason = "";
|
|
136
|
+
if (isRemoved) {
|
|
137
|
+
action = "❌ 频道主已移除 Bot";
|
|
138
|
+
canListen = false;
|
|
139
|
+
reason = "Bot 已被移除";
|
|
140
|
+
} else if (wasRemoved && isAdmin) {
|
|
141
|
+
action = "✅ 频道主已添加 Bot 为管理员";
|
|
142
|
+
canListen = true;
|
|
143
|
+
} else if (isAdmin && !wasAdmin) {
|
|
144
|
+
action = "✅ 频道主已授予 Bot 管理员权限";
|
|
145
|
+
canListen = true;
|
|
146
|
+
} else if (wasAdmin && !isAdmin) {
|
|
147
|
+
action = "⚠️ 频道主已移除 Bot 的管理员权限";
|
|
148
|
+
canListen = false;
|
|
149
|
+
reason = "Bot 不再是管理员";
|
|
150
|
+
} else if (wasRemoved && !isAdmin) {
|
|
151
|
+
action = "⚠️ 频道主已添加 Bot,但未授予管理员权限";
|
|
152
|
+
canListen = false;
|
|
153
|
+
reason = "需要管理员权限才能监听";
|
|
154
|
+
} else if (oldStatus === "restricted" && isAdmin) {
|
|
155
|
+
action = "✅ 频道主已授予 Bot 管理员权限";
|
|
156
|
+
canListen = true;
|
|
157
|
+
} else if (wasAdmin && newStatus === "restricted") {
|
|
158
|
+
action = "⚠️ 频道主已限制 Bot 的权限";
|
|
159
|
+
canListen = false;
|
|
160
|
+
reason = "Bot 权限受限,无法监听";
|
|
161
|
+
} else if (oldStatus === "restricted" && !isAdmin) {
|
|
162
|
+
action = `状态变更: ${oldStatus} → ${newStatus}`;
|
|
163
|
+
canListen = false;
|
|
164
|
+
reason = "需要管理员权限才能监听";
|
|
165
|
+
} else {
|
|
166
|
+
action = `状态变更: ${oldStatus} → ${newStatus}`;
|
|
167
|
+
canListen = isAdmin;
|
|
168
|
+
if (!canListen) {
|
|
169
|
+
reason = "需要管理员权限才能监听";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
init.logger.info(`📢 频道状态更新: ${chatTitle}`);
|
|
173
|
+
init.logger.line("", action);
|
|
174
|
+
init.logger.line(
|
|
175
|
+
"",
|
|
176
|
+
`当前状态: ${canListen ? "✅ 可监听预测消息" : "❌ 无法监听预测消息"}`
|
|
177
|
+
);
|
|
178
|
+
if (reason) {
|
|
179
|
+
init.logger.line("", `原因: ${reason}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (isAdmin && !this.targetChatIds.has(chatId)) {
|
|
183
|
+
this.targetChatIds.add(chatId);
|
|
184
|
+
await this.saveChatIds();
|
|
185
|
+
init.logger.line("📡", "已自动开始监听此频道");
|
|
186
|
+
}
|
|
187
|
+
if ((isRemoved || !isAdmin) && this.targetChatIds.has(chatId)) {
|
|
188
|
+
this.targetChatIds.delete(chatId);
|
|
189
|
+
await this.saveChatIds();
|
|
190
|
+
if (isRemoved) {
|
|
191
|
+
init.logger.line("⏹️", "已停止监听此频道(Bot 已被移除)");
|
|
192
|
+
} else {
|
|
193
|
+
init.logger.line("⏹️", "已停止监听此频道(Bot 不再是管理员)");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 保存频道 ID 列表到配置(用于持久化)
|
|
199
|
+
*/
|
|
200
|
+
async saveChatIds() {
|
|
201
|
+
if (!this.onChatIdsChangedCallback) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const chatIds = Array.from(this.targetChatIds);
|
|
206
|
+
await this.onChatIdsChangedCallback(chatIds);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
init.logger.error("保存频道 ID 列表时出错:", error);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 处理频道消息(channel_post 和 edited_channel_post)
|
|
213
|
+
*/
|
|
214
|
+
async handleChannelPost(ctx) {
|
|
215
|
+
const message = ctx.channelPost || ctx.editedChannelPost;
|
|
216
|
+
if (!message) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
await this.processMessage(message, ctx);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 处理频道消息(统一的消息处理逻辑)
|
|
223
|
+
*/
|
|
224
|
+
async processMessage(message, ctx) {
|
|
225
|
+
if (!message) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const chatId = message.chat.id;
|
|
229
|
+
const chatTitle = message.chat.title || "未知频道";
|
|
230
|
+
const isChannel = chatId < 0 && chatId.toString().startsWith("-100");
|
|
231
|
+
const messageText = message.text || message.caption || "";
|
|
232
|
+
if (!messageText) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (isChannel && !this.targetChatIds.has(chatId)) {
|
|
236
|
+
this.targetChatIds.add(chatId);
|
|
237
|
+
init.logger.info(`➕ 自动添加频道到监听列表: ${chatTitle} (ID: ${chatId})`);
|
|
238
|
+
await this.saveChatIds();
|
|
239
|
+
init.logger.line("💾", "已保存到配置文件");
|
|
240
|
+
}
|
|
241
|
+
if (!isChannel) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (!this.targetChatIds.has(chatId)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const metadata = parseSignalMetadata(messageText);
|
|
248
|
+
if (!metadata) {
|
|
249
|
+
if (messageText.includes("🤖")) {
|
|
250
|
+
const emojiIndex = messageText.indexOf("🤖");
|
|
251
|
+
const jsonStart = emojiIndex + 1;
|
|
252
|
+
const jsonEnd = Math.min(jsonStart + 200, messageText.length);
|
|
253
|
+
init.logger.warning(`解析预测消息失败:`);
|
|
254
|
+
init.logger.line("", `频道 ID: ${chatId}`);
|
|
255
|
+
init.logger.line(
|
|
256
|
+
"",
|
|
257
|
+
`消息片段: ${messageText.substring(emojiIndex, jsonEnd)}${messageText.length > jsonEnd ? "..." : ""}`
|
|
258
|
+
);
|
|
259
|
+
init.logger.line("", "提示: 请检查消息格式是否正确");
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const parsedSignal = {
|
|
264
|
+
metadata,
|
|
265
|
+
rawMessage: messageText,
|
|
266
|
+
messageId: message.message_id,
|
|
267
|
+
chatId,
|
|
268
|
+
timestamp: message.date * 1e3
|
|
269
|
+
};
|
|
270
|
+
if (this.onSignalCallback) {
|
|
271
|
+
try {
|
|
272
|
+
await this.onSignalCallback(parsedSignal);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
init.logger.error("处理信号时出错:", error);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* 设置信号回调函数
|
|
280
|
+
*/
|
|
281
|
+
onSignal(callback) {
|
|
282
|
+
this.onSignalCallback = callback;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* 设置频道 ID 列表变更回调函数(当频道自动添加/移除时调用,用于持久化)
|
|
286
|
+
*/
|
|
287
|
+
onChatIdsChanged(callback) {
|
|
288
|
+
this.onChatIdsChangedCallback = callback;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* 设置监听的频道 ID 列表(用于启动时加载已保存的配置)
|
|
292
|
+
*/
|
|
293
|
+
setTargetChatIds(chatIds) {
|
|
294
|
+
this.targetChatIds = new Set(chatIds);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* 启动监听
|
|
298
|
+
* @param onReady 可选回调,在频道检查完成后、开始监听之前调用
|
|
299
|
+
*/
|
|
300
|
+
async start(onReady) {
|
|
301
|
+
try {
|
|
302
|
+
init.logger.info("正在启动 Bot...");
|
|
303
|
+
const botInfo = await this.bot.api.getMe();
|
|
304
|
+
const botId = botInfo.id;
|
|
305
|
+
init.logger.info(`Bot 信息: @${botInfo.username} (ID: ${botId})`);
|
|
306
|
+
await this.checkAndDisplayChannels(botId);
|
|
307
|
+
if (onReady) {
|
|
308
|
+
await onReady();
|
|
309
|
+
}
|
|
310
|
+
init.logger.info("开始监听预测消息...");
|
|
311
|
+
await this.bot.start({
|
|
312
|
+
drop_pending_updates: true,
|
|
313
|
+
allowed_updates: [
|
|
314
|
+
"channel_post",
|
|
315
|
+
"edited_channel_post",
|
|
316
|
+
"my_chat_member"
|
|
317
|
+
]
|
|
318
|
+
});
|
|
319
|
+
} catch (error) {
|
|
320
|
+
init.logger.error("启动 Bot 失败:", error);
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* 检查并显示 Bot 所在的频道
|
|
326
|
+
* 验证配置文件中保存的频道,检查 Bot 是否仍在频道中以及是否是管理员
|
|
327
|
+
*/
|
|
328
|
+
async checkAndDisplayChannels(botId) {
|
|
329
|
+
const chatIds = Array.from(this.targetChatIds);
|
|
330
|
+
if (chatIds.length === 0) {
|
|
331
|
+
init.logger.info(`📋 当前没有保存的频道配置`);
|
|
332
|
+
init.logger.line(
|
|
333
|
+
"",
|
|
334
|
+
"提示: 当 Bot 被添加为频道管理员时,会自动添加到监听列表"
|
|
335
|
+
);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
init.logger.info(`📋 检查 Bot 所在的频道:`);
|
|
339
|
+
const validChannels = [];
|
|
340
|
+
const invalidChannels = [];
|
|
341
|
+
for (const chatId of chatIds) {
|
|
342
|
+
try {
|
|
343
|
+
const chat = await this.bot.api.getChat(chatId);
|
|
344
|
+
const chatTitle = chat.title || "未知频道";
|
|
345
|
+
try {
|
|
346
|
+
const member = await this.bot.api.getChatMember(chatId, botId);
|
|
347
|
+
const isAdmin = member.status === "administrator" || member.status === "creator";
|
|
348
|
+
validChannels.push({
|
|
349
|
+
id: chatId,
|
|
350
|
+
title: chatTitle,
|
|
351
|
+
isAdmin
|
|
352
|
+
});
|
|
353
|
+
} catch (error) {
|
|
354
|
+
invalidChannels.push({
|
|
355
|
+
id: chatId,
|
|
356
|
+
reason: "无法获取成员信息(可能已被移除)"
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
invalidChannels.push({
|
|
361
|
+
id: chatId,
|
|
362
|
+
reason: "无法获取频道信息(可能已被移除)"
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (validChannels.length > 0) {
|
|
367
|
+
const adminChannels = validChannels.filter((c) => c.isAdmin);
|
|
368
|
+
const nonAdminChannels = validChannels.filter((c) => !c.isAdmin);
|
|
369
|
+
if (adminChannels.length > 0) {
|
|
370
|
+
init.logger.info(`📡 可监听频道 (${adminChannels.length} 个):`);
|
|
371
|
+
adminChannels.forEach((channel, index) => {
|
|
372
|
+
init.logger.item(
|
|
373
|
+
`${index + 1}. ${channel.title} (ID: ${channel.id}) 👑 管理员`,
|
|
374
|
+
1
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
if (nonAdminChannels.length > 0) {
|
|
379
|
+
init.logger.warning(`无法监听频道 (${nonAdminChannels.length} 个):`);
|
|
380
|
+
nonAdminChannels.forEach((channel, index) => {
|
|
381
|
+
init.logger.item(`${index + 1}. ${channel.title}`, 1);
|
|
382
|
+
init.logger.item(`ID: ${channel.id}`, 2);
|
|
383
|
+
init.logger.item(`状态: 🚫 非管理员(无法接收预测消息)`, 2);
|
|
384
|
+
init.logger.item(`原因: 需要管理员权限才能监听`, 2);
|
|
385
|
+
});
|
|
386
|
+
nonAdminChannels.forEach((channel) => {
|
|
387
|
+
this.targetChatIds.delete(channel.id);
|
|
388
|
+
});
|
|
389
|
+
if (nonAdminChannels.length > 0) {
|
|
390
|
+
await this.saveChatIds();
|
|
391
|
+
init.logger.line("💾", "已清理无法监听的频道并保存配置");
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (invalidChannels.length > 0) {
|
|
396
|
+
init.logger.error(`无效频道 (${invalidChannels.length} 个):`);
|
|
397
|
+
invalidChannels.forEach((channel, index) => {
|
|
398
|
+
init.logger.item(`${index + 1}. ID: ${channel.id}`, 1);
|
|
399
|
+
init.logger.item(`原因: ${channel.reason}`, 2);
|
|
400
|
+
});
|
|
401
|
+
invalidChannels.forEach((channel) => {
|
|
402
|
+
this.targetChatIds.delete(channel.id);
|
|
403
|
+
});
|
|
404
|
+
if (invalidChannels.length > 0) {
|
|
405
|
+
await this.saveChatIds();
|
|
406
|
+
init.logger.line("💾", "已清理无效频道并保存配置");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* 停止监听
|
|
412
|
+
*/
|
|
413
|
+
async stop() {
|
|
414
|
+
await this.bot.stop();
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* 获取 bot 信息(用于验证 token 是否有效)
|
|
418
|
+
*/
|
|
419
|
+
async getBotInfo() {
|
|
420
|
+
try {
|
|
421
|
+
return this.bot.api.getMe();
|
|
422
|
+
} catch (error) {
|
|
423
|
+
throw new Error(`获取 Bot 信息失败: ${error}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
class TelegramService {
|
|
36
428
|
bot = null;
|
|
429
|
+
listener = null;
|
|
37
430
|
adminChatId = null;
|
|
38
|
-
|
|
431
|
+
notifyEnabled = false;
|
|
39
432
|
/**
|
|
40
|
-
*
|
|
433
|
+
* 初始化 Telegram 服务
|
|
41
434
|
*/
|
|
42
|
-
init(botToken, adminChatId) {
|
|
435
|
+
init(botToken, adminChatId, targetChatIds = []) {
|
|
436
|
+
if (this.listener) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
this.bot = new grammy.Bot(botToken);
|
|
440
|
+
this.listener = new TelegramListener(this.bot, targetChatIds);
|
|
43
441
|
if (!adminChatId) {
|
|
44
442
|
init.logger.warning("未配置管理员 Chat ID,通知功能已禁用");
|
|
443
|
+
this.notifyEnabled = false;
|
|
45
444
|
return;
|
|
46
445
|
}
|
|
47
|
-
this.bot = new grammy.Bot(botToken);
|
|
48
446
|
this.adminChatId = adminChatId;
|
|
49
|
-
this.
|
|
447
|
+
this.notifyEnabled = true;
|
|
50
448
|
init.logger.info("管理员通知已启用");
|
|
51
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* 设置频道 ID 列表变更回调
|
|
452
|
+
*/
|
|
453
|
+
onChatIdsChanged(callback) {
|
|
454
|
+
this.ensureListener().onChatIdsChanged(callback);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* 设置预测信号回调
|
|
458
|
+
*/
|
|
459
|
+
onSignal(callback) {
|
|
460
|
+
this.ensureListener().onSignal(callback);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* 启动监听
|
|
464
|
+
*/
|
|
465
|
+
async start(onReady) {
|
|
466
|
+
await this.ensureListener().start(onReady);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* 停止监听
|
|
470
|
+
*/
|
|
471
|
+
async stop() {
|
|
472
|
+
await this.ensureListener().stop();
|
|
473
|
+
}
|
|
52
474
|
/**
|
|
53
475
|
* 发送错误通知
|
|
54
476
|
*/
|
|
@@ -67,11 +489,17 @@ class Notifier {
|
|
|
67
489
|
async success(message, details) {
|
|
68
490
|
await this.send(`🟢 ${message}`, details);
|
|
69
491
|
}
|
|
492
|
+
ensureListener() {
|
|
493
|
+
if (!this.listener) {
|
|
494
|
+
throw new Error("TelegramService 未初始化");
|
|
495
|
+
}
|
|
496
|
+
return this.listener;
|
|
497
|
+
}
|
|
70
498
|
/**
|
|
71
499
|
* 发送消息给管理员
|
|
72
500
|
*/
|
|
73
501
|
async send(message, details) {
|
|
74
|
-
if (!this.
|
|
502
|
+
if (!this.notifyEnabled || !this.bot || !this.adminChatId) {
|
|
75
503
|
return;
|
|
76
504
|
}
|
|
77
505
|
const fullMessage = details ? `${message}
|
|
@@ -87,7 +515,7 @@ ${details}` : message;
|
|
|
87
515
|
}
|
|
88
516
|
}
|
|
89
517
|
}
|
|
90
|
-
const
|
|
518
|
+
const telegramService = new TelegramService();
|
|
91
519
|
const CLOB_HOST = "https://clob.polymarket.com";
|
|
92
520
|
class ClobClientWrapper {
|
|
93
521
|
client = null;
|
|
@@ -204,13 +632,13 @@ class ClobClientWrapper {
|
|
|
204
632
|
clobClient.OrderType.GTC
|
|
205
633
|
);
|
|
206
634
|
if (response.success) {
|
|
207
|
-
init.logger.success(
|
|
635
|
+
init.logger.success(`买单已提交: ${response.orderID}`);
|
|
208
636
|
return {
|
|
209
637
|
success: true,
|
|
210
638
|
orderId: response.orderID
|
|
211
639
|
};
|
|
212
640
|
} else {
|
|
213
|
-
init.logger.error(
|
|
641
|
+
init.logger.error(`买单提交失败: ${response.errorMsg}`);
|
|
214
642
|
return {
|
|
215
643
|
success: false,
|
|
216
644
|
orderId: "",
|
|
@@ -219,7 +647,7 @@ class ClobClientWrapper {
|
|
|
219
647
|
}
|
|
220
648
|
} catch (error) {
|
|
221
649
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
222
|
-
init.logger.error(
|
|
650
|
+
init.logger.error(`买单提交失败: ${errorMsg}`);
|
|
223
651
|
return {
|
|
224
652
|
success: false,
|
|
225
653
|
orderId: "",
|
|
@@ -410,7 +838,7 @@ class AssetFilter {
|
|
|
410
838
|
this.traded.set(endTime, /* @__PURE__ */ new Set());
|
|
411
839
|
}
|
|
412
840
|
this.traded.get(endTime).add(assetId);
|
|
413
|
-
init.logger.info(`已标记 assetId
|
|
841
|
+
init.logger.info(`已标记 assetId: ${assetId.slice(0, 10)}...`);
|
|
414
842
|
}
|
|
415
843
|
/**
|
|
416
844
|
* 清理过期记录
|
|
@@ -530,10 +958,20 @@ class Redeemer {
|
|
|
530
958
|
records = /* @__PURE__ */ new Map();
|
|
531
959
|
intervalId;
|
|
532
960
|
delayTimeoutId;
|
|
533
|
-
running
|
|
961
|
+
// stopped: 未启动; running: 正常轮询; paused: 限流等待恢复
|
|
962
|
+
state = "stopped";
|
|
963
|
+
scheduler;
|
|
534
964
|
redeemFn = null;
|
|
535
965
|
funderAddress = "";
|
|
536
966
|
onRedeemSuccess = null;
|
|
967
|
+
constructor(scheduler = {
|
|
968
|
+
setInterval: setPromiseInterval__default.default,
|
|
969
|
+
clearInterval: setPromiseInterval.clearPromiseInterval,
|
|
970
|
+
setTimeout,
|
|
971
|
+
clearTimeout
|
|
972
|
+
}) {
|
|
973
|
+
this.scheduler = scheduler;
|
|
974
|
+
}
|
|
537
975
|
/**
|
|
538
976
|
* 启动 Redeemer
|
|
539
977
|
* @param redeemFn 执行 Redeem 的函数(返回交易哈希)
|
|
@@ -541,26 +979,24 @@ class Redeemer {
|
|
|
541
979
|
* @param onRedeemSuccess Redeem 成功后的回调(用于更新余额缓存)
|
|
542
980
|
*/
|
|
543
981
|
start(redeemFn, funderAddress, onRedeemSuccess) {
|
|
544
|
-
if (this.
|
|
982
|
+
if (this.state !== "stopped") {
|
|
545
983
|
return;
|
|
546
984
|
}
|
|
547
985
|
this.redeemFn = redeemFn;
|
|
548
986
|
this.funderAddress = funderAddress;
|
|
549
987
|
this.onRedeemSuccess = onRedeemSuccess || null;
|
|
550
|
-
this.
|
|
551
|
-
init.logger.
|
|
552
|
-
this.
|
|
553
|
-
await this.runOnce();
|
|
554
|
-
}, BASE_INTERVAL);
|
|
988
|
+
this.state = "running";
|
|
989
|
+
init.logger.success("自动 redeem 已启动");
|
|
990
|
+
this.startInterval();
|
|
555
991
|
}
|
|
556
992
|
/**
|
|
557
|
-
*
|
|
993
|
+
* 启动定时任务(仅在 running 状态生效)
|
|
558
994
|
*/
|
|
559
995
|
startInterval() {
|
|
560
|
-
if (
|
|
996
|
+
if (this.state !== "running" || this.intervalId !== void 0) {
|
|
561
997
|
return;
|
|
562
998
|
}
|
|
563
|
-
this.intervalId =
|
|
999
|
+
this.intervalId = this.scheduler.setInterval(async () => {
|
|
564
1000
|
await this.runOnce();
|
|
565
1001
|
}, BASE_INTERVAL);
|
|
566
1002
|
}
|
|
@@ -569,15 +1005,19 @@ class Redeemer {
|
|
|
569
1005
|
*/
|
|
570
1006
|
pauseAndRestart(delayMs) {
|
|
571
1007
|
if (this.intervalId !== void 0) {
|
|
572
|
-
|
|
1008
|
+
this.scheduler.clearInterval(this.intervalId);
|
|
573
1009
|
this.intervalId = void 0;
|
|
574
1010
|
}
|
|
575
|
-
this.delayTimeoutId
|
|
1011
|
+
if (this.delayTimeoutId) {
|
|
1012
|
+
this.scheduler.clearTimeout(this.delayTimeoutId);
|
|
1013
|
+
this.delayTimeoutId = void 0;
|
|
1014
|
+
}
|
|
1015
|
+
this.state = "paused";
|
|
1016
|
+
this.delayTimeoutId = this.scheduler.setTimeout(() => {
|
|
576
1017
|
this.delayTimeoutId = void 0;
|
|
577
|
-
if (this.
|
|
578
|
-
this.
|
|
579
|
-
|
|
580
|
-
});
|
|
1018
|
+
if (this.state === "paused") {
|
|
1019
|
+
this.state = "running";
|
|
1020
|
+
this.startInterval();
|
|
581
1021
|
}
|
|
582
1022
|
}, delayMs);
|
|
583
1023
|
}
|
|
@@ -586,14 +1026,14 @@ class Redeemer {
|
|
|
586
1026
|
*/
|
|
587
1027
|
stop() {
|
|
588
1028
|
if (this.intervalId !== void 0) {
|
|
589
|
-
|
|
1029
|
+
this.scheduler.clearInterval(this.intervalId);
|
|
590
1030
|
this.intervalId = void 0;
|
|
591
1031
|
}
|
|
592
1032
|
if (this.delayTimeoutId) {
|
|
593
|
-
clearTimeout(this.delayTimeoutId);
|
|
1033
|
+
this.scheduler.clearTimeout(this.delayTimeoutId);
|
|
594
1034
|
this.delayTimeoutId = void 0;
|
|
595
1035
|
}
|
|
596
|
-
this.
|
|
1036
|
+
this.state = "stopped";
|
|
597
1037
|
init.logger.info("Redeemer 已停止");
|
|
598
1038
|
}
|
|
599
1039
|
/**
|
|
@@ -621,7 +1061,9 @@ class Redeemer {
|
|
|
621
1061
|
positions.push({
|
|
622
1062
|
conditionId: item.conditionId,
|
|
623
1063
|
assetId: item.asset,
|
|
624
|
-
currentValue
|
|
1064
|
+
currentValue,
|
|
1065
|
+
slug: item.slug,
|
|
1066
|
+
outcome: item.outcome
|
|
625
1067
|
});
|
|
626
1068
|
} else {
|
|
627
1069
|
break;
|
|
@@ -633,7 +1075,7 @@ class Redeemer {
|
|
|
633
1075
|
* 执行一轮 Redeem
|
|
634
1076
|
*/
|
|
635
1077
|
async runOnce() {
|
|
636
|
-
if (
|
|
1078
|
+
if (this.state !== "running" || !this.redeemFn) {
|
|
637
1079
|
return;
|
|
638
1080
|
}
|
|
639
1081
|
let positions;
|
|
@@ -644,14 +1086,26 @@ class Redeemer {
|
|
|
644
1086
|
if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
|
|
645
1087
|
const resetSeconds = this.parseResetSeconds(errorMsg);
|
|
646
1088
|
init.logger.warning(`获取仓位限流,${resetSeconds} 秒后重试`);
|
|
647
|
-
|
|
1089
|
+
telegramService.warning(
|
|
1090
|
+
"API 限流",
|
|
1091
|
+
`获取仓位限流,${resetSeconds} 秒后重试`
|
|
1092
|
+
);
|
|
648
1093
|
this.pauseAndRestart((resetSeconds + 60) * 1e3);
|
|
649
1094
|
return;
|
|
650
1095
|
}
|
|
651
1096
|
init.logger.error(`获取仓位失败: ${errorMsg}`);
|
|
652
1097
|
return;
|
|
653
1098
|
}
|
|
654
|
-
|
|
1099
|
+
const pendingRedeemablePositions = positions.filter((position) => {
|
|
1100
|
+
const record = this.records.get(position.conditionId);
|
|
1101
|
+
return !record?.success;
|
|
1102
|
+
});
|
|
1103
|
+
if (pendingRedeemablePositions.length > 0) {
|
|
1104
|
+
this.logPositions(
|
|
1105
|
+
`自动 redeem: 发现 ${pendingRedeemablePositions.length} 个可 Redeem 仓位`,
|
|
1106
|
+
pendingRedeemablePositions.map((position) => ({ position }))
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
655
1109
|
for (const position of positions) {
|
|
656
1110
|
let record = this.records.get(position.conditionId);
|
|
657
1111
|
if (!record) {
|
|
@@ -670,7 +1124,7 @@ class Redeemer {
|
|
|
670
1124
|
init.logger.error(
|
|
671
1125
|
`Redeem 失败: 执行成功但仍存在 ${record.matchedCount} 次 (conditionId: ${position.conditionId.slice(0, 10)}...)`
|
|
672
1126
|
);
|
|
673
|
-
|
|
1127
|
+
telegramService.error(
|
|
674
1128
|
"Redeem 失败",
|
|
675
1129
|
`执行成功但仓位仍存在 ${record.matchedCount} 次
|
|
676
1130
|
conditionId: ${position.conditionId}
|
|
@@ -679,24 +1133,24 @@ conditionId: ${position.conditionId}
|
|
|
679
1133
|
}
|
|
680
1134
|
} else if (record.failedCount < MAX_MATCH_TIMES) {
|
|
681
1135
|
try {
|
|
682
|
-
const txHash = await this.redeemFn(position.conditionId);
|
|
1136
|
+
const txHash = await this.redeemFn?.(position.conditionId);
|
|
683
1137
|
record.success = true;
|
|
684
|
-
init.logger.info(`Redeem
|
|
1138
|
+
init.logger.info(`Redeem 已执行: ${txHash}`);
|
|
685
1139
|
} catch (error) {
|
|
686
|
-
record.failedCount++;
|
|
687
1140
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
688
1141
|
if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
|
|
689
1142
|
const resetSeconds = this.parseResetSeconds(errorMsg);
|
|
690
1143
|
init.logger.warning(`Redeem 限流,${resetSeconds} 秒后重试`);
|
|
691
|
-
|
|
1144
|
+
telegramService.warning("Redeem 限流", `${resetSeconds} 秒后重试`);
|
|
692
1145
|
this.pauseAndRestart((resetSeconds + 60) * 1e3);
|
|
693
1146
|
return;
|
|
694
1147
|
}
|
|
1148
|
+
record.failedCount++;
|
|
695
1149
|
init.logger.error(
|
|
696
1150
|
`Redeem 异常 (第 ${record.failedCount} 次): ${errorMsg}`
|
|
697
1151
|
);
|
|
698
1152
|
if (record.failedCount >= MAX_MATCH_TIMES) {
|
|
699
|
-
|
|
1153
|
+
telegramService.error(
|
|
700
1154
|
"Redeem 异常",
|
|
701
1155
|
`重试 ${record.failedCount} 次仍失败
|
|
702
1156
|
conditionId: ${position.conditionId}
|
|
@@ -706,24 +1160,49 @@ conditionId: ${position.conditionId}
|
|
|
706
1160
|
}
|
|
707
1161
|
}
|
|
708
1162
|
}
|
|
709
|
-
const
|
|
1163
|
+
const successItems = [];
|
|
710
1164
|
for (const [conditionId, record] of this.records.entries()) {
|
|
711
1165
|
const stillExists = positions.some((p) => p.conditionId === conditionId);
|
|
712
1166
|
if (!stillExists) {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
1167
|
+
successItems.push({
|
|
1168
|
+
position: record.position,
|
|
1169
|
+
matchedCount: record.matchedCount
|
|
1170
|
+
});
|
|
716
1171
|
this.records.delete(conditionId);
|
|
717
1172
|
}
|
|
718
1173
|
}
|
|
719
|
-
if (
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
1174
|
+
if (successItems.length > 0) {
|
|
1175
|
+
const successTitle = `自动 redeem: 成功 ${successItems.length} 个`;
|
|
1176
|
+
const successLines = this.logPositions(successTitle, successItems);
|
|
1177
|
+
telegramService.success(successTitle, successLines.join("\n"));
|
|
724
1178
|
this.onRedeemSuccess?.();
|
|
725
1179
|
}
|
|
726
|
-
init.logger.info(`redeem
|
|
1180
|
+
init.logger.info(`redeem 状态: ${this.records.size} 个待确认`);
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* 统一格式化仓位信息
|
|
1184
|
+
*/
|
|
1185
|
+
formatPositionLine(position, matchedCount) {
|
|
1186
|
+
const label = position.slug || `${position.assetId.slice(0, 10)}...`;
|
|
1187
|
+
const outcomeLabel = position.outcome ? ` ${position.outcome}` : "";
|
|
1188
|
+
const matchedLabel = matchedCount !== void 0 ? ` (matched: ${matchedCount})` : "";
|
|
1189
|
+
return `${label}${outcomeLabel}: ${position.currentValue} USDC${matchedLabel}`;
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* 统一输出 Redeem 仓位列表
|
|
1193
|
+
*/
|
|
1194
|
+
logPositions(title, items) {
|
|
1195
|
+
init.logger.info(title);
|
|
1196
|
+
const lines = [];
|
|
1197
|
+
for (const item of items) {
|
|
1198
|
+
const line = this.formatPositionLine(
|
|
1199
|
+
item.position,
|
|
1200
|
+
item.matchedCount
|
|
1201
|
+
);
|
|
1202
|
+
lines.push(line);
|
|
1203
|
+
init.logger.line("", `- ${line}`);
|
|
1204
|
+
}
|
|
1205
|
+
return lines;
|
|
727
1206
|
}
|
|
728
1207
|
/**
|
|
729
1208
|
* 解析 429 错误中的 resets in 时间
|
|
@@ -833,7 +1312,7 @@ class Trader {
|
|
|
833
1312
|
if (amountMode === "fixed" && balance < amountValue) {
|
|
834
1313
|
const msg = `余额不足: ${balance} USDC < ${amountValue} USDC`;
|
|
835
1314
|
init.logger.warning(msg);
|
|
836
|
-
|
|
1315
|
+
telegramService.warning(
|
|
837
1316
|
"余额不足",
|
|
838
1317
|
`当前余额: ${balance} USDC
|
|
839
1318
|
需要金额: ${amountValue} USDC`
|
|
@@ -842,7 +1321,7 @@ class Trader {
|
|
|
842
1321
|
}
|
|
843
1322
|
if (amountMode === "percentage" && balance <= 0) {
|
|
844
1323
|
init.logger.warning(`余额为零: ${balance} USDC`);
|
|
845
|
-
|
|
1324
|
+
telegramService.warning("余额为零", `当前余额: ${balance} USDC`);
|
|
846
1325
|
return;
|
|
847
1326
|
}
|
|
848
1327
|
const orderAmount = amountMode === "fixed" ? amountValue : balance * amountValue;
|
|
@@ -853,7 +1332,7 @@ class Trader {
|
|
|
853
1332
|
const buyResult = await this.client.createBuyOrder(assetId, buyPrice, size);
|
|
854
1333
|
if (!buyResult.success) {
|
|
855
1334
|
init.logger.error(`买单失败: ${buyResult.errorMsg}`);
|
|
856
|
-
|
|
1335
|
+
telegramService.error(
|
|
857
1336
|
"买单失败",
|
|
858
1337
|
`事件: ${metadata.eventId}
|
|
859
1338
|
错误: ${buyResult.errorMsg}`
|
|
@@ -862,8 +1341,7 @@ class Trader {
|
|
|
862
1341
|
}
|
|
863
1342
|
this.assetFilter.markTraded(assetId, endTime);
|
|
864
1343
|
if (sellPrice === 1) {
|
|
865
|
-
init.logger.
|
|
866
|
-
init.logger.line("", "sellPrice = 1,成交后将自动 redeem");
|
|
1344
|
+
init.logger.info("sellPrice = 1,成交后将自动 redeem");
|
|
867
1345
|
return;
|
|
868
1346
|
}
|
|
869
1347
|
this.watchAndHandle(
|
|
@@ -894,7 +1372,7 @@ class Trader {
|
|
|
894
1372
|
} catch (error) {
|
|
895
1373
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
896
1374
|
init.logger.error(`订单监控异常: ${errorMsg}`);
|
|
897
|
-
|
|
1375
|
+
telegramService.error(
|
|
898
1376
|
"订单监控异常",
|
|
899
1377
|
`事件: ${eventId}
|
|
900
1378
|
订单: ${orderId}
|
|
@@ -922,11 +1400,11 @@ class Trader {
|
|
|
922
1400
|
break;
|
|
923
1401
|
case "FAILED":
|
|
924
1402
|
init.logger.error("订单执行失败");
|
|
925
|
-
|
|
1403
|
+
telegramService.error("订单执行失败", `订单 ID: ${orderId}`);
|
|
926
1404
|
break;
|
|
927
1405
|
case "TIMEOUT":
|
|
928
1406
|
init.logger.warning("订单超时,尝试取消");
|
|
929
|
-
|
|
1407
|
+
telegramService.warning("订单超时", `订单 ID: ${orderId}
|
|
930
1408
|
已尝试取消`);
|
|
931
1409
|
await this.client.cancelOrder(orderId);
|
|
932
1410
|
break;
|
|
@@ -969,7 +1447,7 @@ class Trader {
|
|
|
969
1447
|
if (!this.relayClient.isInitialized()) {
|
|
970
1448
|
const msg = "仓位同步超时且 Redeem 不可用";
|
|
971
1449
|
init.logger.warning(msg);
|
|
972
|
-
|
|
1450
|
+
telegramService.warning(
|
|
973
1451
|
msg,
|
|
974
1452
|
`Asset: ${assetId.substring(0, 20)}...
|
|
975
1453
|
预期数量: ${expectedSize.toFixed(4)}
|
|
@@ -990,10 +1468,10 @@ class Trader {
|
|
|
990
1468
|
init.logger.info(`创建卖单: ${size.toFixed(4)} 份 @ ${price}`);
|
|
991
1469
|
const sellResult = await this.client.createSellOrder(assetId, price, size);
|
|
992
1470
|
if (sellResult.success) {
|
|
993
|
-
init.logger.success(
|
|
1471
|
+
init.logger.success(`卖单已提交: ${sellResult.orderId}`);
|
|
994
1472
|
} else {
|
|
995
|
-
init.logger.error(
|
|
996
|
-
|
|
1473
|
+
init.logger.error(`卖单提交失败: ${sellResult.errorMsg}`);
|
|
1474
|
+
telegramService.error("卖单提交失败", `错误: ${sellResult.errorMsg}`);
|
|
997
1475
|
}
|
|
998
1476
|
}
|
|
999
1477
|
/**
|
|
@@ -1045,7 +1523,11 @@ async function main() {
|
|
|
1045
1523
|
init.logger.init();
|
|
1046
1524
|
init.logger.section("PolyMarket 跟单机器人");
|
|
1047
1525
|
const config = await init.ensureConfig();
|
|
1048
|
-
|
|
1526
|
+
telegramService.init(
|
|
1527
|
+
config.telegram.botToken,
|
|
1528
|
+
config.telegram.adminChatId,
|
|
1529
|
+
config.telegram.targetChatIds || []
|
|
1530
|
+
);
|
|
1049
1531
|
const autoTradeEnabled = init.canAutoTrade(config);
|
|
1050
1532
|
if (!autoTradeEnabled) {
|
|
1051
1533
|
init.logger.warning("自动跟单功能未启用");
|
|
@@ -1075,11 +1557,7 @@ async function main() {
|
|
|
1075
1557
|
init.logger.line("", "当前不会跟单任何预测消息");
|
|
1076
1558
|
}
|
|
1077
1559
|
}
|
|
1078
|
-
|
|
1079
|
-
config.telegram.botToken,
|
|
1080
|
-
config.telegram.targetChatIds || []
|
|
1081
|
-
);
|
|
1082
|
-
listener.onChatIdsChanged((chatIds) => {
|
|
1560
|
+
telegramService.onChatIdsChanged((chatIds) => {
|
|
1083
1561
|
const telegram = init.configLocal.getItem("telegram");
|
|
1084
1562
|
if (!telegram) {
|
|
1085
1563
|
init.logger.error("警告: 无法保存频道 ID,telegram 配置不存在");
|
|
@@ -1089,7 +1567,7 @@ async function main() {
|
|
|
1089
1567
|
init.configLocal.setItem("telegram", telegram);
|
|
1090
1568
|
init.logger.line("💾", `已保存频道 ID 列表: ${chatIds.length} 个频道`);
|
|
1091
1569
|
});
|
|
1092
|
-
|
|
1570
|
+
telegramService.onSignal(async (signal) => {
|
|
1093
1571
|
init.logger.info(`📨 收到预测信号:`);
|
|
1094
1572
|
init.logger.line("", `事件: ${signal.metadata.eventId}`);
|
|
1095
1573
|
init.logger.line(
|
|
@@ -1114,29 +1592,24 @@ async function main() {
|
|
|
1114
1592
|
enterDaemonMode();
|
|
1115
1593
|
return;
|
|
1116
1594
|
}
|
|
1117
|
-
const runMode = process.env.POLYCOPY_DAEMON_CHILD === "1" ? "后台模式" : "前台模式";
|
|
1118
|
-
notifier.success("程序已启动", `模式: ${runMode}
|
|
1119
|
-
PID: ${process.pid}`);
|
|
1120
1595
|
let isShuttingDown = false;
|
|
1121
|
-
const shutdown = () => {
|
|
1596
|
+
const shutdown = async () => {
|
|
1122
1597
|
if (isShuttingDown) return;
|
|
1123
1598
|
isShuttingDown = true;
|
|
1124
1599
|
process.removeAllListeners("SIGINT");
|
|
1125
1600
|
process.removeAllListeners("SIGTERM");
|
|
1126
1601
|
init.logger.blank();
|
|
1127
1602
|
init.logger.info("正在退出...");
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
setTimeout(() => {
|
|
1131
|
-
process.exit(0);
|
|
1132
|
-
}, 1e3);
|
|
1133
|
-
Promise.all([trader.shutdown(), listener.stop()]).finally(() => {
|
|
1134
|
-
process.exit(0);
|
|
1135
|
-
});
|
|
1603
|
+
await Promise.all([trader.shutdown(), telegramService.stop()]);
|
|
1604
|
+
process.exit(0);
|
|
1136
1605
|
};
|
|
1137
|
-
process.
|
|
1138
|
-
|
|
1139
|
-
|
|
1606
|
+
process.prependListener("SIGINT", () => {
|
|
1607
|
+
shutdown();
|
|
1608
|
+
});
|
|
1609
|
+
process.prependListener("SIGTERM", () => {
|
|
1610
|
+
shutdown();
|
|
1611
|
+
});
|
|
1612
|
+
await telegramService.start(() => {
|
|
1140
1613
|
if (trader.isInitialized()) {
|
|
1141
1614
|
trader.startRedeemer();
|
|
1142
1615
|
}
|
|
@@ -192,398 +192,6 @@ ${this.withTimestamp(`❌ ${formatted}`)}`);
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
const logger = new Logger();
|
|
195
|
-
function parseSignalMetadata(messageText) {
|
|
196
|
-
const match = messageText.match(/🤖\s*({.*})/s);
|
|
197
|
-
if (!match) {
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
try {
|
|
201
|
-
const jsonStr = match[1];
|
|
202
|
-
const metadata = JSON.parse(jsonStr);
|
|
203
|
-
if (!metadata.eventId || !metadata.assetId || !metadata.outcome || !metadata.endTime) {
|
|
204
|
-
logger.error("解析失败: 缺少必需字段", {
|
|
205
|
-
hasEventId: !!metadata.eventId,
|
|
206
|
-
hasAssetId: !!metadata.assetId,
|
|
207
|
-
hasOutcome: !!metadata.outcome,
|
|
208
|
-
hasEndTime: !!metadata.endTime
|
|
209
|
-
});
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
if (metadata.outcome !== "Up" && metadata.outcome !== "Down") {
|
|
213
|
-
logger.error("解析失败: outcome 值无效", {
|
|
214
|
-
outcome: metadata.outcome,
|
|
215
|
-
expected: "Up 或 Down"
|
|
216
|
-
});
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
if (typeof metadata.endTime !== "number" || metadata.endTime <= 0) {
|
|
220
|
-
logger.error("解析失败: endTime 值无效", {
|
|
221
|
-
endTime: metadata.endTime,
|
|
222
|
-
expected: "毫秒级时间戳"
|
|
223
|
-
});
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
return metadata;
|
|
227
|
-
} catch (error) {
|
|
228
|
-
logger.error("解析信号元数据失败:", error);
|
|
229
|
-
if (error instanceof Error) {
|
|
230
|
-
logger.line("", `错误详情: ${error.message}`);
|
|
231
|
-
}
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
class TelegramListener {
|
|
236
|
-
bot;
|
|
237
|
-
onSignalCallback;
|
|
238
|
-
onChatIdsChangedCallback;
|
|
239
|
-
targetChatIds;
|
|
240
|
-
// 运行时监听的频道 ID 集合(自动管理)
|
|
241
|
-
constructor(botToken, targetChatIds = []) {
|
|
242
|
-
this.bot = new grammy.Bot(botToken);
|
|
243
|
-
this.targetChatIds = new Set(targetChatIds);
|
|
244
|
-
this.setupHandlers();
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* 设置消息处理器
|
|
248
|
-
*/
|
|
249
|
-
setupHandlers() {
|
|
250
|
-
this.bot.on("channel_post", async (ctx) => {
|
|
251
|
-
await this.handleChannelPost(ctx);
|
|
252
|
-
});
|
|
253
|
-
this.bot.on("edited_channel_post", async (ctx) => {
|
|
254
|
-
await this.handleChannelPost(ctx);
|
|
255
|
-
});
|
|
256
|
-
this.bot.on("my_chat_member", async (ctx) => {
|
|
257
|
-
await this.handleMyChatMemberUpdate(ctx);
|
|
258
|
-
});
|
|
259
|
-
this.bot.catch((err) => {
|
|
260
|
-
logger.error("Telegram Bot 错误:", err);
|
|
261
|
-
if (err.error) {
|
|
262
|
-
logger.line("", `错误详情: ${err.error}`);
|
|
263
|
-
}
|
|
264
|
-
if (err.ctx) {
|
|
265
|
-
logger.line("", `上下文信息:`, {
|
|
266
|
-
chatId: err.ctx.chat?.id,
|
|
267
|
-
messageId: err.ctx.message?.message_id
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* 处理 Bot 自己的成员状态变更
|
|
274
|
-
*/
|
|
275
|
-
async handleMyChatMemberUpdate(ctx) {
|
|
276
|
-
const update = ctx.myChatMember;
|
|
277
|
-
if (!update) {
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
const chatId = update.chat.id;
|
|
281
|
-
const chatTitle = update.chat.title || "未知频道";
|
|
282
|
-
const oldStatus = update.old_chat_member.status;
|
|
283
|
-
const newStatus = update.new_chat_member.status;
|
|
284
|
-
const chatType = update.chat.type;
|
|
285
|
-
if (chatType !== "channel") {
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
const isAdmin = newStatus === "administrator" || newStatus === "creator";
|
|
289
|
-
const wasAdmin = oldStatus === "administrator" || oldStatus === "creator";
|
|
290
|
-
const isRemoved = newStatus === "left" || newStatus === "kicked";
|
|
291
|
-
const wasRemoved = oldStatus === "left" || oldStatus === "kicked";
|
|
292
|
-
if (oldStatus !== newStatus) {
|
|
293
|
-
let action = "";
|
|
294
|
-
let canListen = false;
|
|
295
|
-
let reason = "";
|
|
296
|
-
if (isRemoved) {
|
|
297
|
-
action = "❌ 频道主已移除 Bot";
|
|
298
|
-
canListen = false;
|
|
299
|
-
reason = "Bot 已被移除";
|
|
300
|
-
} else if (wasRemoved && isAdmin) {
|
|
301
|
-
action = "✅ 频道主已添加 Bot 为管理员";
|
|
302
|
-
canListen = true;
|
|
303
|
-
} else if (isAdmin && !wasAdmin) {
|
|
304
|
-
action = "✅ 频道主已授予 Bot 管理员权限";
|
|
305
|
-
canListen = true;
|
|
306
|
-
} else if (wasAdmin && !isAdmin) {
|
|
307
|
-
action = "⚠️ 频道主已移除 Bot 的管理员权限";
|
|
308
|
-
canListen = false;
|
|
309
|
-
reason = "Bot 不再是管理员";
|
|
310
|
-
} else if (wasRemoved && !isAdmin) {
|
|
311
|
-
action = "⚠️ 频道主已添加 Bot,但未授予管理员权限";
|
|
312
|
-
canListen = false;
|
|
313
|
-
reason = "需要管理员权限才能监听";
|
|
314
|
-
} else if (oldStatus === "restricted" && isAdmin) {
|
|
315
|
-
action = "✅ 频道主已授予 Bot 管理员权限";
|
|
316
|
-
canListen = true;
|
|
317
|
-
} else if (wasAdmin && newStatus === "restricted") {
|
|
318
|
-
action = "⚠️ 频道主已限制 Bot 的权限";
|
|
319
|
-
canListen = false;
|
|
320
|
-
reason = "Bot 权限受限,无法监听";
|
|
321
|
-
} else if (oldStatus === "restricted" && !isAdmin) {
|
|
322
|
-
action = `状态变更: ${oldStatus} → ${newStatus}`;
|
|
323
|
-
canListen = false;
|
|
324
|
-
reason = "需要管理员权限才能监听";
|
|
325
|
-
} else {
|
|
326
|
-
action = `状态变更: ${oldStatus} → ${newStatus}`;
|
|
327
|
-
canListen = isAdmin;
|
|
328
|
-
if (!canListen) {
|
|
329
|
-
reason = "需要管理员权限才能监听";
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
logger.info(`📢 频道状态更新: ${chatTitle}`);
|
|
333
|
-
logger.line("", action);
|
|
334
|
-
logger.line(
|
|
335
|
-
"",
|
|
336
|
-
`当前状态: ${canListen ? "✅ 可监听预测消息" : "❌ 无法监听预测消息"}`
|
|
337
|
-
);
|
|
338
|
-
if (reason) {
|
|
339
|
-
logger.line("", `原因: ${reason}`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
if (isAdmin && !this.targetChatIds.has(chatId)) {
|
|
343
|
-
this.targetChatIds.add(chatId);
|
|
344
|
-
await this.saveChatIds();
|
|
345
|
-
logger.line("📡", "已自动开始监听此频道");
|
|
346
|
-
}
|
|
347
|
-
if ((isRemoved || !isAdmin) && this.targetChatIds.has(chatId)) {
|
|
348
|
-
this.targetChatIds.delete(chatId);
|
|
349
|
-
await this.saveChatIds();
|
|
350
|
-
if (isRemoved) {
|
|
351
|
-
logger.line("⏹️", "已停止监听此频道(Bot 已被移除)");
|
|
352
|
-
} else {
|
|
353
|
-
logger.line("⏹️", "已停止监听此频道(Bot 不再是管理员)");
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* 保存频道 ID 列表到配置(用于持久化)
|
|
359
|
-
*/
|
|
360
|
-
async saveChatIds() {
|
|
361
|
-
if (!this.onChatIdsChangedCallback) {
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
try {
|
|
365
|
-
const chatIds = Array.from(this.targetChatIds);
|
|
366
|
-
await this.onChatIdsChangedCallback(chatIds);
|
|
367
|
-
} catch (error) {
|
|
368
|
-
logger.error("保存频道 ID 列表时出错:", error);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* 处理频道消息(channel_post 和 edited_channel_post)
|
|
373
|
-
*/
|
|
374
|
-
async handleChannelPost(ctx) {
|
|
375
|
-
const message = ctx.channelPost || ctx.editedChannelPost;
|
|
376
|
-
if (!message) {
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
await this.processMessage(message, ctx);
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* 处理频道消息(统一的消息处理逻辑)
|
|
383
|
-
*/
|
|
384
|
-
async processMessage(message, ctx) {
|
|
385
|
-
if (!message) {
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
const chatId = message.chat.id;
|
|
389
|
-
const chatTitle = message.chat.title || "未知频道";
|
|
390
|
-
const isChannel = chatId < 0 && chatId.toString().startsWith("-100");
|
|
391
|
-
const messageText = message.text || message.caption || "";
|
|
392
|
-
if (!messageText) {
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
if (isChannel && !this.targetChatIds.has(chatId)) {
|
|
396
|
-
this.targetChatIds.add(chatId);
|
|
397
|
-
logger.info(`➕ 自动添加频道到监听列表: ${chatTitle} (ID: ${chatId})`);
|
|
398
|
-
await this.saveChatIds();
|
|
399
|
-
logger.line("💾", "已保存到配置文件");
|
|
400
|
-
}
|
|
401
|
-
if (!isChannel) {
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
if (!this.targetChatIds.has(chatId)) {
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
const metadata = parseSignalMetadata(messageText);
|
|
408
|
-
if (!metadata) {
|
|
409
|
-
if (messageText.includes("🤖")) {
|
|
410
|
-
const emojiIndex = messageText.indexOf("🤖");
|
|
411
|
-
const jsonStart = emojiIndex + 1;
|
|
412
|
-
const jsonEnd = Math.min(jsonStart + 200, messageText.length);
|
|
413
|
-
logger.warning(`解析预测消息失败:`);
|
|
414
|
-
logger.line("", `频道 ID: ${chatId}`);
|
|
415
|
-
logger.line(
|
|
416
|
-
"",
|
|
417
|
-
`消息片段: ${messageText.substring(emojiIndex, jsonEnd)}${messageText.length > jsonEnd ? "..." : ""}`
|
|
418
|
-
);
|
|
419
|
-
logger.line("", "提示: 请检查消息格式是否正确");
|
|
420
|
-
}
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
const parsedSignal = {
|
|
424
|
-
metadata,
|
|
425
|
-
rawMessage: messageText,
|
|
426
|
-
messageId: message.message_id,
|
|
427
|
-
chatId,
|
|
428
|
-
timestamp: message.date * 1e3
|
|
429
|
-
};
|
|
430
|
-
if (this.onSignalCallback) {
|
|
431
|
-
try {
|
|
432
|
-
await this.onSignalCallback(parsedSignal);
|
|
433
|
-
} catch (error) {
|
|
434
|
-
logger.error("处理信号时出错:", error);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* 设置信号回调函数
|
|
440
|
-
*/
|
|
441
|
-
onSignal(callback) {
|
|
442
|
-
this.onSignalCallback = callback;
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* 设置频道 ID 列表变更回调函数(当频道自动添加/移除时调用,用于持久化)
|
|
446
|
-
*/
|
|
447
|
-
onChatIdsChanged(callback) {
|
|
448
|
-
this.onChatIdsChangedCallback = callback;
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* 设置监听的频道 ID 列表(用于启动时加载已保存的配置)
|
|
452
|
-
*/
|
|
453
|
-
setTargetChatIds(chatIds) {
|
|
454
|
-
this.targetChatIds = new Set(chatIds);
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* 启动监听
|
|
458
|
-
* @param onReady 可选回调,在频道检查完成后、开始监听之前调用
|
|
459
|
-
*/
|
|
460
|
-
async start(onReady) {
|
|
461
|
-
try {
|
|
462
|
-
logger.info("正在启动 Bot...");
|
|
463
|
-
const botInfo = await this.bot.api.getMe();
|
|
464
|
-
const botId = botInfo.id;
|
|
465
|
-
logger.success(`Bot 信息: @${botInfo.username} (ID: ${botId})`);
|
|
466
|
-
await this.checkAndDisplayChannels(botId);
|
|
467
|
-
if (onReady) {
|
|
468
|
-
await onReady();
|
|
469
|
-
}
|
|
470
|
-
logger.info("开始监听预测消息...");
|
|
471
|
-
await this.bot.start({
|
|
472
|
-
drop_pending_updates: true,
|
|
473
|
-
allowed_updates: [
|
|
474
|
-
"channel_post",
|
|
475
|
-
"edited_channel_post",
|
|
476
|
-
"my_chat_member"
|
|
477
|
-
]
|
|
478
|
-
});
|
|
479
|
-
} catch (error) {
|
|
480
|
-
logger.error("启动 Bot 失败:", error);
|
|
481
|
-
throw error;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
|
-
* 检查并显示 Bot 所在的频道
|
|
486
|
-
* 验证配置文件中保存的频道,检查 Bot 是否仍在频道中以及是否是管理员
|
|
487
|
-
*/
|
|
488
|
-
async checkAndDisplayChannels(botId) {
|
|
489
|
-
const chatIds = Array.from(this.targetChatIds);
|
|
490
|
-
if (chatIds.length === 0) {
|
|
491
|
-
logger.info(`📋 当前没有保存的频道配置`);
|
|
492
|
-
logger.line(
|
|
493
|
-
"",
|
|
494
|
-
"提示: 当 Bot 被添加为频道管理员时,会自动添加到监听列表"
|
|
495
|
-
);
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
logger.info(`📋 检查 Bot 所在的频道:`);
|
|
499
|
-
const validChannels = [];
|
|
500
|
-
const invalidChannels = [];
|
|
501
|
-
for (const chatId of chatIds) {
|
|
502
|
-
try {
|
|
503
|
-
const chat = await this.bot.api.getChat(chatId);
|
|
504
|
-
const chatTitle = chat.title || "未知频道";
|
|
505
|
-
try {
|
|
506
|
-
const member = await this.bot.api.getChatMember(chatId, botId);
|
|
507
|
-
const isAdmin = member.status === "administrator" || member.status === "creator";
|
|
508
|
-
validChannels.push({
|
|
509
|
-
id: chatId,
|
|
510
|
-
title: chatTitle,
|
|
511
|
-
isAdmin
|
|
512
|
-
});
|
|
513
|
-
} catch (error) {
|
|
514
|
-
invalidChannels.push({
|
|
515
|
-
id: chatId,
|
|
516
|
-
reason: "无法获取成员信息(可能已被移除)"
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
} catch (error) {
|
|
520
|
-
invalidChannels.push({
|
|
521
|
-
id: chatId,
|
|
522
|
-
reason: "无法获取频道信息(可能已被移除)"
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
if (validChannels.length > 0) {
|
|
527
|
-
const adminChannels = validChannels.filter((c) => c.isAdmin);
|
|
528
|
-
const nonAdminChannels = validChannels.filter((c) => !c.isAdmin);
|
|
529
|
-
if (adminChannels.length > 0) {
|
|
530
|
-
logger.info(`📡 可监听频道 (${adminChannels.length} 个):`);
|
|
531
|
-
adminChannels.forEach((channel, index) => {
|
|
532
|
-
logger.item(
|
|
533
|
-
`${index + 1}. ${channel.title} (ID: ${channel.id}) 👑 管理员`,
|
|
534
|
-
1
|
|
535
|
-
);
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
if (nonAdminChannels.length > 0) {
|
|
539
|
-
logger.warning(`无法监听频道 (${nonAdminChannels.length} 个):`);
|
|
540
|
-
nonAdminChannels.forEach((channel, index) => {
|
|
541
|
-
logger.item(`${index + 1}. ${channel.title}`, 1);
|
|
542
|
-
logger.item(`ID: ${channel.id}`, 2);
|
|
543
|
-
logger.item(`状态: 🚫 非管理员(无法接收预测消息)`, 2);
|
|
544
|
-
logger.item(`原因: 需要管理员权限才能监听`, 2);
|
|
545
|
-
});
|
|
546
|
-
nonAdminChannels.forEach((channel) => {
|
|
547
|
-
this.targetChatIds.delete(channel.id);
|
|
548
|
-
});
|
|
549
|
-
if (nonAdminChannels.length > 0) {
|
|
550
|
-
await this.saveChatIds();
|
|
551
|
-
logger.line("💾", "已清理无法监听的频道并保存配置");
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
if (invalidChannels.length > 0) {
|
|
556
|
-
logger.error(`无效频道 (${invalidChannels.length} 个):`);
|
|
557
|
-
invalidChannels.forEach((channel, index) => {
|
|
558
|
-
logger.item(`${index + 1}. ID: ${channel.id}`, 1);
|
|
559
|
-
logger.item(`原因: ${channel.reason}`, 2);
|
|
560
|
-
});
|
|
561
|
-
invalidChannels.forEach((channel) => {
|
|
562
|
-
this.targetChatIds.delete(channel.id);
|
|
563
|
-
});
|
|
564
|
-
if (invalidChannels.length > 0) {
|
|
565
|
-
await this.saveChatIds();
|
|
566
|
-
logger.line("💾", "已清理无效频道并保存配置");
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
/**
|
|
571
|
-
* 停止监听
|
|
572
|
-
*/
|
|
573
|
-
async stop() {
|
|
574
|
-
await this.bot.stop();
|
|
575
|
-
}
|
|
576
|
-
/**
|
|
577
|
-
* 获取 bot 信息(用于验证 token 是否有效)
|
|
578
|
-
*/
|
|
579
|
-
async getBotInfo() {
|
|
580
|
-
try {
|
|
581
|
-
return this.bot.api.getMe();
|
|
582
|
-
} catch (error) {
|
|
583
|
-
throw new Error(`获取 Bot 信息失败: ${error}`);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
195
|
let globalRl = null;
|
|
588
196
|
function getReadlineInterface() {
|
|
589
197
|
if (!globalRl) {
|
|
@@ -602,9 +210,8 @@ function question(query) {
|
|
|
602
210
|
}
|
|
603
211
|
async function validateBotToken(botToken) {
|
|
604
212
|
try {
|
|
605
|
-
const
|
|
606
|
-
await
|
|
607
|
-
await listener.stop();
|
|
213
|
+
const bot = new grammy.Bot(botToken);
|
|
214
|
+
await bot.api.getMe();
|
|
608
215
|
return true;
|
|
609
216
|
} catch (error) {
|
|
610
217
|
logger.error(`Bot Token 验证失败: ${error}`);
|
|
@@ -966,7 +573,6 @@ async function ensureConfig() {
|
|
|
966
573
|
}
|
|
967
574
|
return config;
|
|
968
575
|
}
|
|
969
|
-
exports.TelegramListener = TelegramListener;
|
|
970
576
|
exports.canAutoTrade = canAutoTrade;
|
|
971
577
|
exports.configLocal = configLocal;
|
|
972
578
|
exports.ensureConfig = ensureConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polycopy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "polycopy test",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"cli": "node dist/cli.js",
|
|
16
16
|
"preuninstall": "node dist/cli.js stop 2>/dev/null || true",
|
|
17
17
|
"prepublishOnly": "npm run build",
|
|
18
|
-
"test": "
|
|
18
|
+
"test": "HOME=$PWD node --import tsx tests/redeemer.test.ts",
|
|
19
|
+
"test:redeemer": "HOME=$PWD node --import tsx tests/redeemer.test.ts"
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
21
22
|
"@polymarket/builder-relayer-client": "^0.0.8",
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"@types/lodash": "^4.17.23",
|
|
34
35
|
"@types/node": "^20.0.0",
|
|
35
36
|
"prettier": "^3.7.4",
|
|
37
|
+
"tsx": "^4.19.2",
|
|
36
38
|
"typescript": "^5.9.3",
|
|
37
39
|
"vite": "^7.3.1",
|
|
38
40
|
"vite-tsconfig-paths": "^6.0.4"
|