From f8cf98984b8d8f630ec43a8efac97166af3f2658 Mon Sep 17 00:00:00 2001 From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:53:56 +0800 Subject: [PATCH 1/2] fix: restore Binance chart order lines --- scripts/binance-orderbook-trade.user.js | 39 +++++++++---------- .../core/tradingview-orders-recovery.js | 4 -- src/binance-orderbook-trade/index.user.js | 36 +++++++++-------- .../chart-orders-recovery.test.js | 15 +++---- .../source-regressions.test.js | 15 +++++-- 5 files changed, 59 insertions(+), 50 deletions(-) diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index c011722..954c6d7 100644 --- a/scripts/binance-orderbook-trade.user.js +++ b/scripts/binance-orderbook-trade.user.js @@ -3,7 +3,7 @@ // @namespace binance.orderbook.trade // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E -// @version 2.7.138 +// @version 2.7.139 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -1682,7 +1682,6 @@ // src/binance-orderbook-trade/core/tradingview-orders-recovery.js var TRADINGVIEW_ORDERS_RECOVERY_STORAGE_KEY = "binance-orderbook-trade:tradingview-orders-recovery:v1"; - var TRADINGVIEW_ORDERS_RECOVERY_MAX_AGE_MS = 10 * 60 * 1e3; function createTradingViewOrdersRecoveryRecord(nowMs) { if (!Number.isFinite(nowMs)) throw new Error("Chart orders recovery timestamp is invalid"); return JSON.stringify({ version: 1, originalVisible: true, createdAtMs: nowMs }); @@ -1700,9 +1699,6 @@ if (keys.join(",") !== "createdAtMs,originalVisible,version" || record.version !== 1 || record.originalVisible !== true || !Number.isFinite(record.createdAtMs) || record.createdAtMs > nowMs) { return { status: "invalid", record: null }; } - if (nowMs - record.createdAtMs > TRADINGVIEW_ORDERS_RECOVERY_MAX_AGE_MS) { - return { status: "expired", record }; - } return { status: "valid", record }; } @@ -4721,19 +4717,26 @@ dialogSignal }; const recordAction = (eventTarget) => { + const contract = findBinanceCancelAllDialog(document, isVisibleElement); + if (!contract) return; + watcher.seenDialog = true; + const action = classifyBinanceCancelAllDialogAction(contract, eventTarget); + if (action && !watcher.action) watcher.action = action; + }; + const rejectInvalidDialogAction = (event, error) => { + event.preventDefault(); + event.stopImmediatePropagation(); + watcher.error = error; + }; + const handleClick = (event) => { try { - const contract = findBinanceCancelAllDialog(document, isVisibleElement); - if (!contract) return; - watcher.seenDialog = true; - const action = classifyBinanceCancelAllDialogAction(contract, eventTarget); - if (action && !watcher.action) watcher.action = action; + recordAction(event.target); } catch (error) { - watcher.error = error; + rejectInvalidDialogAction(event, error); } finally { dialogSignal.notify(); } }; - const handleClick = (event) => recordAction(event.target); const handleKeydown = (event) => { if (event.key !== "Escape" && event.key !== "Enter" && event.key !== " ") return; try { @@ -4747,7 +4750,7 @@ ); if (action && !watcher.action) watcher.action = action; } catch (error) { - watcher.error = error; + rejectInvalidDialogAction(event, error); } finally { dialogSignal.notify(); } @@ -4858,11 +4861,8 @@ tradingViewOrdersRecoveryPendingAtStartup = false; return { status: "missing" }; } - if (recovery.status === "invalid" || recovery.status === "expired") { - emit( - "ERR", - `图表当前委托恢复记录${recovery.status === "invalid" ? "无效" : "已过期"},未修改页面状态` - ); + if (recovery.status === "invalid") { + emit("ERR", "图表当前委托恢复记录无效,未修改页面状态"); clearTradingViewOrdersRecoveryRecord(); tradingViewOrdersRecoveryPendingAtStartup = false; return { status: recovery.status }; @@ -5021,9 +5021,8 @@ ); } catch (error) { restoreTemporaryUiState = false; - restoreTradingViewOrdersState = false; emit("ERR", "币安撤单确认弹窗结构异常", error); - const message = `${symbol} 撤单确认弹窗结构异常,图表当前委托保持隐藏`; + const message = `${symbol} 撤单确认弹窗结构异常,未执行弹窗操作`; setLadderStatus(message); return { ok: false, status: "dialog_contract_invalid", message }; } finally { diff --git a/src/binance-orderbook-trade/core/tradingview-orders-recovery.js b/src/binance-orderbook-trade/core/tradingview-orders-recovery.js index 93bfa3d..2d778e5 100644 --- a/src/binance-orderbook-trade/core/tradingview-orders-recovery.js +++ b/src/binance-orderbook-trade/core/tradingview-orders-recovery.js @@ -1,6 +1,5 @@ export const TRADINGVIEW_ORDERS_RECOVERY_STORAGE_KEY = 'binance-orderbook-trade:tradingview-orders-recovery:v1'; -export const TRADINGVIEW_ORDERS_RECOVERY_MAX_AGE_MS = 10 * 60 * 1000; export function createTradingViewOrdersRecoveryRecord(nowMs) { if (!Number.isFinite(nowMs)) throw new Error('Chart orders recovery timestamp is invalid'); @@ -27,8 +26,5 @@ export function parseTradingViewOrdersRecoveryRecord(rawValue, nowMs) { ) { return { status: 'invalid', record: null }; } - if (nowMs - record.createdAtMs > TRADINGVIEW_ORDERS_RECOVERY_MAX_AGE_MS) { - return { status: 'expired', record }; - } return { status: 'valid', record }; } diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index 87c3093..70983ea 100644 --- a/src/binance-orderbook-trade/index.user.js +++ b/src/binance-orderbook-trade/index.user.js @@ -3,7 +3,7 @@ // @namespace binance.orderbook.trade // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E -// @version 2.7.138 +// @version 2.7.139 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -3473,19 +3473,27 @@ import { }; const recordAction = (eventTarget) => { + const contract = findBinanceCancelAllDialog(document, isVisibleElement); + if (!contract) return; + watcher.seenDialog = true; + const action = classifyBinanceCancelAllDialogAction(contract, eventTarget); + if (action && !watcher.action) watcher.action = action; + }; + const rejectInvalidDialogAction = (event, error) => { + // Do not let Binance execute an action that the script can no longer classify or recover from. + event.preventDefault(); + event.stopImmediatePropagation(); + watcher.error = error; + }; + const handleClick = (event) => { try { - const contract = findBinanceCancelAllDialog(document, isVisibleElement); - if (!contract) return; - watcher.seenDialog = true; - const action = classifyBinanceCancelAllDialogAction(contract, eventTarget); - if (action && !watcher.action) watcher.action = action; + recordAction(event.target); } catch (error) { - watcher.error = error; + rejectInvalidDialogAction(event, error); } finally { dialogSignal.notify(); } }; - const handleClick = (event) => recordAction(event.target); const handleKeydown = (event) => { if (event.key !== 'Escape' && event.key !== 'Enter' && event.key !== ' ') return; try { @@ -3499,7 +3507,7 @@ import { ); if (action && !watcher.action) watcher.action = action; } catch (error) { - watcher.error = error; + rejectInvalidDialogAction(event, error); } finally { dialogSignal.notify(); } @@ -3627,11 +3635,8 @@ import { tradingViewOrdersRecoveryPendingAtStartup = false; return { status: 'missing' }; } - if (recovery.status === 'invalid' || recovery.status === 'expired') { - emit( - 'ERR', - `图表当前委托恢复记录${recovery.status === 'invalid' ? '无效' : '已过期'},未修改页面状态`, - ); + if (recovery.status === 'invalid') { + emit('ERR', '图表当前委托恢复记录无效,未修改页面状态'); clearTradingViewOrdersRecoveryRecord(); tradingViewOrdersRecoveryPendingAtStartup = false; return { status: recovery.status }; @@ -3809,9 +3814,8 @@ import { ); } catch (error) { restoreTemporaryUiState = false; - restoreTradingViewOrdersState = false; emit('ERR', '币安撤单确认弹窗结构异常', error); - const message = `${symbol} 撤单确认弹窗结构异常,图表当前委托保持隐藏`; + const message = `${symbol} 撤单确认弹窗结构异常,未执行弹窗操作`; setLadderStatus(message); return { ok: false, status: 'dialog_contract_invalid', message }; } finally { diff --git a/test/unit/binance-orderbook-trade/chart-orders-recovery.test.js b/test/unit/binance-orderbook-trade/chart-orders-recovery.test.js index 6cc046c..dce72df 100644 --- a/test/unit/binance-orderbook-trade/chart-orders-recovery.test.js +++ b/test/unit/binance-orderbook-trade/chart-orders-recovery.test.js @@ -2,7 +2,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { - TRADINGVIEW_ORDERS_RECOVERY_MAX_AGE_MS, createTradingViewOrdersRecoveryRecord, parseTradingViewOrdersRecoveryRecord, } from '../../../src/binance-orderbook-trade/core/tradingview-orders-recovery.js'; @@ -20,7 +19,7 @@ test('creates and parses an exact TradingView orders reload recovery record', () }); }); -test('distinguishes missing, invalid, future, and expired recovery records', () => { +test('distinguishes missing, invalid, and future recovery records', () => { assert.deepEqual(parseTradingViewOrdersRecoveryRecord(null, 2_000), { status: 'missing', record: null, @@ -45,14 +44,16 @@ test('distinguishes missing, invalid, future, and expired recovery records', () status: 'invalid', record: null, }); +}); - const expiredRecord = { version: 1, originalVisible: true, createdAtMs: 1_000 }; +test('valid recovery records do not expire before the hidden chart state is restored', () => { + const oldRecord = { version: 1, originalVisible: true, createdAtMs: 1_000 }; assert.deepEqual(parseTradingViewOrdersRecoveryRecord( - JSON.stringify(expiredRecord), - 1_000 + TRADINGVIEW_ORDERS_RECOVERY_MAX_AGE_MS + 1, + JSON.stringify(oldRecord), + 1_000 + (365 * 24 * 60 * 60 * 1000), ), { - status: 'expired', - record: expiredRecord, + status: 'valid', + record: oldRecord, }); }); diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js index 153aedc..5b76a1f 100644 --- a/test/unit/binance-orderbook-trade/source-regressions.test.js +++ b/test/unit/binance-orderbook-trade/source-regressions.test.js @@ -726,7 +726,7 @@ test('stopping a ladder aborts replacement waits before another cancel or submit assert.match(waitOutcomeBody, /waitForPromiseOrAbort\([\s\S]*abortSignal/); }); -test('bulk cancel hides TradingView orders before opening the native dialog and restores them independently', () => { +test('bulk cancel settles hidden TradingView orders before opening the native dialog', () => { assert.match(tradingViewOrdersSource, /applyOverrides/); assert.match(tradingViewOrdersSource, /tradingProperties\.showOrders/); assert.doesNotMatch(source, /coalesceTradingViewDrawingSaves/); @@ -791,6 +791,8 @@ test('bulk cancel hides TradingView orders before opening the native dialog and test('bulk cancel distinguishes native confirm from cancellation before clear polling', () => { const watcherBody = readFunctionBody('createBinanceCancelAllDialogDecisionWatcher'); + assert.match(watcherBody, /event\.preventDefault\(\)[\s\S]*event\.stopImmediatePropagation\(\)/); + assert.equal(watcherBody.match(/rejectInvalidDialogAction\(event, error\)/g)?.length, 2); assert.match(watcherBody, /document\.addEventListener\('click', handleClick, true\)/); assert.match(watcherBody, /document\.addEventListener\('keydown', handleKeydown, true\)/); assert.match(watcherBody, /findBinanceCancelAllDialog\(document, isVisibleElement\)/); @@ -813,9 +815,15 @@ test('bulk cancel distinguishes native confirm from cancellation before clear po assert.ok(clickIndex < cancelledIndex && cancelledIndex < clearIndex); assert.match(cancelBody, /status: 'dialog_not_found'/); assert.match(cancelBody, /status: 'dialog_contract_invalid'/); + const invalidDialogBranch = cancelBody.match( + /catch \(error\) \{\s*restoreTemporaryUiState = false;([\s\S]*?)status: 'dialog_contract_invalid'/, + )?.[1] || ''; + assert.notEqual(invalidDialogBranch, ''); + assert.doesNotMatch(invalidDialogBranch, /restoreTradingViewOrdersState = false/); + assert.doesNotMatch(invalidDialogBranch, /图表当前委托保持隐藏/); }); -test('TradingView showOrders reload recovery is journaled, bounded, and retried only from startup state', () => { +test('TradingView showOrders reload recovery remains pending until restoration succeeds', () => { assert.match(source, /tradingViewOrdersRecoveryPendingAtStartup =\s*sessionStorage\.getItem\(TRADINGVIEW_ORDERS_RECOVERY_STORAGE_KEY\) !== null/); const hideBody = readFunctionBody('hideTradingViewOrdersForBulkCancel'); @@ -831,7 +839,8 @@ test('TradingView showOrders reload recovery is journaled, bounded, and retried ); const recoverBody = readFunctionBody('recoverChartOrdersStateAfterReload'); - assert.match(recoverBody, /recovery\.status === 'invalid' \|\| recovery\.status === 'expired'/); + assert.match(recoverBody, /recovery\.status === 'invalid'/); + assert.doesNotMatch(recoverBody, /expired/); assert.match(recoverBody, /clearTradingViewOrdersRecoveryRecord\(\)/); assert.match(recoverBody, /findBinanceTradingViewTargetDom\(document\)/); assert.match(recoverBody, /state\.originalVisible = recovery\.record\.originalVisible/); From b3b202eb18962e5cedb87d9a65786ce30622864a Mon Sep 17 00:00:00 2001 From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:57:56 +0800 Subject: [PATCH 2/2] test: cover chart order recovery --- .../specs/cancel-current-symbol.pw.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js b/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js index 2dc2727..669a3ee 100644 --- a/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js +++ b/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js @@ -260,7 +260,7 @@ test('a missing native dialog stops cleanly and restores temporary UI state', as }); for (const dialogMode of ['extraAction', 'missingPrimary']) { - test(`an invalid ${dialogMode} dialog contract fails explicitly and preserves recovery state`, async ({ page }) => { + test(`an invalid ${dialogMode} dialog contract blocks the action and restores chart orders`, async ({ page }) => { const scenario = createCancelScenario({ positions: POSITION_SETS.current, orders: ORDER_SETS.current, @@ -271,12 +271,18 @@ for (const dialogMode of ['extraAction', 'missingPrimary']) { await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByText( - 'HYPEUSDT 撤单确认弹窗结构异常,图表当前委托保持隐藏', + 'HYPEUSDT 撤单确认弹窗结构异常,未执行弹窗操作', )).toBeVisible(); const state = await readFixtureState(page); expect(state.orders).toEqual(ORDER_SETS.current); - expect(state.showOrders).toBe(false); + expect(state.events.filter((event) => event.type === 'cancel-requested')).toEqual([]); + expect( + state.events + .filter((event) => event.type === 'show-orders') + .map((event) => event.value), + ).toEqual([false, true]); + expect(state.showOrders).toBe(true); expect(state.hideOtherSymbols).toBe(true); expect(errors).toEqual([]); });