polycopy 0.1.1 → 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 +470 -44
- package/dist/{init-C5cwUXyD.js → init-C9dmq3BS.js} +2 -396
- package/package.json +1 -1
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;
|
|
@@ -658,7 +1086,10 @@ class Redeemer {
|
|
|
658
1086
|
if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
|
|
659
1087
|
const resetSeconds = this.parseResetSeconds(errorMsg);
|
|
660
1088
|
init.logger.warning(`获取仓位限流,${resetSeconds} 秒后重试`);
|
|
661
|
-
|
|
1089
|
+
telegramService.warning(
|
|
1090
|
+
"API 限流",
|
|
1091
|
+
`获取仓位限流,${resetSeconds} 秒后重试`
|
|
1092
|
+
);
|
|
662
1093
|
this.pauseAndRestart((resetSeconds + 60) * 1e3);
|
|
663
1094
|
return;
|
|
664
1095
|
}
|
|
@@ -693,7 +1124,7 @@ class Redeemer {
|
|
|
693
1124
|
init.logger.error(
|
|
694
1125
|
`Redeem 失败: 执行成功但仍存在 ${record.matchedCount} 次 (conditionId: ${position.conditionId.slice(0, 10)}...)`
|
|
695
1126
|
);
|
|
696
|
-
|
|
1127
|
+
telegramService.error(
|
|
697
1128
|
"Redeem 失败",
|
|
698
1129
|
`执行成功但仓位仍存在 ${record.matchedCount} 次
|
|
699
1130
|
conditionId: ${position.conditionId}
|
|
@@ -710,7 +1141,7 @@ conditionId: ${position.conditionId}
|
|
|
710
1141
|
if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
|
|
711
1142
|
const resetSeconds = this.parseResetSeconds(errorMsg);
|
|
712
1143
|
init.logger.warning(`Redeem 限流,${resetSeconds} 秒后重试`);
|
|
713
|
-
|
|
1144
|
+
telegramService.warning("Redeem 限流", `${resetSeconds} 秒后重试`);
|
|
714
1145
|
this.pauseAndRestart((resetSeconds + 60) * 1e3);
|
|
715
1146
|
return;
|
|
716
1147
|
}
|
|
@@ -719,7 +1150,7 @@ conditionId: ${position.conditionId}
|
|
|
719
1150
|
`Redeem 异常 (第 ${record.failedCount} 次): ${errorMsg}`
|
|
720
1151
|
);
|
|
721
1152
|
if (record.failedCount >= MAX_MATCH_TIMES) {
|
|
722
|
-
|
|
1153
|
+
telegramService.error(
|
|
723
1154
|
"Redeem 异常",
|
|
724
1155
|
`重试 ${record.failedCount} 次仍失败
|
|
725
1156
|
conditionId: ${position.conditionId}
|
|
@@ -743,7 +1174,7 @@ conditionId: ${position.conditionId}
|
|
|
743
1174
|
if (successItems.length > 0) {
|
|
744
1175
|
const successTitle = `自动 redeem: 成功 ${successItems.length} 个`;
|
|
745
1176
|
const successLines = this.logPositions(successTitle, successItems);
|
|
746
|
-
|
|
1177
|
+
telegramService.success(successTitle, successLines.join("\n"));
|
|
747
1178
|
this.onRedeemSuccess?.();
|
|
748
1179
|
}
|
|
749
1180
|
init.logger.info(`redeem 状态: ${this.records.size} 个待确认`);
|
|
@@ -881,7 +1312,7 @@ class Trader {
|
|
|
881
1312
|
if (amountMode === "fixed" && balance < amountValue) {
|
|
882
1313
|
const msg = `余额不足: ${balance} USDC < ${amountValue} USDC`;
|
|
883
1314
|
init.logger.warning(msg);
|
|
884
|
-
|
|
1315
|
+
telegramService.warning(
|
|
885
1316
|
"余额不足",
|
|
886
1317
|
`当前余额: ${balance} USDC
|
|
887
1318
|
需要金额: ${amountValue} USDC`
|
|
@@ -890,7 +1321,7 @@ class Trader {
|
|
|
890
1321
|
}
|
|
891
1322
|
if (amountMode === "percentage" && balance <= 0) {
|
|
892
1323
|
init.logger.warning(`余额为零: ${balance} USDC`);
|
|
893
|
-
|
|
1324
|
+
telegramService.warning("余额为零", `当前余额: ${balance} USDC`);
|
|
894
1325
|
return;
|
|
895
1326
|
}
|
|
896
1327
|
const orderAmount = amountMode === "fixed" ? amountValue : balance * amountValue;
|
|
@@ -901,7 +1332,7 @@ class Trader {
|
|
|
901
1332
|
const buyResult = await this.client.createBuyOrder(assetId, buyPrice, size);
|
|
902
1333
|
if (!buyResult.success) {
|
|
903
1334
|
init.logger.error(`买单失败: ${buyResult.errorMsg}`);
|
|
904
|
-
|
|
1335
|
+
telegramService.error(
|
|
905
1336
|
"买单失败",
|
|
906
1337
|
`事件: ${metadata.eventId}
|
|
907
1338
|
错误: ${buyResult.errorMsg}`
|
|
@@ -941,7 +1372,7 @@ class Trader {
|
|
|
941
1372
|
} catch (error) {
|
|
942
1373
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
943
1374
|
init.logger.error(`订单监控异常: ${errorMsg}`);
|
|
944
|
-
|
|
1375
|
+
telegramService.error(
|
|
945
1376
|
"订单监控异常",
|
|
946
1377
|
`事件: ${eventId}
|
|
947
1378
|
订单: ${orderId}
|
|
@@ -969,11 +1400,11 @@ class Trader {
|
|
|
969
1400
|
break;
|
|
970
1401
|
case "FAILED":
|
|
971
1402
|
init.logger.error("订单执行失败");
|
|
972
|
-
|
|
1403
|
+
telegramService.error("订单执行失败", `订单 ID: ${orderId}`);
|
|
973
1404
|
break;
|
|
974
1405
|
case "TIMEOUT":
|
|
975
1406
|
init.logger.warning("订单超时,尝试取消");
|
|
976
|
-
|
|
1407
|
+
telegramService.warning("订单超时", `订单 ID: ${orderId}
|
|
977
1408
|
已尝试取消`);
|
|
978
1409
|
await this.client.cancelOrder(orderId);
|
|
979
1410
|
break;
|
|
@@ -1016,7 +1447,7 @@ class Trader {
|
|
|
1016
1447
|
if (!this.relayClient.isInitialized()) {
|
|
1017
1448
|
const msg = "仓位同步超时且 Redeem 不可用";
|
|
1018
1449
|
init.logger.warning(msg);
|
|
1019
|
-
|
|
1450
|
+
telegramService.warning(
|
|
1020
1451
|
msg,
|
|
1021
1452
|
`Asset: ${assetId.substring(0, 20)}...
|
|
1022
1453
|
预期数量: ${expectedSize.toFixed(4)}
|
|
@@ -1040,7 +1471,7 @@ class Trader {
|
|
|
1040
1471
|
init.logger.success(`卖单已提交: ${sellResult.orderId}`);
|
|
1041
1472
|
} else {
|
|
1042
1473
|
init.logger.error(`卖单提交失败: ${sellResult.errorMsg}`);
|
|
1043
|
-
|
|
1474
|
+
telegramService.error("卖单提交失败", `错误: ${sellResult.errorMsg}`);
|
|
1044
1475
|
}
|
|
1045
1476
|
}
|
|
1046
1477
|
/**
|
|
@@ -1092,7 +1523,11 @@ async function main() {
|
|
|
1092
1523
|
init.logger.init();
|
|
1093
1524
|
init.logger.section("PolyMarket 跟单机器人");
|
|
1094
1525
|
const config = await init.ensureConfig();
|
|
1095
|
-
|
|
1526
|
+
telegramService.init(
|
|
1527
|
+
config.telegram.botToken,
|
|
1528
|
+
config.telegram.adminChatId,
|
|
1529
|
+
config.telegram.targetChatIds || []
|
|
1530
|
+
);
|
|
1096
1531
|
const autoTradeEnabled = init.canAutoTrade(config);
|
|
1097
1532
|
if (!autoTradeEnabled) {
|
|
1098
1533
|
init.logger.warning("自动跟单功能未启用");
|
|
@@ -1122,11 +1557,7 @@ async function main() {
|
|
|
1122
1557
|
init.logger.line("", "当前不会跟单任何预测消息");
|
|
1123
1558
|
}
|
|
1124
1559
|
}
|
|
1125
|
-
|
|
1126
|
-
config.telegram.botToken,
|
|
1127
|
-
config.telegram.targetChatIds || []
|
|
1128
|
-
);
|
|
1129
|
-
listener.onChatIdsChanged((chatIds) => {
|
|
1560
|
+
telegramService.onChatIdsChanged((chatIds) => {
|
|
1130
1561
|
const telegram = init.configLocal.getItem("telegram");
|
|
1131
1562
|
if (!telegram) {
|
|
1132
1563
|
init.logger.error("警告: 无法保存频道 ID,telegram 配置不存在");
|
|
@@ -1136,7 +1567,7 @@ async function main() {
|
|
|
1136
1567
|
init.configLocal.setItem("telegram", telegram);
|
|
1137
1568
|
init.logger.line("💾", `已保存频道 ID 列表: ${chatIds.length} 个频道`);
|
|
1138
1569
|
});
|
|
1139
|
-
|
|
1570
|
+
telegramService.onSignal(async (signal) => {
|
|
1140
1571
|
init.logger.info(`📨 收到预测信号:`);
|
|
1141
1572
|
init.logger.line("", `事件: ${signal.metadata.eventId}`);
|
|
1142
1573
|
init.logger.line(
|
|
@@ -1161,29 +1592,24 @@ async function main() {
|
|
|
1161
1592
|
enterDaemonMode();
|
|
1162
1593
|
return;
|
|
1163
1594
|
}
|
|
1164
|
-
const runMode = process.env.POLYCOPY_DAEMON_CHILD === "1" ? "后台模式" : "前台模式";
|
|
1165
|
-
notifier.success("程序已启动", `模式: ${runMode}
|
|
1166
|
-
PID: ${process.pid}`);
|
|
1167
1595
|
let isShuttingDown = false;
|
|
1168
|
-
const shutdown = () => {
|
|
1596
|
+
const shutdown = async () => {
|
|
1169
1597
|
if (isShuttingDown) return;
|
|
1170
1598
|
isShuttingDown = true;
|
|
1171
1599
|
process.removeAllListeners("SIGINT");
|
|
1172
1600
|
process.removeAllListeners("SIGTERM");
|
|
1173
1601
|
init.logger.blank();
|
|
1174
1602
|
init.logger.info("正在退出...");
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
setTimeout(() => {
|
|
1178
|
-
process.exit(0);
|
|
1179
|
-
}, 1e3);
|
|
1180
|
-
Promise.all([trader.shutdown(), listener.stop()]).finally(() => {
|
|
1181
|
-
process.exit(0);
|
|
1182
|
-
});
|
|
1603
|
+
await Promise.all([trader.shutdown(), telegramService.stop()]);
|
|
1604
|
+
process.exit(0);
|
|
1183
1605
|
};
|
|
1184
|
-
process.
|
|
1185
|
-
|
|
1186
|
-
|
|
1606
|
+
process.prependListener("SIGINT", () => {
|
|
1607
|
+
shutdown();
|
|
1608
|
+
});
|
|
1609
|
+
process.prependListener("SIGTERM", () => {
|
|
1610
|
+
shutdown();
|
|
1611
|
+
});
|
|
1612
|
+
await telegramService.start(() => {
|
|
1187
1613
|
if (trader.isInitialized()) {
|
|
1188
1614
|
trader.startRedeemer();
|
|
1189
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.info(`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;
|