From 2c62d2ed3e7d19e3591aa6c3575face767f6c74b Mon Sep 17 00:00:00 2001 From: discountry Date: Sat, 27 Sep 2025 00:01:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=85=A8=E5=B1=80=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86=E6=9C=BA=E5=88=B6=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=BF=90=E8=A1=8C=E6=97=B6=E9=94=99=E8=AF=AF=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=AE=B0=E5=BD=95=EF=BC=8C=E6=8F=90=E5=8D=87=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E7=A8=B3=E5=AE=9A=E6=80=A7=E5=92=8C=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E8=BF=BD=E8=B8=AA=E8=83=BD=E5=8A=9B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/trend-engine.ts | 21 +++++--- src/exchanges/aster-adapter.ts | 99 ++++++++++++++++++++++++++-------- src/index.tsx | 4 +- src/runtime-errors.ts | 87 ++++++++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 30 deletions(-) create mode 100644 src/runtime-errors.ts diff --git a/src/core/trend-engine.ts b/src/core/trend-engine.ts index a940750..69580e8 100644 --- a/src/core/trend-engine.ts +++ b/src/core/trend-engine.ts @@ -25,7 +25,7 @@ import { unlockOperating, } from "./order-coordinator"; import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "./order-coordinator"; -import { isUnknownOrderError } from "../utils/errors"; +import { extractMessage, isUnknownOrderError } from "../utils/errors"; import { roundDownToTick } from "../utils/math"; import { createTradeLog, type TradeLogEntry } from "../state/trade-log"; import { decryptCopyright } from "../utils/copyright"; @@ -153,7 +153,7 @@ export class TrendEngine { this.updateSessionVolume(position); this.emitUpdate(); } catch (err) { - this.tradeLog.push("error", `账户推送处理异常: ${String(err)}`); + this.tradeLog.push("error", `账户推送处理异常: ${extractMessage(err)}`); } }); } catch (err) { @@ -178,7 +178,7 @@ export class TrendEngine { this.ordersSnapshotReady = true; this.emitUpdate(); } catch (err) { - this.tradeLog.push("error", `订单推送处理异常: ${String(err)}`); + this.tradeLog.push("error", `订单推送处理异常: ${extractMessage(err)}`); } }); } catch (err) { @@ -190,7 +190,7 @@ export class TrendEngine { this.depthSnapshot = depth; this.emitUpdate(); } catch (err) { - this.tradeLog.push("error", `深度推送处理异常: ${String(err)}`); + this.tradeLog.push("error", `深度推送处理异常: ${extractMessage(err)}`); } }); } catch (err) { @@ -202,7 +202,7 @@ export class TrendEngine { this.tickerSnapshot = ticker; this.emitUpdate(); } catch (err) { - this.tradeLog.push("error", `价格推送处理异常: ${String(err)}`); + this.tradeLog.push("error", `价格推送处理异常: ${extractMessage(err)}`); } }); } catch (err) { @@ -214,7 +214,7 @@ export class TrendEngine { this.klineSnapshot = Array.isArray(klines) ? klines : []; this.emitUpdate(); } catch (err) { - this.tradeLog.push("error", `K线推送处理异常: ${String(err)}`); + this.tradeLog.push("error", `K线推送处理异常: ${extractMessage(err)}`); } }); } catch (err) { @@ -300,8 +300,13 @@ export class TrendEngine { } this.emitUpdate(); } finally { - this.rateLimit.onCycleComplete(hadRateLimit); - this.processing = false; + try { + this.rateLimit.onCycleComplete(hadRateLimit); + } catch (rateLimitError) { + this.tradeLog.push("error", `限频控制器状态更新失败: ${String(rateLimitError)}`); + } finally { + this.processing = false; + } } } diff --git a/src/exchanges/aster-adapter.ts b/src/exchanges/aster-adapter.ts index 13d4d82..b6cd445 100644 --- a/src/exchanges/aster-adapter.ts +++ b/src/exchanges/aster-adapter.ts @@ -7,6 +7,7 @@ import type { TickerListener, } from "./adapter"; import type { AsterOrder, CreateOrderParams, AsterDepth, AsterTicker, AsterKline } from "./types"; +import { extractMessage } from "../utils/errors"; import { AsterGateway } from "./aster/client"; export interface AsterCredentials { @@ -20,71 +21,127 @@ export class AsterExchangeAdapter implements ExchangeAdapter { private readonly gateway: AsterGateway; private readonly symbol: string; private initPromise: Promise | null = null; + private lastInitErrorAt = 0; + private readonly initContexts = new Set(); + private retryTimer: ReturnType | null = null; + private retryDelayMs = 3000; constructor(credentials: AsterCredentials = {}) { this.gateway = new AsterGateway({ apiKey: credentials.apiKey, apiSecret: credentials.apiSecret }); this.symbol = (credentials.symbol ?? process.env.TRADE_SYMBOL ?? "BTCUSDT").toUpperCase(); } - private ensureInitialized(): Promise { + private safeInvoke void>(context: string, cb: T): T { + const wrapped = ((...args: any[]) => { + try { + cb(...args); + } catch (error) { + console.error(`[AsterExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`); + } + }) as T; + return wrapped; + } + + private ensureInitialized(context?: string): Promise { if (!this.initPromise) { - this.initPromise = this.gateway.ensureInitialized(this.symbol); + this.initContexts.clear(); + this.initPromise = this.gateway.ensureInitialized(this.symbol).then((value) => { + this.clearRetry(); + return value; + }).catch((error) => { + this.handleInitError("initialize", error); + this.initPromise = null; + this.scheduleRetry(); + throw error; + }); + } + if (context && !this.initContexts.has(context)) { + this.initContexts.add(context); + this.initPromise.catch((error) => { + this.handleInitError(context, error); + this.scheduleRetry(); + }); } return this.initPromise; } + private scheduleRetry(): void { + if (this.retryTimer) return; + this.retryTimer = setTimeout(() => { + this.retryTimer = null; + if (this.initPromise) return; + this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000); + void this.ensureInitialized("retry"); + }, this.retryDelayMs); + } + + private clearRetry(): void { + if (this.retryTimer) { + clearTimeout(this.retryTimer); + this.retryTimer = null; + } + this.retryDelayMs = 3000; + } + + private handleInitError(context: string, error: unknown): void { + const now = Date.now(); + if (now - this.lastInitErrorAt < 5000) return; + this.lastInitErrorAt = now; + console.error(`[AsterExchangeAdapter] ${context} failed`, error); + } + watchAccount(cb: AccountListener): void { - void this.ensureInitialized(); - this.gateway.onAccount((snapshot) => { + void this.ensureInitialized("watchAccount"); + this.gateway.onAccount(this.safeInvoke("watchAccount", (snapshot) => { cb(snapshot); - }); + })); } watchOrders(cb: OrderListener): void { - void this.ensureInitialized(); - this.gateway.onOrders((orders) => { + void this.ensureInitialized("watchOrders"); + this.gateway.onOrders(this.safeInvoke("watchOrders", (orders) => { cb(orders); - }); + })); } watchDepth(symbol: string, cb: DepthListener): void { - void this.ensureInitialized(); - this.gateway.onDepth(symbol, (depth: AsterDepth) => { + void this.ensureInitialized("watchDepth"); + this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", (depth: AsterDepth) => { cb(depth); - }); + })); } watchTicker(symbol: string, cb: TickerListener): void { - void this.ensureInitialized(); - this.gateway.onTicker(symbol, (ticker: AsterTicker) => { + void this.ensureInitialized("watchTicker"); + this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", (ticker: AsterTicker) => { cb(ticker); - }); + })); } watchKlines(symbol: string, interval: string, cb: KlineListener): void { - void this.ensureInitialized(); - this.gateway.onKlines(symbol, interval, (klines: AsterKline[]) => { + void this.ensureInitialized("watchKlines"); + this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", (klines: AsterKline[]) => { cb(klines); - }); + })); } async createOrder(params: CreateOrderParams): Promise { - await this.ensureInitialized(); + await this.ensureInitialized("createOrder"); return this.gateway.createOrder(params); } async cancelOrder(params: { symbol: string; orderId: number | string }): Promise { - await this.ensureInitialized(); + await this.ensureInitialized("cancelOrder"); await this.gateway.cancelOrder({ symbol: params.symbol, orderId: Number(params.orderId) }); } async cancelOrders(params: { symbol: string; orderIdList: Array }): Promise { - await this.ensureInitialized(); + await this.ensureInitialized("cancelOrders"); await this.gateway.cancelOrders({ symbol: params.symbol, orderIdList: params.orderIdList }); } async cancelAllOrders(params: { symbol: string }): Promise { - await this.ensureInitialized(); + await this.ensureInitialized("cancelAllOrders"); await this.gateway.cancelAllOrders(params); } } diff --git a/src/index.tsx b/src/index.tsx index 7679907..a8c52f6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,8 @@ -import "dotenv/config"; import React from "react"; import { render } from "ink"; import { App } from "./ui/App"; +import { setupGlobalErrorHandlers } from "./runtime-errors"; + +setupGlobalErrorHandlers(); render(); diff --git a/src/runtime-errors.ts b/src/runtime-errors.ts new file mode 100644 index 0000000..629d801 --- /dev/null +++ b/src/runtime-errors.ts @@ -0,0 +1,87 @@ +import { extractMessage } from "./utils/errors"; + +declare const Bun: { on?: (event: string, listener: (payload: any) => void) => void } | undefined; + +type Handler = (error: unknown) => void; + +let installed = false; +const lastLogAt = new Map(); + +function logRuntimeIssue(kind: string, error: unknown): void { + const message = extractMessage(error); + const now = Date.now(); + const key = `${kind}:${message}`; + const previous = lastLogAt.get(key) ?? 0; + if (now - previous < 1000) return; + lastLogAt.set(key, now); + console.error(`[RuntimeGuard] ${kind}: ${message}`); + if (error instanceof Error && error.stack) { + console.error(error.stack); + } +} + +function bindProcessEvent(event: string, handler: Handler): void { + if (typeof process === "undefined" || typeof process.on !== "function") return; + process.on(event as any, (error: unknown) => { + try { + handler(error); + } catch (loggingError) { + console.error(`[RuntimeGuard] Failed to log ${event}:`, loggingError); + } + }); +} + +export function setupGlobalErrorHandlers(): void { + if (installed) return; + installed = true; + + bindProcessEvent("uncaughtException", (error) => { + logRuntimeIssue("uncaughtException", error); + }); + + bindProcessEvent("unhandledRejection", (reason) => { + logRuntimeIssue("unhandledRejection", reason); + }); + + bindProcessEvent("multipleResolves", (payload) => { + logRuntimeIssue("multipleResolves", payload); + }); + + const globalWithEvents = globalThis as unknown as { + addEventListener?: (type: string, listener: (event: any) => void) => void; + }; + + if (typeof globalWithEvents.addEventListener === "function") { + try { + globalWithEvents.addEventListener("unhandledrejection", (event: any) => { + logRuntimeIssue("unhandledRejection", event?.reason); + if (event && typeof event.preventDefault === "function") { + event.preventDefault(); + } + }); + globalWithEvents.addEventListener("error", (event: any) => { + logRuntimeIssue("unhandledError", event?.error ?? event); + if (event && typeof event.preventDefault === "function") { + event.preventDefault(); + } + }); + } catch (handlerError) { + console.error("[RuntimeGuard] Failed to bind global listeners", handlerError); + } + } + + if (typeof Bun !== "undefined" && typeof Bun?.on === "function") { + try { + Bun.on("unhandledRejection", (reason: unknown) => { + logRuntimeIssue("bun.unhandledRejection", reason); + }); + Bun.on("error", (error: unknown) => { + logRuntimeIssue("bun.error", error); + }); + } catch (bunHandlerError) { + console.error("[RuntimeGuard] Failed to bind Bun listeners", bunHandlerError); + } + } +} + +