polycopy 0.1.7 → 0.1.9

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/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
- const init = require("./init-Bs-N_AZi.js");
2
+ const processLock = require("./process-lock-y_yjnMCr.js");
3
3
  const grammy = require("grammy");
4
+ const log4js = require("log4js");
5
+ const path = require("path");
4
6
  const wallet = require("@ethersproject/wallet");
5
7
  const clobClient = require("@polymarket/clob-client");
6
8
  const builderSigningSdk = require("@polymarket/builder-signing-sdk");
@@ -11,7 +13,6 @@ const builderRelayerClient = require("@polymarket/builder-relayer-client");
11
13
  const setPromiseInterval = require("set-promise-interval");
12
14
  const child_process = require("child_process");
13
15
  const fs = require("fs");
14
- const paths = require("./paths-CEjGES8j.js");
15
16
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
16
17
  function _interopNamespace(e) {
17
18
  if (e && e.__esModule) return e;
@@ -30,8 +31,114 @@ function _interopNamespace(e) {
30
31
  n.default = e;
31
32
  return Object.freeze(n);
32
33
  }
34
+ const log4js__default = /* @__PURE__ */ _interopDefault(log4js);
35
+ const path__namespace = /* @__PURE__ */ _interopNamespace(path);
33
36
  const setPromiseInterval__default = /* @__PURE__ */ _interopDefault(setPromiseInterval);
34
37
  const fs__namespace = /* @__PURE__ */ _interopNamespace(fs);
38
+ const LOG_FILE = path__namespace.join(processLock.LOGS_DIR, "polycopy.log");
39
+ path__namespace.join(processLock.LOGS_DIR, "polycopy");
40
+ log4js__default.default.configure({
41
+ appenders: {
42
+ file: {
43
+ type: "dateFile",
44
+ filename: LOG_FILE,
45
+ pattern: "yyyy-MM-dd",
46
+ alwaysIncludePattern: true,
47
+ compress: false,
48
+ keepFileExt: true,
49
+ maxLogSize: 2 * 1024 * 1024,
50
+ backups: 10,
51
+ layout: { type: "pattern", pattern: "[%d{yyyy-MM-dd hh:mm:ss}] %m" }
52
+ },
53
+ stdout: {
54
+ type: "stdout",
55
+ layout: { type: "pattern", pattern: "[%d{yyyy-MM-dd hh:mm:ss}] %m" }
56
+ }
57
+ },
58
+ categories: {
59
+ default: { appenders: ["stdout", "file"], level: "info" }
60
+ }
61
+ });
62
+ const log4jsLogger = log4js__default.default.getLogger("polycopy");
63
+ const formatMessage = (message, args) => args.length > 0 ? `${message} ${args.join(" ")}` : message;
64
+ class Logger {
65
+ initialized = false;
66
+ /**
67
+ * 初始化日志模块(输出日志路径信息)
68
+ */
69
+ init() {
70
+ if (this.initialized) return;
71
+ this.initialized = true;
72
+ console.log(`
73
+ 📁 日志目录: ${processLock.LOGS_DIR}`);
74
+ }
75
+ /**
76
+ * 输出信息日志(前后自动添加空行)
77
+ */
78
+ info(message, ...args) {
79
+ log4jsLogger.info(formatMessage(message, args));
80
+ }
81
+ /**
82
+ * 输出成功日志(前后自动添加空行)
83
+ */
84
+ success(message, ...args) {
85
+ log4jsLogger.info(`✅ ${formatMessage(message, args)}`);
86
+ }
87
+ /**
88
+ * 输出警告日志(前后自动添加空行)
89
+ */
90
+ warning(message, ...args) {
91
+ log4jsLogger.warn(`⚠️ ${formatMessage(message, args)}`);
92
+ }
93
+ /**
94
+ * 输出错误日志(前后自动添加空行)
95
+ */
96
+ error(message, ...args) {
97
+ log4jsLogger.error(`❌ ${formatMessage(message, args)}`);
98
+ }
99
+ /**
100
+ * 输出章节标题(前后自动添加空行)
101
+ */
102
+ section(title) {
103
+ log4jsLogger.info(`=== ${title} ===`);
104
+ }
105
+ /**
106
+ * 输出列表项(不添加空行,用于连续输出)
107
+ */
108
+ item(message, indent = 0) {
109
+ const indentStr = " ".repeat(indent);
110
+ const formatted = `${indentStr}${message}`;
111
+ log4jsLogger.info(formatted);
112
+ }
113
+ /**
114
+ * 输出普通文本(不添加空行,用于连续输出)
115
+ */
116
+ text(message, ...args) {
117
+ log4jsLogger.info(formatMessage(message, args));
118
+ }
119
+ /**
120
+ * 输出空行
121
+ */
122
+ blank() {
123
+ console.log("");
124
+ }
125
+ /**
126
+ * 输出带前缀的信息(不添加空行)
127
+ * 用于在同一组日志中输出多行信息
128
+ */
129
+ line(prefix, message, ...args) {
130
+ const formatted = args.length > 0 ? ` ${prefix} ${message} ${args.join(" ")}` : ` ${prefix} ${message}`;
131
+ log4jsLogger.info(formatted);
132
+ }
133
+ /**
134
+ * 刷新日志输出(用于进程退出前)
135
+ */
136
+ async flush(timeoutMs = 1e3) {
137
+ await log4js__default.default.shutdown();
138
+ await new Promise((resolve) => setTimeout(resolve, timeoutMs));
139
+ }
140
+ }
141
+ const logger = new Logger();
35
142
  function parseSignalMetadata(messageText) {
36
143
  const match = messageText.match(/🤖\s*({.*})/s);
37
144
  if (!match) {
@@ -41,7 +148,7 @@ function parseSignalMetadata(messageText) {
41
148
  const jsonStr = match[1];
42
149
  const metadata = JSON.parse(jsonStr);
43
150
  if (!metadata.eventId || !metadata.assetId || !metadata.outcome || !metadata.endTime) {
44
- init.logger.error("解析失败: 缺少必需字段", {
151
+ logger.error("解析失败: 缺少必需字段", {
45
152
  hasEventId: !!metadata.eventId,
46
153
  hasAssetId: !!metadata.assetId,
47
154
  hasOutcome: !!metadata.outcome,
@@ -50,14 +157,14 @@ function parseSignalMetadata(messageText) {
50
157
  return null;
51
158
  }
52
159
  if (metadata.outcome !== "Up" && metadata.outcome !== "Down") {
53
- init.logger.error("解析失败: outcome 值无效", {
160
+ logger.error("解析失败: outcome 值无效", {
54
161
  outcome: metadata.outcome,
55
162
  expected: "Up 或 Down"
56
163
  });
57
164
  return null;
58
165
  }
59
166
  if (typeof metadata.endTime !== "number" || metadata.endTime <= 0) {
60
- init.logger.error("解析失败: endTime 值无效", {
167
+ logger.error("解析失败: endTime 值无效", {
61
168
  endTime: metadata.endTime,
62
169
  expected: "毫秒级时间戳"
63
170
  });
@@ -65,9 +172,9 @@ function parseSignalMetadata(messageText) {
65
172
  }
66
173
  return metadata;
67
174
  } catch (error) {
68
- init.logger.error("解析信号元数据失败:", error);
175
+ logger.error("解析信号元数据失败:", error);
69
176
  if (error instanceof Error) {
70
- init.logger.line("", `错误详情: ${error.message}`);
177
+ logger.line("", `错误详情: ${error.message}`);
71
178
  }
72
179
  return null;
73
180
  }
@@ -97,12 +204,12 @@ class TelegramListener {
97
204
  await this.handleMyChatMemberUpdate(ctx);
98
205
  });
99
206
  this.bot.catch((err) => {
100
- init.logger.error("Telegram Bot 错误:", err);
207
+ logger.error("Telegram Bot 错误:", err);
101
208
  if (err.error) {
102
- init.logger.line("", `错误详情: ${err.error}`);
209
+ logger.line("", `错误详情: ${err.error}`);
103
210
  }
104
211
  if (err.ctx) {
105
- init.logger.line("", `上下文信息:`, {
212
+ logger.line("", `上下文信息:`, {
106
213
  chatId: err.ctx.chat?.id,
107
214
  messageId: err.ctx.message?.message_id
108
215
  });
@@ -169,28 +276,28 @@ class TelegramListener {
169
276
  reason = "需要管理员权限才能监听";
170
277
  }
171
278
  }
172
- init.logger.info(`📢 频道状态更新: ${chatTitle}`);
173
- init.logger.line("", action);
174
- init.logger.line(
279
+ logger.info(`📢 频道状态更新: ${chatTitle}`);
280
+ logger.line("", action);
281
+ logger.line(
175
282
  "",
176
283
  `当前状态: ${canListen ? "✅ 可监听预测消息" : "❌ 无法监听预测消息"}`
177
284
  );
178
285
  if (reason) {
179
- init.logger.line("", `原因: ${reason}`);
286
+ logger.line("", `原因: ${reason}`);
180
287
  }
181
288
  }
182
289
  if (isAdmin && !this.targetChatIds.has(chatId)) {
183
290
  this.targetChatIds.add(chatId);
184
291
  await this.saveChatIds();
185
- init.logger.line("📡", "已自动开始监听此频道");
292
+ logger.line("📡", "已自动开始监听此频道");
186
293
  }
187
294
  if ((isRemoved || !isAdmin) && this.targetChatIds.has(chatId)) {
188
295
  this.targetChatIds.delete(chatId);
189
296
  await this.saveChatIds();
190
297
  if (isRemoved) {
191
- init.logger.line("⏹️", "已停止监听此频道(Bot 已被移除)");
298
+ logger.line("⏹️", "已停止监听此频道(Bot 已被移除)");
192
299
  } else {
193
- init.logger.line("⏹️", "已停止监听此频道(Bot 不再是管理员)");
300
+ logger.line("⏹️", "已停止监听此频道(Bot 不再是管理员)");
194
301
  }
195
302
  }
196
303
  }
@@ -205,7 +312,7 @@ class TelegramListener {
205
312
  const chatIds = Array.from(this.targetChatIds);
206
313
  await this.onChatIdsChangedCallback(chatIds);
207
314
  } catch (error) {
208
- init.logger.error("保存频道 ID 列表时出错:", error);
315
+ logger.error("保存频道 ID 列表时出错:", error);
209
316
  }
210
317
  }
211
318
  /**
@@ -234,9 +341,9 @@ class TelegramListener {
234
341
  }
235
342
  if (isChannel && !this.targetChatIds.has(chatId)) {
236
343
  this.targetChatIds.add(chatId);
237
- init.logger.info(`➕ 自动添加频道到监听列表: ${chatTitle} (ID: ${chatId})`);
344
+ logger.info(`➕ 自动添加频道到监听列表: ${chatTitle} (ID: ${chatId})`);
238
345
  await this.saveChatIds();
239
- init.logger.line("💾", "已保存到配置文件");
346
+ logger.line("💾", "已保存到配置文件");
240
347
  }
241
348
  if (!isChannel) {
242
349
  return;
@@ -250,13 +357,13 @@ class TelegramListener {
250
357
  const emojiIndex = messageText.indexOf("🤖");
251
358
  const jsonStart = emojiIndex + 1;
252
359
  const jsonEnd = Math.min(jsonStart + 200, messageText.length);
253
- init.logger.warning(`解析预测消息失败:`);
254
- init.logger.line("", `频道 ID: ${chatId}`);
255
- init.logger.line(
360
+ logger.warning(`解析预测消息失败:`);
361
+ logger.line("", `频道 ID: ${chatId}`);
362
+ logger.line(
256
363
  "",
257
364
  `消息片段: ${messageText.substring(emojiIndex, jsonEnd)}${messageText.length > jsonEnd ? "..." : ""}`
258
365
  );
259
- init.logger.line("", "提示: 请检查消息格式是否正确");
366
+ logger.line("", "提示: 请检查消息格式是否正确");
260
367
  }
261
368
  return;
262
369
  }
@@ -271,7 +378,7 @@ class TelegramListener {
271
378
  try {
272
379
  await this.onSignalCallback(parsedSignal);
273
380
  } catch (error) {
274
- init.logger.error("处理信号时出错:", error);
381
+ logger.error("处理信号时出错:", error);
275
382
  }
276
383
  }
277
384
  }
@@ -299,15 +406,15 @@ class TelegramListener {
299
406
  */
300
407
  async start(onReady) {
301
408
  try {
302
- init.logger.info("正在启动 Bot...");
409
+ logger.info("正在启动 Bot...");
303
410
  const botInfo = await this.bot.api.getMe();
304
411
  const botId = botInfo.id;
305
- init.logger.info(`Bot 信息: @${botInfo.username} (ID: ${botId})`);
412
+ logger.info(`Bot 信息: @${botInfo.username} (ID: ${botId})`);
306
413
  await this.checkAndDisplayChannels(botId);
307
414
  if (onReady) {
308
415
  await onReady();
309
416
  }
310
- init.logger.info("开始监听预测消息...");
417
+ logger.info("开始监听预测消息...");
311
418
  await this.bot.start({
312
419
  drop_pending_updates: true,
313
420
  allowed_updates: [
@@ -317,7 +424,7 @@ class TelegramListener {
317
424
  ]
318
425
  });
319
426
  } catch (error) {
320
- init.logger.error("启动 Bot 失败:", error);
427
+ logger.error("启动 Bot 失败:", error);
321
428
  throw error;
322
429
  }
323
430
  }
@@ -328,14 +435,14 @@ class TelegramListener {
328
435
  async checkAndDisplayChannels(botId) {
329
436
  const chatIds = Array.from(this.targetChatIds);
330
437
  if (chatIds.length === 0) {
331
- init.logger.info(`📋 当前没有保存的频道配置`);
332
- init.logger.line(
438
+ logger.info(`📋 当前没有保存的频道配置`);
439
+ logger.line(
333
440
  "",
334
441
  "提示: 当 Bot 被添加为频道管理员时,会自动添加到监听列表"
335
442
  );
336
443
  return;
337
444
  }
338
- init.logger.info(`📋 检查 Bot 所在的频道:`);
445
+ logger.info(`📋 检查 Bot 所在的频道:`);
339
446
  const validChannels = [];
340
447
  const invalidChannels = [];
341
448
  for (const chatId of chatIds) {
@@ -367,43 +474,43 @@ class TelegramListener {
367
474
  const adminChannels = validChannels.filter((c) => c.isAdmin);
368
475
  const nonAdminChannels = validChannels.filter((c) => !c.isAdmin);
369
476
  if (adminChannels.length > 0) {
370
- init.logger.info(`📡 可监听频道 (${adminChannels.length} 个):`);
477
+ logger.info(`📡 可监听频道 (${adminChannels.length} 个):`);
371
478
  adminChannels.forEach((channel, index) => {
372
- init.logger.item(
479
+ logger.item(
373
480
  `${index + 1}. ${channel.title} (ID: ${channel.id}) 👑 管理员`,
374
481
  1
375
482
  );
376
483
  });
377
484
  }
378
485
  if (nonAdminChannels.length > 0) {
379
- init.logger.warning(`无法监听频道 (${nonAdminChannels.length} 个):`);
486
+ logger.warning(`无法监听频道 (${nonAdminChannels.length} 个):`);
380
487
  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);
488
+ logger.item(`${index + 1}. ${channel.title}`, 1);
489
+ logger.item(`ID: ${channel.id}`, 2);
490
+ logger.item(`状态: 🚫 非管理员(无法接收预测消息)`, 2);
491
+ logger.item(`原因: 需要管理员权限才能监听`, 2);
385
492
  });
386
493
  nonAdminChannels.forEach((channel) => {
387
494
  this.targetChatIds.delete(channel.id);
388
495
  });
389
496
  if (nonAdminChannels.length > 0) {
390
497
  await this.saveChatIds();
391
- init.logger.line("💾", "已清理无法监听的频道并保存配置");
498
+ logger.line("💾", "已清理无法监听的频道并保存配置");
392
499
  }
393
500
  }
394
501
  }
395
502
  if (invalidChannels.length > 0) {
396
- init.logger.error(`无效频道 (${invalidChannels.length} 个):`);
503
+ logger.error(`无效频道 (${invalidChannels.length} 个):`);
397
504
  invalidChannels.forEach((channel, index) => {
398
- init.logger.item(`${index + 1}. ID: ${channel.id}`, 1);
399
- init.logger.item(`原因: ${channel.reason}`, 2);
505
+ logger.item(`${index + 1}. ID: ${channel.id}`, 1);
506
+ logger.item(`原因: ${channel.reason}`, 2);
400
507
  });
401
508
  invalidChannels.forEach((channel) => {
402
509
  this.targetChatIds.delete(channel.id);
403
510
  });
404
511
  if (invalidChannels.length > 0) {
405
512
  await this.saveChatIds();
406
- init.logger.line("💾", "已清理无效频道并保存配置");
513
+ logger.line("💾", "已清理无效频道并保存配置");
407
514
  }
408
515
  }
409
516
  }
@@ -439,13 +546,13 @@ class TelegramService {
439
546
  this.bot = new grammy.Bot(botToken);
440
547
  this.listener = new TelegramListener(this.bot, targetChatIds);
441
548
  if (!adminChatId) {
442
- init.logger.warning("未配置管理员 Chat ID,通知功能已禁用");
549
+ logger.warning("未配置管理员 Chat ID,通知功能已禁用");
443
550
  this.notifyEnabled = false;
444
551
  return;
445
552
  }
446
553
  this.adminChatId = adminChatId;
447
554
  this.notifyEnabled = true;
448
- init.logger.info("管理员通知已启用");
555
+ logger.info("管理员通知已启用");
449
556
  }
450
557
  /**
451
558
  * 设置频道 ID 列表变更回调
@@ -511,7 +618,7 @@ ${details}` : message;
511
618
  });
512
619
  } catch (error) {
513
620
  const errorMsg = error instanceof Error ? error.message : String(error);
514
- init.logger.error(`发送通知失败: ${errorMsg}`);
621
+ logger.error(`发送通知失败: ${errorMsg}`);
515
622
  }
516
623
  }
517
624
  }
@@ -528,11 +635,11 @@ class ClobClientWrapper {
528
635
  return;
529
636
  }
530
637
  const wallet$1 = new wallet.Wallet(config.privateKey);
531
- init.logger.info(`钱包地址: ${wallet$1.address}`);
638
+ logger.info(`钱包地址: ${wallet$1.address}`);
532
639
  const tempClient = new clobClient.ClobClient(CLOB_HOST, clobClient.Chain.POLYGON, wallet$1);
533
640
  this.patchClient(tempClient);
534
641
  const creds = await tempClient.deriveApiKey();
535
- init.logger.success("API 凭证已获取");
642
+ logger.success("API 凭证已获取");
536
643
  let builderConfig;
537
644
  if (config.builderCreds) {
538
645
  builderConfig = new builderSigningSdk.BuilderConfig({
@@ -556,7 +663,7 @@ class ClobClientWrapper {
556
663
  );
557
664
  this.patchClient(this.client);
558
665
  this.initialized = true;
559
- init.logger.success("ClobClient 初始化完成");
666
+ logger.success("ClobClient 初始化完成");
560
667
  }
561
668
  /**
562
669
  * 魔法操作:clob-client 请求出错不抛异常,只返回 { error: ... }
@@ -632,13 +739,13 @@ class ClobClientWrapper {
632
739
  clobClient.OrderType.GTC
633
740
  );
634
741
  if (response.success) {
635
- init.logger.success(`买单已提交: ${response.orderID}`);
742
+ logger.success(`买单已提交: ${response.orderID}`);
636
743
  return {
637
744
  success: true,
638
745
  orderId: response.orderID
639
746
  };
640
747
  } else {
641
- init.logger.error(`买单提交失败: ${response.errorMsg}`);
748
+ logger.error(`买单提交失败: ${response.errorMsg}`);
642
749
  return {
643
750
  success: false,
644
751
  orderId: "",
@@ -647,7 +754,7 @@ class ClobClientWrapper {
647
754
  }
648
755
  } catch (error) {
649
756
  const errorMsg = error instanceof Error ? error.message : String(error);
650
- init.logger.error(`买单提交失败: ${errorMsg}`);
757
+ logger.error(`买单提交失败: ${errorMsg}`);
651
758
  return {
652
759
  success: false,
653
760
  orderId: "",
@@ -709,11 +816,11 @@ class ClobClientWrapper {
709
816
  const client = this.ensureInitialized();
710
817
  try {
711
818
  await client.cancelOrder({ orderID: orderId });
712
- init.logger.info(`订单已取消: ${orderId}`);
819
+ logger.info(`订单已取消: ${orderId}`);
713
820
  return true;
714
821
  } catch (error) {
715
822
  const errorMsg = error instanceof Error ? error.message : String(error);
716
- init.logger.error(`取消订单失败: ${errorMsg}`);
823
+ logger.error(`取消订单失败: ${errorMsg}`);
717
824
  return false;
718
825
  }
719
826
  }
@@ -759,7 +866,7 @@ class RelayClientWrapper {
759
866
  builderRelayerClient.RelayerTxType.SAFE
760
867
  );
761
868
  this.initialized = true;
762
- init.logger.success("RelayClient 初始化完成");
869
+ logger.success("RelayClient 初始化完成");
763
870
  }
764
871
  /**
765
872
  * 检查是否已初始化
@@ -767,15 +874,7 @@ class RelayClientWrapper {
767
874
  isInitialized() {
768
875
  return this.initialized;
769
876
  }
770
- /**
771
- * 执行 Redeem
772
- * @param conditionId 条件 ID
773
- * @returns 交易哈希
774
- */
775
- async redeem(conditionId) {
776
- if (!this.relayClient || !this.initialized) {
777
- throw new Error("RelayClient 未初始化");
778
- }
877
+ buildRedeemTransaction(conditionId) {
779
878
  const calldata = viem.encodeFunctionData({
780
879
  abi: [
781
880
  {
@@ -794,14 +893,27 @@ class RelayClientWrapper {
794
893
  functionName: "redeemPositions",
795
894
  args: [USDC_ADDRESS, viem.zeroHash, conditionId, [1n, 2n]]
796
895
  });
797
- const tx = {
896
+ return {
798
897
  to: CTF_ADDRESS,
799
898
  value: "0",
800
899
  data: calldata
801
900
  };
901
+ }
902
+ /**
903
+ * 批量执行 Redeem
904
+ * @param conditionIds 条件 ID 列表
905
+ * @returns 交易哈希
906
+ */
907
+ async redeemBatch(conditionIds) {
908
+ if (!this.relayClient || !this.initialized) {
909
+ throw new Error("RelayClient 未初始化");
910
+ }
911
+ const transactions = conditionIds.map(
912
+ (conditionId) => this.buildRedeemTransaction(conditionId)
913
+ );
802
914
  const response = await this.relayClient.execute(
803
- [tx],
804
- "Polymarket redeem positions"
915
+ transactions,
916
+ `Polymarket redeem positions (${transactions.length})`
805
917
  );
806
918
  const result = await response.wait();
807
919
  if (!result) {
@@ -811,6 +923,14 @@ class RelayClientWrapper {
811
923
  }
812
924
  return result.transactionHash;
813
925
  }
926
+ /**
927
+ * 执行 Redeem(兼容单个)
928
+ * @param conditionId 条件 ID
929
+ * @returns 交易哈希
930
+ */
931
+ async redeem(conditionId) {
932
+ return this.redeemBatch([conditionId]);
933
+ }
814
934
  }
815
935
  class AssetFilter {
816
936
  // endTime -> Set<assetId>
@@ -838,7 +958,7 @@ class AssetFilter {
838
958
  this.traded.set(endTime, /* @__PURE__ */ new Set());
839
959
  }
840
960
  this.traded.get(endTime).add(assetId);
841
- init.logger.info(`已标记 assetId: ${assetId.slice(0, 10)}...`);
961
+ logger.info(`已标记 assetId: ${assetId.slice(0, 10)}...`);
842
962
  }
843
963
  /**
844
964
  * 清理过期记录
@@ -852,7 +972,7 @@ class AssetFilter {
852
972
  }
853
973
  }
854
974
  if (cleanedCount > 0) {
855
- init.logger.info(`已清理 ${cleanedCount} 个过期周期的交易记录`);
975
+ logger.info(`已清理 ${cleanedCount} 个过期周期的交易记录`);
856
976
  }
857
977
  }
858
978
  }
@@ -872,12 +992,12 @@ class BalanceCache {
872
992
  }
873
993
  if (this.cache.has(endTime)) {
874
994
  const cached = this.cache.get(endTime);
875
- init.logger.info(`使用缓存余额: ${cached} USDC`);
995
+ logger.info(`使用缓存余额: ${cached} USDC`);
876
996
  return cached;
877
997
  }
878
998
  const balance = await fetchFn();
879
999
  this.cache.set(endTime, balance);
880
- init.logger.info(`获取当前余额: ${balance} USDC`);
1000
+ logger.info(`获取当前余额: ${balance} USDC`);
881
1001
  return balance;
882
1002
  }
883
1003
  /**
@@ -886,7 +1006,7 @@ class BalanceCache {
886
1006
  invalidate() {
887
1007
  if (this.cache.size > 0) {
888
1008
  this.cache.clear();
889
- init.logger.info("余额缓存已清除");
1009
+ logger.info("余额缓存已清除");
890
1010
  }
891
1011
  }
892
1012
  /**
@@ -901,7 +1021,7 @@ class BalanceCache {
901
1021
  }
902
1022
  }
903
1023
  if (cleanedCount > 0) {
904
- init.logger.info(`已清理 ${cleanedCount} 个过期周期的余额缓存`);
1024
+ logger.info(`已清理 ${cleanedCount} 个过期周期的余额缓存`);
905
1025
  }
906
1026
  }
907
1027
  }
@@ -918,13 +1038,13 @@ class OrderWatcher {
918
1038
  async watch(orderId, endTime, getOrderFn) {
919
1039
  const timeoutAt = endTime + TIMEOUT_BUFFER;
920
1040
  const shortId = orderId.substring(0, 10) + "...";
921
- init.logger.info(
1041
+ logger.info(
922
1042
  `开始监控订单 ${shortId},超时: ${new Date(timeoutAt).toLocaleString()}`
923
1043
  );
924
1044
  while (true) {
925
1045
  const now = Date.now();
926
1046
  if (now >= timeoutAt) {
927
- init.logger.warning(`订单超时: ${shortId}`);
1047
+ logger.warning(`订单超时: ${shortId}`);
928
1048
  return { status: "TIMEOUT" };
929
1049
  }
930
1050
  try {
@@ -932,7 +1052,7 @@ class OrderWatcher {
932
1052
  const status = order.status.toUpperCase();
933
1053
  if (TERMINAL_STATUSES.includes(status)) {
934
1054
  const filledSize = parseFloat(order.size_matched || "0");
935
- init.logger.info(`订单 ${shortId} 终态: ${status},成交: ${filledSize}`);
1055
+ logger.info(`订单 ${shortId} 终态: ${status},成交: ${filledSize}`);
936
1056
  return {
937
1057
  status,
938
1058
  filledSize,
@@ -941,7 +1061,7 @@ class OrderWatcher {
941
1061
  }
942
1062
  } catch (error) {
943
1063
  const errorMsg = error instanceof Error ? error.message : String(error);
944
- init.logger.warning(`查询订单失败: ${errorMsg},继续重试...`);
1064
+ logger.warning(`查询订单失败: ${errorMsg},继续重试...`);
945
1065
  }
946
1066
  await this.delay(POLL_INTERVAL);
947
1067
  }
@@ -952,6 +1072,7 @@ class OrderWatcher {
952
1072
  }
953
1073
  const DATA_API_HOST = "https://data-api.polymarket.com";
954
1074
  const BASE_INTERVAL = 5 * 60 * 1e3;
1075
+ const DEFAULT_BATCH_SIZE = 5;
955
1076
  const MAX_MATCH_TIMES = 3;
956
1077
  class Redeemer {
957
1078
  // conditionId -> RedeemRecord
@@ -962,6 +1083,7 @@ class Redeemer {
962
1083
  state = "stopped";
963
1084
  scheduler;
964
1085
  redeemFn = null;
1086
+ batchSize = DEFAULT_BATCH_SIZE;
965
1087
  funderAddress = "";
966
1088
  onRedeemSuccess = null;
967
1089
  constructor(scheduler = {
@@ -983,10 +1105,14 @@ class Redeemer {
983
1105
  return;
984
1106
  }
985
1107
  this.redeemFn = redeemFn;
1108
+ const envBatchSize = Number(process.env.POLYCOPY_REDEEM_BATCH_SIZE || "");
1109
+ if (!Number.isNaN(envBatchSize) && envBatchSize > 0) {
1110
+ this.batchSize = Math.floor(envBatchSize);
1111
+ }
986
1112
  this.funderAddress = funderAddress;
987
1113
  this.onRedeemSuccess = onRedeemSuccess || null;
988
1114
  this.state = "running";
989
- init.logger.success("自动 redeem 已启动");
1115
+ logger.success("自动 redeem 已启动");
990
1116
  this.startInterval();
991
1117
  }
992
1118
  /**
@@ -1034,7 +1160,7 @@ class Redeemer {
1034
1160
  this.delayTimeoutId = void 0;
1035
1161
  }
1036
1162
  this.state = "stopped";
1037
- init.logger.info("自动 redeem 已停止");
1163
+ logger.info("自动 redeem 已停止");
1038
1164
  }
1039
1165
  /**
1040
1166
  * 获取可 Redeem 的仓位
@@ -1085,7 +1211,7 @@ class Redeemer {
1085
1211
  const errorMsg = error instanceof Error ? error.message : String(error);
1086
1212
  if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
1087
1213
  const resetSeconds = this.parseResetSeconds(errorMsg);
1088
- init.logger.warning(`获取仓位限流,${resetSeconds} 秒后重试`);
1214
+ logger.warning(`获取仓位限流,${resetSeconds} 秒后重试`);
1089
1215
  telegramService.warning(
1090
1216
  "API 限流",
1091
1217
  `获取仓位限流,${resetSeconds} 秒后重试`
@@ -1093,9 +1219,27 @@ class Redeemer {
1093
1219
  this.pauseAndRestart((resetSeconds + 60) * 1e3);
1094
1220
  return;
1095
1221
  }
1096
- init.logger.error(`获取仓位失败: ${errorMsg}`);
1222
+ logger.error(`获取仓位失败: ${errorMsg}`);
1097
1223
  return;
1098
1224
  }
1225
+ const currentIds = new Set(positions.map((position) => position.conditionId));
1226
+ this.logPendingPositions(positions);
1227
+ const pendingRecords = this.buildRecords(positions);
1228
+ this.updateMatchedRecords(currentIds);
1229
+ const shouldContinue = await this.executeBatches(pendingRecords);
1230
+ if (!shouldContinue) {
1231
+ return;
1232
+ }
1233
+ const successItems = this.collectSuccessRecords(currentIds);
1234
+ if (successItems.length > 0) {
1235
+ const successTitle = `自动 redeem: 成功 ${successItems.length} 个`;
1236
+ const successLines = this.logPositions(successTitle, successItems);
1237
+ telegramService.success(successTitle, successLines.join("\n"));
1238
+ this.onRedeemSuccess?.();
1239
+ }
1240
+ logger.info(`redeem 状态: ${this.records.size} 个待确认`);
1241
+ }
1242
+ logPendingPositions(positions) {
1099
1243
  const pendingRedeemablePositions = positions.filter((position) => {
1100
1244
  const record = this.records.get(position.conditionId);
1101
1245
  return !record?.success;
@@ -1105,8 +1249,11 @@ class Redeemer {
1105
1249
  `自动 redeem: 发现 ${pendingRedeemablePositions.length} 个可 Redeem 仓位`,
1106
1250
  pendingRedeemablePositions.map((position) => ({ position }))
1107
1251
  );
1252
+ logger.info(`Redeem 批量大小: ${this.batchSize}`);
1108
1253
  }
1109
- for (const position of positions) {
1254
+ }
1255
+ buildRecords(positions) {
1256
+ return positions.map((position) => {
1110
1257
  let record = this.records.get(position.conditionId);
1111
1258
  if (!record) {
1112
1259
  record = {
@@ -1118,52 +1265,76 @@ class Redeemer {
1118
1265
  this.records.set(position.conditionId, record);
1119
1266
  }
1120
1267
  record.position = position;
1121
- if (record.success) {
1122
- record.matchedCount++;
1123
- if (record.matchedCount >= MAX_MATCH_TIMES) {
1124
- init.logger.error(
1125
- `Redeem 失败: 执行成功但仍存在 ${record.matchedCount} (conditionId: ${position.conditionId.slice(0, 10)}...)`
1126
- );
1127
- telegramService.error(
1128
- "Redeem 失败",
1129
- `执行成功但仓位仍存在 ${record.matchedCount}
1130
- conditionId: ${position.conditionId}
1131
- 价值: ${position.currentValue} USDC`
1132
- );
1133
- }
1134
- } else if (record.failedCount < MAX_MATCH_TIMES) {
1135
- try {
1136
- const txHash = await this.redeemFn?.(position.conditionId);
1268
+ return record;
1269
+ }).filter((record) => !record.success && record.failedCount < MAX_MATCH_TIMES);
1270
+ }
1271
+ updateMatchedRecords(currentIds) {
1272
+ for (const record of this.records.values()) {
1273
+ if (!record.success) {
1274
+ continue;
1275
+ }
1276
+ if (!currentIds.has(record.position.conditionId)) {
1277
+ continue;
1278
+ }
1279
+ record.matchedCount++;
1280
+ if (record.matchedCount >= MAX_MATCH_TIMES) {
1281
+ logger.error(
1282
+ `Redeem 失败: 执行成功但仍存在 ${record.matchedCount} 次 (conditionId: ${record.position.conditionId.slice(0, 10)}...)`
1283
+ );
1284
+ telegramService.error(
1285
+ "Redeem 失败",
1286
+ `执行成功但仓位仍存在 ${record.matchedCount} 次
1287
+ conditionId: ${record.position.conditionId}
1288
+ 价值: ${record.position.currentValue} USDC`
1289
+ );
1290
+ }
1291
+ }
1292
+ }
1293
+ async executeBatches(pendingRecords) {
1294
+ for (let i = 0; i < pendingRecords.length; i += this.batchSize) {
1295
+ const batch = pendingRecords.slice(i, i + this.batchSize);
1296
+ const conditionIds = batch.map((record) => record.position.conditionId);
1297
+ if (conditionIds.length === 0) {
1298
+ continue;
1299
+ }
1300
+ try {
1301
+ const txHash = await this.redeemFn?.(conditionIds);
1302
+ batch.forEach((record) => {
1137
1303
  record.success = true;
1138
- init.logger.info(`Redeem 已执行: ${txHash}`);
1139
- } catch (error) {
1140
- const errorMsg = error instanceof Error ? error.message : String(error);
1141
- if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
1142
- const resetSeconds = this.parseResetSeconds(errorMsg);
1143
- init.logger.warning(`Redeem 限流,${resetSeconds} 秒后重试`);
1144
- telegramService.warning("Redeem 限流", `${resetSeconds} 秒后重试`);
1145
- this.pauseAndRestart((resetSeconds + 60) * 1e3);
1146
- return;
1147
- }
1304
+ record.matchedCount = 0;
1305
+ });
1306
+ logger.info(`Redeem 已执行 (批量 ${batch.length}): ${txHash}`);
1307
+ } catch (error) {
1308
+ const errorMsg = error instanceof Error ? error.message : String(error);
1309
+ if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
1310
+ const resetSeconds = this.parseResetSeconds(errorMsg);
1311
+ logger.warning(`Redeem 限流,${resetSeconds} 秒后重试`);
1312
+ telegramService.warning("Redeem 限流", `${resetSeconds} 秒后重试`);
1313
+ this.pauseAndRestart((resetSeconds + 60) * 1e3);
1314
+ return false;
1315
+ }
1316
+ batch.forEach((record) => {
1148
1317
  record.failedCount++;
1149
- init.logger.error(
1150
- `Redeem 异常 ( ${record.failedCount}): ${errorMsg}`
1151
- );
1318
+ });
1319
+ logger.error(`Redeem 批量异常 (批量 ${batch.length}): ${errorMsg}`);
1320
+ batch.forEach((record) => {
1152
1321
  if (record.failedCount >= MAX_MATCH_TIMES) {
1153
1322
  telegramService.error(
1154
1323
  "Redeem 异常",
1155
1324
  `重试 ${record.failedCount} 次仍失败
1156
- conditionId: ${position.conditionId}
1325
+ conditionId: ${record.position.conditionId}
1157
1326
  错误: ${errorMsg}`
1158
1327
  );
1159
1328
  }
1160
- }
1329
+ });
1161
1330
  }
1162
1331
  }
1332
+ return true;
1333
+ }
1334
+ collectSuccessRecords(currentIds) {
1163
1335
  const successItems = [];
1164
1336
  for (const [conditionId, record] of this.records.entries()) {
1165
- const stillExists = positions.some((p) => p.conditionId === conditionId);
1166
- if (!stillExists) {
1337
+ if (!currentIds.has(conditionId)) {
1167
1338
  successItems.push({
1168
1339
  position: record.position,
1169
1340
  matchedCount: record.matchedCount
@@ -1171,13 +1342,7 @@ conditionId: ${position.conditionId}
1171
1342
  this.records.delete(conditionId);
1172
1343
  }
1173
1344
  }
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"));
1178
- this.onRedeemSuccess?.();
1179
- }
1180
- init.logger.info(`redeem 状态: ${this.records.size} 个待确认`);
1345
+ return successItems;
1181
1346
  }
1182
1347
  /**
1183
1348
  * 统一格式化仓位信息
@@ -1192,15 +1357,12 @@ conditionId: ${position.conditionId}
1192
1357
  * 统一输出 Redeem 仓位列表
1193
1358
  */
1194
1359
  logPositions(title, items) {
1195
- init.logger.info(title);
1360
+ logger.info(title);
1196
1361
  const lines = [];
1197
1362
  for (const item of items) {
1198
- const line = this.formatPositionLine(
1199
- item.position,
1200
- item.matchedCount
1201
- );
1363
+ const line = this.formatPositionLine(item.position, item.matchedCount);
1202
1364
  lines.push(line);
1203
- init.logger.line("", `- ${line}`);
1365
+ logger.line("", `- ${line}`);
1204
1366
  }
1205
1367
  return lines;
1206
1368
  }
@@ -1240,7 +1402,7 @@ class Trader {
1240
1402
  }
1241
1403
  this.config = config;
1242
1404
  if (!config.polymarket?.privateKey) {
1243
- init.logger.warning("未配置 PolyMarket 私钥,交易功能已禁用");
1405
+ logger.warning("未配置 PolyMarket 私钥,交易功能已禁用");
1244
1406
  return;
1245
1407
  }
1246
1408
  await this.client.init({
@@ -1254,10 +1416,10 @@ class Trader {
1254
1416
  builderCreds: config.polymarket.builderCreds
1255
1417
  });
1256
1418
  } else {
1257
- init.logger.warning("未配置 builderCreds,自动 Redeem 功能已禁用");
1419
+ logger.warning("未配置 builderCreds,自动 Redeem 功能已禁用");
1258
1420
  }
1259
1421
  this.initialized = true;
1260
- init.logger.success("交易模块初始化完成");
1422
+ logger.success("交易模块初始化完成");
1261
1423
  }
1262
1424
  /**
1263
1425
  * 启动 Redeemer(在 Telegram listener 就绪后调用)
@@ -1267,7 +1429,7 @@ class Trader {
1267
1429
  return;
1268
1430
  }
1269
1431
  this.redeemer.start(
1270
- this.executeRedeem.bind(this),
1432
+ this.executeRedeemBatch.bind(this),
1271
1433
  this.config.polymarket.funderAddress || "",
1272
1434
  () => this.balanceCache.invalidate()
1273
1435
  );
@@ -1283,19 +1445,19 @@ class Trader {
1283
1445
  */
1284
1446
  async executeSignal(signal) {
1285
1447
  if (!this.initialized || !this.config) {
1286
- init.logger.warning("交易模块未初始化,跳过信号");
1448
+ logger.warning("交易模块未初始化,跳过信号");
1287
1449
  return;
1288
1450
  }
1289
1451
  const { metadata } = signal;
1290
1452
  const { assetId, endTime, outcome } = metadata;
1291
- init.logger.section(`处理信号: ${outcome} (${metadata.eventId})`);
1453
+ logger.section(`处理信号: ${outcome} (${metadata.eventId})`);
1292
1454
  if (!this.assetFilter.canTrade(assetId, endTime)) {
1293
- init.logger.warning(`assetId 已交易,跳过: ${assetId.slice(0, 10)}...`);
1455
+ logger.warning(`assetId 已交易,跳过: ${assetId.slice(0, 10)}...`);
1294
1456
  return;
1295
1457
  }
1296
1458
  const tradingConfig = this.config.trading;
1297
1459
  const amountMode = tradingConfig?.amountMode || "fixed";
1298
- const amountValue = tradingConfig?.amountValue || 1;
1460
+ const amountValue = amountMode === "percentage" ? tradingConfig?.amountPercentageValue ?? 0.1 : tradingConfig?.amountFixedValue ?? 5;
1299
1461
  const buyPrice = tradingConfig?.buyPrice || 0.98;
1300
1462
  const sellPrice = tradingConfig?.sellPrice || 1;
1301
1463
  let balance;
@@ -1306,12 +1468,12 @@ class Trader {
1306
1468
  );
1307
1469
  } catch (error) {
1308
1470
  const errorMsg = error instanceof Error ? error.message : String(error);
1309
- init.logger.error(`获取余额失败: ${errorMsg}`);
1471
+ logger.error(`获取余额失败: ${errorMsg}`);
1310
1472
  return;
1311
1473
  }
1312
1474
  if (amountMode === "fixed" && balance < amountValue) {
1313
1475
  const msg = `余额不足: ${balance} USDC < ${amountValue} USDC`;
1314
- init.logger.warning(msg);
1476
+ logger.warning(msg);
1315
1477
  telegramService.warning(
1316
1478
  "余额不足",
1317
1479
  `当前余额: ${balance} USDC
@@ -1320,18 +1482,18 @@ class Trader {
1320
1482
  return;
1321
1483
  }
1322
1484
  if (amountMode === "percentage" && balance <= 0) {
1323
- init.logger.warning(`余额为零: ${balance} USDC`);
1485
+ logger.warning(`余额为零: ${balance} USDC`);
1324
1486
  telegramService.warning("余额为零", `当前余额: ${balance} USDC`);
1325
1487
  return;
1326
1488
  }
1327
1489
  const orderAmount = amountMode === "fixed" ? amountValue : balance * amountValue;
1328
1490
  const size = orderAmount / buyPrice;
1329
- init.logger.info(`下单金额: ${orderAmount.toFixed(2)} USDC`);
1330
- init.logger.info(`下单价格: ${buyPrice}`);
1331
- init.logger.info(`下单数量: ${size.toFixed(4)}`);
1491
+ logger.info(`下单金额: ${orderAmount.toFixed(2)} USDC`);
1492
+ logger.info(`下单价格: ${buyPrice}`);
1493
+ logger.info(`下单数量: ${size.toFixed(4)}`);
1332
1494
  const buyResult = await this.client.createBuyOrder(assetId, buyPrice, size);
1333
1495
  if (!buyResult.success) {
1334
- init.logger.error(`买单失败: ${buyResult.errorMsg}`);
1496
+ logger.error(`买单失败: ${buyResult.errorMsg}`);
1335
1497
  telegramService.error(
1336
1498
  "买单失败",
1337
1499
  `事件: ${metadata.eventId}
@@ -1341,7 +1503,7 @@ class Trader {
1341
1503
  }
1342
1504
  this.assetFilter.markTraded(assetId, endTime);
1343
1505
  if (sellPrice === 1) {
1344
- init.logger.info("sellPrice = 1,成交后将自动 redeem");
1506
+ logger.info("sellPrice = 1,成交后将自动 redeem");
1345
1507
  return;
1346
1508
  }
1347
1509
  this.watchAndHandle(
@@ -1371,7 +1533,7 @@ class Trader {
1371
1533
  );
1372
1534
  } catch (error) {
1373
1535
  const errorMsg = error instanceof Error ? error.message : String(error);
1374
- init.logger.error(`订单监控异常: ${errorMsg}`);
1536
+ logger.error(`订单监控异常: ${errorMsg}`);
1375
1537
  telegramService.error(
1376
1538
  "订单监控异常",
1377
1539
  `事件: ${eventId}
@@ -1388,22 +1550,22 @@ class Trader {
1388
1550
  case "MATCHED":
1389
1551
  case "CONFIRMED":
1390
1552
  const filledSize = result.filledSize || 0;
1391
- init.logger.success(`买单成交: ${filledSize.toFixed(4)} 份`);
1553
+ logger.success(`买单成交: ${filledSize.toFixed(4)} 份`);
1392
1554
  if (sellPrice < 1) {
1393
1555
  this.waitAndSell(assetId, filledSize, sellPrice, endTime);
1394
1556
  } else {
1395
- init.logger.info("sellPrice = 1,等待 Redeem");
1557
+ logger.info("sellPrice = 1,等待 Redeem");
1396
1558
  }
1397
1559
  break;
1398
1560
  case "CANCELLED":
1399
- init.logger.info("订单已取消(市场可能已结束)");
1561
+ logger.info("订单已取消(市场可能已结束)");
1400
1562
  break;
1401
1563
  case "FAILED":
1402
- init.logger.error("订单执行失败");
1564
+ logger.error("订单执行失败");
1403
1565
  telegramService.error("订单执行失败", `订单 ID: ${orderId}`);
1404
1566
  break;
1405
1567
  case "TIMEOUT":
1406
- init.logger.warning("订单超时,尝试取消");
1568
+ logger.warning("订单超时,尝试取消");
1407
1569
  await this.client.cancelOrder(orderId);
1408
1570
  break;
1409
1571
  }
@@ -1414,18 +1576,18 @@ class Trader {
1414
1576
  async waitAndSell(assetId, expectedSize, sellPrice, endTime) {
1415
1577
  const shortId = assetId.substring(0, 10) + "...";
1416
1578
  const threshold = expectedSize * POSITION_MATCH_THRESHOLD;
1417
- init.logger.info(`等待仓位同步: ${shortId},预期 >= ${threshold.toFixed(4)}`);
1579
+ logger.info(`等待仓位同步: ${shortId},预期 >= ${threshold.toFixed(4)}`);
1418
1580
  while (true) {
1419
1581
  const now = Date.now();
1420
1582
  if (now >= endTime) {
1421
- init.logger.warning(`仓位同步超时: ${shortId},等待 Redeem`);
1583
+ logger.warning(`仓位同步超时: ${shortId},等待 Redeem`);
1422
1584
  this.notifyRedeemFallback(assetId, expectedSize);
1423
1585
  return;
1424
1586
  }
1425
1587
  try {
1426
1588
  const positionSize = await this.client.getPositionSize(assetId);
1427
1589
  if (positionSize >= threshold) {
1428
- init.logger.info(
1590
+ logger.info(
1429
1591
  `仓位已同步: ${shortId},数量: ${positionSize.toFixed(4)}`
1430
1592
  );
1431
1593
  await this.executeSellOrder(assetId, sellPrice, positionSize);
@@ -1433,7 +1595,7 @@ class Trader {
1433
1595
  }
1434
1596
  } catch (error) {
1435
1597
  const errorMsg = error instanceof Error ? error.message : String(error);
1436
- init.logger.warning(`获取仓位失败: ${shortId},${errorMsg}`);
1598
+ logger.warning(`获取仓位失败: ${shortId},${errorMsg}`);
1437
1599
  }
1438
1600
  await this.delay(POSITION_POLL_INTERVAL);
1439
1601
  }
@@ -1444,7 +1606,7 @@ class Trader {
1444
1606
  notifyRedeemFallback(assetId, expectedSize) {
1445
1607
  if (!this.relayClient.isInitialized()) {
1446
1608
  const msg = "仓位同步超时且 Redeem 不可用";
1447
- init.logger.warning(msg);
1609
+ logger.warning(msg);
1448
1610
  telegramService.warning(
1449
1611
  msg,
1450
1612
  `Asset: ${assetId.substring(0, 20)}...
@@ -1463,36 +1625,36 @@ class Trader {
1463
1625
  * 执行卖单
1464
1626
  */
1465
1627
  async executeSellOrder(assetId, price, size) {
1466
- init.logger.info(`创建卖单: ${size.toFixed(4)} 份 @ ${price}`);
1628
+ logger.info(`创建卖单: ${size.toFixed(4)} 份 @ ${price}`);
1467
1629
  const sellResult = await this.client.createSellOrder(assetId, price, size);
1468
1630
  if (sellResult.success) {
1469
- init.logger.success(`卖单已提交: ${sellResult.orderId}`);
1631
+ logger.success(`卖单已提交: ${sellResult.orderId}`);
1470
1632
  } else {
1471
- init.logger.error(`卖单提交失败: ${sellResult.errorMsg}`);
1633
+ logger.error(`卖单提交失败: ${sellResult.errorMsg}`);
1472
1634
  telegramService.error("卖单提交失败", `错误: ${sellResult.errorMsg}`);
1473
1635
  }
1474
1636
  }
1475
1637
  /**
1476
- * 执行 Redeem(供 Redeemer 调用)
1638
+ * 批量执行 Redeem(供 Redeemer 调用)
1477
1639
  */
1478
- async executeRedeem(conditionId) {
1640
+ async executeRedeemBatch(conditionIds) {
1479
1641
  if (!this.relayClient.isInitialized()) {
1480
1642
  throw new Error("RelayClient 未初始化");
1481
1643
  }
1482
- return this.relayClient.redeem(conditionId);
1644
+ return this.relayClient.redeemBatch(conditionIds);
1483
1645
  }
1484
1646
  /**
1485
1647
  * 停止交易模块
1486
1648
  */
1487
1649
  async shutdown() {
1488
1650
  this.redeemer.stop();
1489
- init.logger.info("交易模块已停止");
1651
+ logger.info("交易模块已停止");
1490
1652
  }
1491
1653
  }
1492
1654
  const trader = new Trader();
1493
1655
  function enterDaemonMode() {
1494
- if (!fs__namespace.existsSync(paths.PID_DIR)) {
1495
- fs__namespace.mkdirSync(paths.PID_DIR, { recursive: true });
1656
+ if (!fs__namespace.existsSync(processLock.PID_DIR)) {
1657
+ fs__namespace.mkdirSync(processLock.PID_DIR, { recursive: true });
1496
1658
  }
1497
1659
  const child = child_process.spawn(process.execPath, [__filename], {
1498
1660
  detached: true,
@@ -1504,46 +1666,81 @@ function enterDaemonMode() {
1504
1666
  }
1505
1667
  });
1506
1668
  if (child.pid) {
1507
- fs__namespace.writeFileSync(paths.PID_FILE, child.pid.toString());
1508
- init.logger.blank();
1509
- init.logger.success(`已进入后台模式 (PID: ${child.pid})`);
1510
- init.logger.line("", `日志目录: ${paths.LOGS_DIR}`);
1511
- init.logger.line("", `使用 'polycopy stop' 停止`);
1512
- init.logger.line("", `使用 'polycopy log' 查看日志`);
1669
+ fs__namespace.writeFileSync(processLock.PID_FILE, child.pid.toString());
1670
+ logger.blank();
1671
+ logger.success(`已进入后台模式 (PID: ${child.pid})`);
1672
+ logger.line("", `日志目录: ${processLock.LOGS_DIR}`);
1673
+ logger.line("", `使用 'polycopy stop' 停止`);
1674
+ logger.line("", `使用 'polycopy log' 查看日志`);
1513
1675
  child.unref();
1514
1676
  process.exit(0);
1515
1677
  } else {
1516
- init.logger.error("进入后台模式失败");
1678
+ logger.error("进入后台模式失败");
1517
1679
  process.exit(1);
1518
1680
  }
1519
1681
  }
1520
1682
  async function main() {
1521
- init.logger.init();
1522
- init.logger.section("PolyMarket 跟单机器人");
1523
- const config = await init.ensureConfig();
1683
+ logger.init();
1684
+ if (process.env.POLYCOPY_DAEMON_CHILD !== "1") {
1685
+ const runningPid = processLock.resolveRunningPid();
1686
+ if (runningPid) {
1687
+ console.log(`⚠️ 程序正在运行中 (PID: ${runningPid})`);
1688
+ console.log(` 请先停止程序后再启动`);
1689
+ process.exit(1);
1690
+ }
1691
+ processLock.savePid(process.pid);
1692
+ }
1693
+ const exitOnFatal = async () => {
1694
+ await logger.flush();
1695
+ if (process.env.POLYCOPY_DAEMON_CHILD !== "1") {
1696
+ processLock.removePid();
1697
+ }
1698
+ process.exit(1);
1699
+ };
1700
+ process.on("uncaughtException", async (error) => {
1701
+ const message = error instanceof Error ? error.stack || error.message : String(error);
1702
+ logger.error(`捕获未处理异常:
1703
+ ${message}`);
1704
+ await exitOnFatal();
1705
+ });
1706
+ process.on("unhandledRejection", async (reason) => {
1707
+ const message = reason instanceof Error ? reason.stack || reason.message : String(reason);
1708
+ logger.error(`捕获未处理 Promise 拒绝:
1709
+ ${message}`);
1710
+ await exitOnFatal();
1711
+ });
1712
+ process.on("beforeExit", async () => {
1713
+ await logger.flush();
1714
+ });
1715
+ const config = await processLock.ensureConfig();
1716
+ if (process.env.POLYCOPY_DAEMON === "1") {
1717
+ enterDaemonMode();
1718
+ return;
1719
+ }
1720
+ logger.section("PolyMarket 跟单机器人");
1524
1721
  telegramService.init(
1525
1722
  config.telegram.botToken,
1526
1723
  config.telegram.adminChatId,
1527
1724
  config.telegram.targetChatIds || []
1528
1725
  );
1529
- const autoTradeEnabled = init.canAutoTrade(config);
1726
+ const autoTradeEnabled = processLock.canAutoTrade(config);
1530
1727
  if (!autoTradeEnabled) {
1531
- init.logger.warning("自动跟单功能未启用");
1728
+ logger.warning("自动跟单功能未启用");
1532
1729
  if (!config.polymarket?.privateKey) {
1533
- init.logger.line("", "- PolyMarket 配置不完整(缺少私钥)");
1730
+ logger.line("", "- PolyMarket 配置不完整(缺少私钥)");
1534
1731
  }
1535
- if (!config.trading?.amountMode || !config.trading?.amountValue || !config.trading?.buyPrice || !config.trading?.sellPrice) {
1536
- init.logger.line("", "- 交易配置不完整");
1732
+ if (!config.trading?.amountMode || !config.trading?.buyPrice || !config.trading?.sellPrice || (config.trading.amountMode === "fixed" ? !config.trading.amountFixedValue : !config.trading.amountPercentageValue)) {
1733
+ logger.line("", "- 交易配置不完整");
1537
1734
  }
1538
- init.logger.line("", "当前不会跟单任何预测消息");
1735
+ logger.line("", "当前不会跟单任何预测消息");
1539
1736
  } else {
1540
1737
  try {
1541
1738
  await trader.init(config);
1542
1739
  if (config.trading?.sellPrice === 1 && !config.polymarket?.builderCreds) {
1543
- init.logger.error(
1740
+ logger.error(
1544
1741
  "sellPrice = 1 但未配置 builderCreds,Redeem 功能无法使用,程序已退出"
1545
1742
  );
1546
- init.logger.line(
1743
+ logger.line(
1547
1744
  "",
1548
1745
  "请配置 Builder API 凭证,或将卖价设置为 < 1 使用限价卖出模式"
1549
1746
  );
@@ -1551,29 +1748,29 @@ async function main() {
1551
1748
  }
1552
1749
  } catch (error) {
1553
1750
  const errorMsg = error instanceof Error ? error.message : String(error);
1554
- init.logger.error(`交易模块初始化失败: ${errorMsg}`);
1555
- init.logger.line("", "当前不会跟单任何预测消息");
1751
+ logger.error(`交易模块初始化失败: ${errorMsg}`);
1752
+ logger.line("", "当前不会跟单任何预测消息");
1556
1753
  }
1557
1754
  }
1558
1755
  telegramService.onChatIdsChanged((chatIds) => {
1559
- const telegram = init.configLocal.getItem("telegram");
1756
+ const telegram = processLock.configLocal.getItem("telegram");
1560
1757
  if (!telegram) {
1561
- init.logger.error("警告: 无法保存频道 ID,telegram 配置不存在");
1758
+ logger.error("警告: 无法保存频道 ID,telegram 配置不存在");
1562
1759
  return;
1563
1760
  }
1564
1761
  telegram.targetChatIds = chatIds.length > 0 ? chatIds : void 0;
1565
- init.configLocal.setItem("telegram", telegram);
1566
- init.logger.line("💾", `已保存频道 ID 列表: ${chatIds.length} 个频道`);
1762
+ processLock.configLocal.setItem("telegram", telegram);
1763
+ logger.line("💾", `已保存频道 ID 列表: ${chatIds.length} 个频道`);
1567
1764
  });
1568
1765
  telegramService.onSignal(async (signal) => {
1569
- init.logger.info(`📨 收到预测信号:`);
1570
- init.logger.line("", `事件: ${signal.metadata.eventId}`);
1571
- init.logger.line(
1766
+ logger.info(`📨 收到预测信号:`);
1767
+ logger.line("", `事件: ${signal.metadata.eventId}`);
1768
+ logger.line(
1572
1769
  "",
1573
1770
  `方向: ${signal.metadata.outcome === "Up" ? "上涨 📈" : "下跌 📉"}`
1574
1771
  );
1575
- init.logger.line("", `Asset: ${signal.metadata.assetId}`);
1576
- init.logger.line(
1772
+ logger.line("", `Asset: ${signal.metadata.assetId}`);
1773
+ logger.line(
1577
1774
  "",
1578
1775
  `结束时间: ${new Date(signal.metadata.endTime).toLocaleString()}`
1579
1776
  );
@@ -1582,24 +1779,23 @@ async function main() {
1582
1779
  await trader.executeSignal(signal);
1583
1780
  } catch (error) {
1584
1781
  const errorMsg = error instanceof Error ? error.message : String(error);
1585
- init.logger.error(`交易执行失败: ${errorMsg}`);
1782
+ logger.error(`交易执行失败: ${errorMsg}`);
1586
1783
  }
1587
1784
  }
1588
1785
  });
1589
- if (process.env.POLYCOPY_DAEMON === "1") {
1590
- enterDaemonMode();
1591
- return;
1592
- }
1593
1786
  let isShuttingDown = false;
1594
1787
  const shutdown = async () => {
1595
1788
  if (isShuttingDown) return;
1596
1789
  isShuttingDown = true;
1597
1790
  process.removeAllListeners("SIGINT");
1598
1791
  process.removeAllListeners("SIGTERM");
1599
- init.logger.blank();
1600
- init.logger.info("正在退出...");
1792
+ logger.blank();
1793
+ logger.info("正在退出...");
1601
1794
  await Promise.all([trader.shutdown(), telegramService.stop()]);
1602
- await init.logger.flush();
1795
+ await logger.flush();
1796
+ if (process.env.POLYCOPY_DAEMON_CHILD !== "1") {
1797
+ processLock.removePid();
1798
+ }
1603
1799
  process.exit(0);
1604
1800
  };
1605
1801
  process.prependListener("SIGINT", () => {
@@ -1614,7 +1810,13 @@ async function main() {
1614
1810
  }
1615
1811
  });
1616
1812
  }
1617
- main().catch((error) => {
1618
- init.logger.error("程序运行出错:", error);
1813
+ main().catch(async (error) => {
1814
+ const message = error instanceof Error ? error.stack || error.message : String(error);
1815
+ logger.error(`程序运行出错:
1816
+ ${message}`);
1817
+ await logger.flush();
1818
+ if (process.env.POLYCOPY_DAEMON_CHILD !== "1") {
1819
+ processLock.removePid();
1820
+ }
1619
1821
  process.exit(1);
1620
1822
  });