cfquant 0.2.53__tar.gz → 0.2.55__tar.gz

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.
Files changed (203) hide show
  1. {cfquant-0.2.53 → cfquant-0.2.55}/MANIFEST.in +1 -0
  2. {cfquant-0.2.53 → cfquant-0.2.55}/PKG-INFO +3 -1
  3. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/_editable_install.py +7 -0
  4. cfquant-0.2.55/cfquant/qmt_component_agent.js +249 -0
  5. cfquant-0.2.55/cfquant/qmt_component_login.py +321 -0
  6. cfquant-0.2.55/cfquant/qmt_credentials.py +78 -0
  7. cfquant-0.2.55/cfquant/qmt_dependencies.py +137 -0
  8. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/15_/347/275/221/351/241/265/347/212/266/346/200/201/347/274/223/345/255/230/346/265/213/350/257/225.py +1 -1
  9. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_lite_deployment_paths.py +1 -0
  10. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_management.py +7 -2
  11. cfquant-0.2.55/cfquant/tests/test_qmt_auto_login_browser.py +199 -0
  12. cfquant-0.2.55/cfquant/tests/test_qmt_auto_login_integration.py +356 -0
  13. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_qmt_binding_settings.py +2 -2
  14. cfquant-0.2.55/cfquant/tests/test_qmt_component_login.py +165 -0
  15. cfquant-0.2.55/cfquant/tests/test_qmt_dependencies.py +55 -0
  16. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_qmt_strategy_browser.py +15 -6
  17. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_qmt_strategy_web.py +44 -34
  18. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_setup_qmt_prompt.py +1 -0
  19. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_stock_connect.py +1 -1
  20. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/trading_calendar.py +8 -0
  21. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/version.py +2 -2
  22. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant.egg-info/PKG-INFO +3 -1
  23. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant.egg-info/SOURCES.txt +8 -0
  24. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant.egg-info/requires.txt +4 -0
  25. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant_web_server.py +303 -116
  26. {cfquant-0.2.53 → cfquant-0.2.55}/pyproject.toml +4 -1
  27. {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts/CFQUANT_LITE.py +1 -1
  28. {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_LITE_SH.py +1 -1
  29. {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_LITE_SZ.py +1 -1
  30. {cfquant-0.2.53 → cfquant-0.2.55}/requirements.txt +2 -0
  31. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/app.js +323 -15
  32. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/index.html +53 -35
  33. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/styles.css +42 -1
  34. {cfquant-0.2.53 → cfquant-0.2.55}/LICENSE +0 -0
  35. {cfquant-0.2.53 → cfquant-0.2.55}/LTtx/__init__.py +0 -0
  36. {cfquant-0.2.53 → cfquant-0.2.55}/LTtx/tx/Config.txt +0 -0
  37. {cfquant-0.2.53 → cfquant-0.2.55}/LTtx/tx/LTtx_server.py +0 -0
  38. {cfquant-0.2.53 → cfquant-0.2.55}/LTtx/tx/__init__.py +0 -0
  39. {cfquant-0.2.53 → cfquant-0.2.55}/LTtx/tx/tx.py +0 -0
  40. {cfquant-0.2.53 → cfquant-0.2.55}/README.md +0 -0
  41. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/__init__.py +0 -0
  42. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/account_routing.py +0 -0
  43. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/batch_orders.py +0 -0
  44. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/build_info.py +0 -0
  45. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/callback_dispatcher.py +0 -0
  46. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/cftrader.py +0 -0
  47. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/channels.py +0 -0
  48. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/cli.py +0 -0
  49. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/client.py +0 -0
  50. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/config.py +0 -0
  51. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/diagnostics.py +0 -0
  52. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/flow_control.py +0 -0
  53. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/hub.py +0 -0
  54. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/level2.py +0 -0
  55. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/log_management.py +0 -0
  56. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/logging_i18n.py +0 -0
  57. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/management.py +0 -0
  58. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/normal_bridge.py +0 -0
  59. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/order_identity.py +0 -0
  60. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/order_meta.py +0 -0
  61. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/pipe_bridge.py +0 -0
  62. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/pipe_client.py +0 -0
  63. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/pipe_hub.py +0 -0
  64. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/pipe_transport.py +0 -0
  65. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/protocol.py +0 -0
  66. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/qmt_binding_settings.py +0 -0
  67. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/qmt_bridge.py +0 -0
  68. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/qmt_pipe_runtime.py +0 -0
  69. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/qmt_strategy_deploy.py +0 -0
  70. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/qmt_strategy_package.py +0 -0
  71. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/qmt_strategy_runtime.py +0 -0
  72. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/quote_history.py +0 -0
  73. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/rpc_timing.py +0 -0
  74. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/runtime_report.py +0 -0
  75. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/stock_connect.py +0 -0
  76. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/10_/345/221/275/344/273/244/350/241/214/346/216/245/345/217/243/346/265/213/350/257/225.py" +0 -0
  77. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/11_/346/231/256/351/200/232/346/241/245/346/216/245/346/264/276/345/217/221/346/265/213/350/257/225.py" +0 -0
  78. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
  79. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/13_/347/256/241/351/201/223/344/270/255/345/277/203/350/277/236/346/216/245/346/265/213/350/257/225.py" +0 -0
  80. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/14_/350/277/220/350/241/214/346/227/266/346/212/245/345/221/212/346/265/213/350/257/225.py" +0 -0
  81. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
  82. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/17_/345/244/247QMT/346/216/245/345/217/243/351/200/202/351/205/215/346/265/213/350/257/225.py" +0 -0
  83. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/18_/351/253/230/347/272/247/346/250/241/345/274/217/345/256/236/346/234/272/350/201/224/350/260/203.py" +0 -0
  84. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/19_/345/256/236/346/234/272/350/201/224/350/260/203/345/210/244/345/256/232/346/265/213/350/257/225.py" +0 -0
  85. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
  86. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/20_/350/201/224/350/260/203/351/227/256/351/242/230/344/277/256/345/244/215/346/265/213/350/257/225.py" +0 -0
  87. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/21_/345/215/225/350/202/241/350/241/214/346/203/205/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  88. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
  89. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
  90. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/24_/350/264/246/346/210/267/347/274/223/345/255/230/346/214/211/351/234/200/345/210/267/346/226/260/346/265/213/350/257/225.py" +0 -0
  91. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/25_/350/264/246/346/210/267/347/274/223/345/255/230/351/241/265/351/235/242/346/265/213/350/257/225.py" +0 -0
  92. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
  93. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/27_/345/205/250/346/216/250/350/256/242/351/230/205/351/241/265/351/235/242/346/265/213/350/257/225.py" +0 -0
  94. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/28_Level2/346/216/245/345/217/243/351/200/202/351/205/215/346/265/213/350/257/225.py" +0 -0
  95. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
  96. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
  97. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/30_cftrader/346/211/271/351/207/217/344/270/213/345/215/225/346/200/247/350/203/275/345/237/272/345/207/206.py" +0 -0
  98. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/31_cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  99. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  100. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/33_/345/220/214/346/255/245/344/270/213/345/215/225/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  101. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/34_/345/205/250/346/216/250/345/206/205/351/203/250/345/273/266/350/277/237/346/265/213/350/257/225.py" +0 -0
  102. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
  103. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/4_/344/272/244/346/230/223/345/247/224/346/211/230/346/237/245/350/257/242/346/265/213/350/257/225.py" +0 -0
  104. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  105. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
  106. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/7_/345/220/214/346/255/245/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
  107. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/8_/346/210/220/344/272/244/350/256/242/345/215/225/345/222/214/345/247/224/346/211/230/350/256/242/345/215/225/346/237/245/350/257/242/346/265/213/350/257/225.py" +0 -0
  108. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
  109. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/_helpers.py +0 -0
  110. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/log/cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225_20260911_024617.md" +0 -0
  111. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/log/cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225_20260921_175732.md" +0 -0
  112. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/readme.md +0 -0
  113. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_advanced_parity.py +0 -0
  114. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_api_tester_browser.py +0 -0
  115. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_api_tester_web.py +0 -0
  116. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
  117. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_binding_runtime_browser.py +0 -0
  118. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_callback_requests.py +0 -0
  119. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_cftrader.py +0 -0
  120. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_cftrader_tutorial.py +0 -0
  121. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_cftrader_web.py +0 -0
  122. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_credit_compat.py +0 -0
  123. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_dataframe_encoding.py +0 -0
  124. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_json_serialization.py +0 -0
  125. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_latency_flow_control.py +0 -0
  126. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_lite_account_routing.py +0 -0
  127. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_lite_parity.py +0 -0
  128. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
  129. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_local_trading_calendar.py +0 -0
  130. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_log_management.py +0 -0
  131. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_management_batch.py +0 -0
  132. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_market_data_fallback.py +0 -0
  133. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_market_timing.py +0 -0
  134. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_order_identity.py +0 -0
  135. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_order_time_callback.py +0 -0
  136. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_project_updater.py +0 -0
  137. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_python_api_reference.py +0 -0
  138. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_qmt_fileless.py +0 -0
  139. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
  140. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_quote_latency.py +0 -0
  141. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_quote_latency_encoding.py +0 -0
  142. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_startup_recovery.py +0 -0
  143. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_stock_connect_browser.py +0 -0
  144. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_table_conversion.py +0 -0
  145. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_trade_strategy_callback.py +0 -0
  146. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_trading_dates.py +0 -0
  147. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_tutorial_reader.py +0 -0
  148. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_tx_auto_import.py +0 -0
  149. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tests/test_tx_shutdown.py +0 -0
  150. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tx.py +0 -0
  151. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/tx_trade_bridge.py +0 -0
  152. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/xtconstant.py +0 -0
  153. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/xtdata.py +0 -0
  154. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/xttrader.py +0 -0
  155. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant/xttype.py +0 -0
  156. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant.egg-info/dependency_links.txt +0 -0
  157. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant.egg-info/entry_points.txt +0 -0
  158. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant.egg-info/top_level.txt +0 -0
  159. {cfquant-0.2.53 → cfquant-0.2.55}/cfquant_pipe_hub.py +0 -0
  160. {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts/CFQUANT.py +0 -0
  161. {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
  162. {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
  163. {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts/__init__.py +0 -0
  164. {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_CTYPE_ALL_LOWLAT_SH.py" +0 -0
  165. {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_CTYPE_ALL_LOWLAT_SZ.py" +0 -0
  166. {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_TRADE_LOWLAT_SH.py" +0 -0
  167. {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_TRADE_LOWLAT_SZ.py" +0 -0
  168. {cfquant-0.2.53 → cfquant-0.2.55}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/readme.md" +0 -0
  169. {cfquant-0.2.53 → cfquant-0.2.55}/setup.cfg +0 -0
  170. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/__init__.py +0 -0
  171. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/api-tester.js +0 -0
  172. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/amber-pulse.svg +0 -0
  173. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/copper-grid.svg +0 -0
  174. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/market-blue.svg +0 -0
  175. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/rose-circuit.svg +0 -0
  176. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/signal-green.svg +0 -0
  177. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/slate-wave.svg +0 -0
  178. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/teal-orbit.svg +0 -0
  179. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/avatars/violet-node.svg +0 -0
  180. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/cftrader-api-data.js +0 -0
  181. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/docs.js +0 -0
  182. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/favicon.svg +0 -0
  183. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
  184. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
  185. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
  186. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
  187. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
  188. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
  189. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
  190. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
  191. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
  192. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
  193. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
  194. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
  195. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
  196. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
  197. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
  198. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
  199. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
  200. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
  201. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/python-api-data.js +0 -0
  202. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/python-api.js +0 -0
  203. {cfquant-0.2.53 → cfquant-0.2.55}/web_dashboard/vendor/mermaid.min.js +0 -0
@@ -4,6 +4,7 @@ include cfquant_web_server.py
4
4
  include cfquant_pipe_hub.py
5
5
 
6
6
  recursive-include cfquant *.py
7
+ include cfquant/qmt_component_agent.js
7
8
  recursive-include cfquant/tests *.py *.md
8
9
  recursive-include LTtx *.py
9
10
  include LTtx/tx/Config.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cfquant
3
- Version: 0.2.53
3
+ Version: 0.2.55
4
4
  Summary: QMT local bridge and xtquant-compatible Python client.
5
5
  License: MIT
6
6
  Requires-Python: >=3.8
@@ -14,6 +14,8 @@ Requires-Dist: pytz
14
14
  Requires-Dist: tabulate
15
15
  Requires-Dist: pandas-market-calendars
16
16
  Requires-Dist: importlib-resources>=5
17
+ Requires-Dist: frida<18,>=17.0; sys_platform == "win32"
18
+ Requires-Dist: pywin32>=306; sys_platform == "win32"
17
19
  Provides-Extra: dev
18
20
  Requires-Dist: build>=1.0; extra == "dev"
19
21
  Requires-Dist: twine>=5.0; extra == "dev"
@@ -9,6 +9,11 @@ import sys
9
9
  import tempfile
10
10
  from pathlib import Path
11
11
 
12
+ if __package__:
13
+ from .qmt_dependencies import report_startup_dependencies
14
+ else:
15
+ from qmt_dependencies import report_startup_dependencies
16
+
12
17
 
13
18
  PACKAGE_NAME = "cfquant"
14
19
 
@@ -503,6 +508,7 @@ def ensure_cfquant_installed(project_root, python_exe=None):
503
508
  print("cfquant installed=%s project=%s" % (installed or "missing", expected))
504
509
  if installed == expected:
505
510
  print("Package versions match; skip installation.")
511
+ report_startup_dependencies(python_exe)
506
512
  return 0
507
513
 
508
514
  command = editable_install_args(project_root, python_exe)
@@ -526,6 +532,7 @@ def ensure_cfquant_installed(project_root, python_exe=None):
526
532
  print("[ERROR] Installed cfquant version does not match the project after installation.", file=sys.stderr)
527
533
  return 1
528
534
  print("%s package is installed." % PACKAGE_NAME)
535
+ report_startup_dependencies(python_exe)
529
536
  return 0
530
537
 
531
538
 
@@ -0,0 +1,249 @@
1
+ 'use strict';
2
+
3
+ const core = Process.getModuleByName('Qt5Core.dll');
4
+ const widgets = Process.getModuleByName('Qt5Widgets.dll');
5
+ const user32 = Process.getModuleByName('user32.dll');
6
+ const fn = (mod, name, ret, args) => new NativeFunction(mod.getExportByName(name), ret, args);
7
+ const findWidget = fn(widgets, '?find@QWidget@@SAPEAV1@_K@Z', 'pointer', ['uint64']);
8
+ const children = fn(core, '?children@QObject@@QEBAAEBV?$QList@PEAVQObject@@@@XZ', 'pointer', ['pointer']);
9
+ const inherits = fn(core, '?inherits@QObject@@QEBA_NPEBD@Z', 'bool', ['pointer', 'pointer']);
10
+ const className = fn(core, '?className@QMetaObject@@QEBAPEBDXZ', 'pointer', ['pointer']);
11
+ const isVisible = fn(widgets, '?isVisible@QWidget@@QEBA_NXZ', 'bool', ['pointer']);
12
+ const isEnabled = fn(widgets, '?isEnabled@QWidget@@QEBA_NXZ', 'bool', ['pointer']);
13
+ const echoMode = fn(widgets, '?echoMode@QLineEdit@@QEBA?AW4EchoMode@1@XZ', 'int', ['pointer']);
14
+ const setText = fn(widgets, '?setText@QLineEdit@@QEAAXAEBVQString@@@Z', 'void', ['pointer', 'pointer']);
15
+ const stringCtor = fn(core, '??0QString@@QEAA@PEBVQChar@@H@Z', 'pointer', ['pointer', 'pointer', 'int']);
16
+ const stringDtor = fn(core, '??1QString@@QEAA@XZ', 'void', ['pointer']);
17
+ const stringSize = fn(core, '?size@QString@@QEBAHXZ', 'int', ['pointer']);
18
+ const stringData = fn(core, '?utf16@QString@@QEBAPEBGXZ', 'pointer', ['pointer']);
19
+ const buttonText = fn(widgets, '?text@QAbstractButton@@QEBA?AVQString@@XZ', 'pointer', ['pointer', 'pointer']);
20
+ const editText = fn(widgets, '?text@QLineEdit@@QEBA?AVQString@@XZ', 'pointer', ['pointer', 'pointer']);
21
+ const labelText = fn(widgets, '?text@QLabel@@QEBA?AVQString@@XZ', 'pointer', ['pointer', 'pointer']);
22
+ const buttonClick = fn(widgets, '?click@QAbstractButton@@QEAAXXZ', 'void', ['pointer']);
23
+ const closeWidget = fn(widgets, '?close@QWidget@@QEAA_NXZ', 'bool', ['pointer']);
24
+ const comboEditable = fn(widgets, '?isEditable@QComboBox@@QEBA_NXZ', 'bool', ['pointer']);
25
+ const comboSetText = fn(widgets, '?setEditText@QComboBox@@QEAAXAEBVQString@@@Z', 'void', ['pointer', 'pointer']);
26
+ const getThread = fn(user32, 'GetWindowThreadProcessId', 'uint32', ['pointer', 'pointer']);
27
+ const postMessage = fn(user32, 'PostMessageW', 'bool', ['pointer', 'uint32', 'pointer', 'pointer']);
28
+ const typeNames = new Map();
29
+ let pending = null;
30
+
31
+ function hasType(object, name) {
32
+ if (!typeNames.has(name)) typeNames.set(name, Memory.allocUtf8String(name));
33
+ return inherits(object, typeNames.get(name));
34
+ }
35
+
36
+ function typeName(object) {
37
+ const metaFn = new NativeFunction(object.readPointer().readPointer(), 'pointer', ['pointer']);
38
+ return className(metaFn(object)).readUtf8String();
39
+ }
40
+
41
+ function childObjects(object) {
42
+ const d = children(object).readPointer();
43
+ const begin = d.add(8).readS32(), end = d.add(12).readS32();
44
+ if (begin < 0 || end < begin || end - begin > 5000) throw new Error('Unexpected QList layout');
45
+ const result = [];
46
+ for (let i = begin; i < end; i++) result.push(d.add(16 + i * Process.pointerSize).readPointer());
47
+ return result;
48
+ }
49
+
50
+ function label(object) {
51
+ const result = Memory.alloc(Process.pointerSize);
52
+ buttonText(object, result);
53
+ try { return stringData(result).readUtf16String(stringSize(result)); }
54
+ finally { stringDtor(result); }
55
+ }
56
+
57
+ function walk(root) {
58
+ const result = [], queue = [{object: root, parent: null}], seen = new Set();
59
+ while (queue.length) {
60
+ const item = queue.shift(), object = item.object;
61
+ if (object.isNull() || seen.has(object.toString())) continue;
62
+ seen.add(object.toString());
63
+ if (seen.size > 5000) throw new Error('Object tree too large');
64
+ const isWidget = hasType(object, 'QWidget');
65
+ const row = {address: object.toString(), parent: item.parent, class_name: typeName(object)};
66
+ if (isWidget) {
67
+ row.visible = isVisible(object);
68
+ row.enabled = isEnabled(object);
69
+ if (hasType(object, 'QLineEdit')) row.echo_mode = echoMode(object);
70
+ if (hasType(object, 'QAbstractButton')) row.label = label(object);
71
+ }
72
+ result.push(row);
73
+ childObjects(object).forEach(child => queue.push({object: child, parent: object.toString()}));
74
+ }
75
+ return result;
76
+ }
77
+
78
+ function getRoot(hwnd) {
79
+ const root = findWidget(uint64(hwnd));
80
+ if (root.isNull()) throw new Error('No Qt widget for target window');
81
+ return root;
82
+ }
83
+
84
+ function passwordEditor(root, requireEnabled = true) {
85
+ if (!hasType(root, 'TC::CLoginDialog')) throw new Error('Not a QMT login dialog');
86
+ const rows = walk(root);
87
+ const edits = rows.filter(row => row.echo_mode === 2 && row.visible && (!requireEnabled || row.enabled));
88
+ if (edits.length !== 1) throw new Error('Expected exactly one visible password editor');
89
+ return {rows, editor: ptr(edits[0].address)};
90
+ }
91
+
92
+ function hasText(editor) {
93
+ const value = Memory.alloc(Process.pointerSize);
94
+ editText(editor, value);
95
+ // Only return emptiness; never copy a remembered password out of the process.
96
+ try { return stringSize(value) > 0; }
97
+ finally { stringDtor(value); }
98
+ }
99
+
100
+ function noticeKind(rows) {
101
+ let credential = false, network = false;
102
+ for (const row of rows) {
103
+ const object = ptr(row.address);
104
+ if (!row.visible || !hasType(object, 'QLabel')) continue;
105
+ const value = Memory.alloc(Process.pointerSize);
106
+ labelText(object, value);
107
+ let text;
108
+ try { text = stringData(value).readUtf16String(stringSize(value)); }
109
+ finally { stringDtor(value); }
110
+ if (/验证码|动态口令|短信验证|captcha/i.test(text)) return 'challenge';
111
+ if (/密码.{0,12}(错误|不正确|失败|无效)|口令.{0,12}(错误|不正确|失败)|password.{0,16}(incorrect|invalid|wrong)/i.test(text)) credential = true;
112
+ if (/(网络|连接|服务器).{0,16}(失败|超时|异常)|network.{0,16}(error|timeout)/i.test(text)) network = true;
113
+ }
114
+ return credential ? 'credential_error' : network ? 'network_error' : null;
115
+ }
116
+
117
+ function submit(root) {
118
+ passwordEditor(root);
119
+ const login = fn(Process.getModuleByName('LoginDialog.dll'),
120
+ '?slot_loginClicked@CLoginDialog@TC@@QEAAXXZ', 'void', ['pointer']);
121
+ login(root);
122
+ }
123
+
124
+ function loginIdentity(root) {
125
+ const getName = fn(Process.getModuleByName('LoginDialog.dll'),
126
+ '?getUserName@CLoginDialog@TC@@IEAA?AVQString@@XZ', 'pointer', ['pointer', 'pointer']);
127
+ const value = Memory.alloc(Process.pointerSize);
128
+ getName(root, value);
129
+ try { return stringData(value).readUtf16String(stringSize(value)); }
130
+ finally { stringDtor(value); }
131
+ }
132
+
133
+ function onGui(hwnd, operation) {
134
+ if (pending) throw new Error('An operation is already pending');
135
+ const thread = getThread(ptr(hwnd), NULL);
136
+ if (!thread) throw new Error('Target window no longer exists');
137
+ return new Promise((resolve, reject) => {
138
+ const timer = setTimeout(() => {
139
+ if (pending && pending.resolve === resolve) {
140
+ pending = null;
141
+ reject(new Error('GUI operation timeout'));
142
+ }
143
+ }, 8000);
144
+ pending = {thread, operation, resolve, reject, timer};
145
+ // Wake the normal message loop without mouse/keyboard events or foreground changes.
146
+ postMessage(ptr(hwnd), 0, NULL, NULL);
147
+ });
148
+ }
149
+
150
+ Interceptor.attach(user32.getExportByName('DispatchMessageW'), {
151
+ onLeave() {
152
+ if (!pending || this.threadId !== pending.thread) return;
153
+ const job = pending;
154
+ pending = null;
155
+ clearTimeout(job.timer);
156
+ try { job.resolve({gui_thread: this.threadId, result: job.operation()}); }
157
+ catch (error) { job.reject(error); }
158
+ }
159
+ });
160
+
161
+ rpc.exports = {
162
+ selectaccount(hwnd, account, replace = false) {
163
+ return onGui(hwnd, () => {
164
+ const root = getRoot(hwnd);
165
+ passwordEditor(root);
166
+ if (!replace && loginIdentity(root) === account) return {changed: false};
167
+ const candidates = walk(root).filter(row => row.visible && row.enabled &&
168
+ hasType(ptr(row.address), 'QComboBox') && comboEditable(ptr(row.address)));
169
+ if (candidates.length !== 1) throw new Error('No unique editable account selector');
170
+ if (replace) {
171
+ const empty = Memory.alloc(Process.pointerSize);
172
+ stringCtor(empty, Memory.allocUtf16String(''), 0);
173
+ try { comboSetText(ptr(candidates[0].address), empty); }
174
+ finally { stringDtor(empty); }
175
+ }
176
+ const data = Memory.allocUtf16String(account), value = Memory.alloc(Process.pointerSize);
177
+ stringCtor(value, data, account.length);
178
+ try { comboSetText(ptr(candidates[0].address), value); }
179
+ finally { stringDtor(value); }
180
+ if (loginIdentity(root) !== account) throw new Error('Could not select configured login account');
181
+ return {changed: true};
182
+ });
183
+ },
184
+ inspect(hwnd) { return onGui(hwnd, () => walk(getRoot(hwnd))); },
185
+ status(hwnd) {
186
+ return onGui(hwnd, () => {
187
+ const root = getRoot(hwnd);
188
+ if (!hasType(root, 'TC::CLoginDialog')) return {is_login: false, is_main: hasType(root, 'TC::CMainWindow')};
189
+ const rows = walk(root);
190
+ const edits = rows.filter(row => row.echo_mode === 2 && row.visible);
191
+ if (edits.length > 1) throw new Error('Multiple password editors are visible');
192
+ return {is_login: true, has_password: edits.length === 1 && hasText(ptr(edits[0].address)),
193
+ busy: edits.length === 0, login_identity: loginIdentity(root), notice: noticeKind(rows),
194
+ login_enabled: rows.some(row => row.visible && row.enabled && /^登\s*录(?:[((]\d+[))])?$/.test(row.label || ''))};
195
+ });
196
+ },
197
+ notice(hwnd) {
198
+ return onGui(hwnd, () => {
199
+ const rows = walk(getRoot(hwnd));
200
+ return {kind: noticeKind(rows)};
201
+ });
202
+ },
203
+ acknowledge(hwnd) {
204
+ return onGui(hwnd, () => {
205
+ const rows = walk(getRoot(hwnd));
206
+ if (noticeKind(rows) !== 'credential_error') throw new Error('Not a recognized credential failure');
207
+ const buttons = rows.filter(row => row.visible && row.enabled && /^(确定|确认|OK|&OK)$/.test(row.label || ''));
208
+ if (buttons.length !== 1) throw new Error('No unique acknowledgement button');
209
+ buttonClick(ptr(buttons[0].address));
210
+ return {acknowledged: true};
211
+ });
212
+ },
213
+ submit(hwnd) { return onGui(hwnd, () => { submit(getRoot(hwnd)); return {submitted: true}; }); },
214
+ close(hwnd) { return onGui(hwnd, () => ({closed: closeWidget(getRoot(hwnd))})); },
215
+ clicklabel(hwnd, text) {
216
+ return onGui(hwnd, () => {
217
+ const rows = walk(getRoot(hwnd)).filter(row => row.label === text && row.visible && row.enabled);
218
+ if (rows.length !== 1) throw new Error('Expected one matching enabled button');
219
+ buttonClick(ptr(rows[0].address));
220
+ return {clicked: true};
221
+ });
222
+ },
223
+ fill(hwnd, value, submit) {
224
+ return onGui(hwnd, () => {
225
+ const root = getRoot(hwnd);
226
+ if (!hasType(root, 'TC::CLoginDialog')) throw new Error('Not a QMT login dialog');
227
+ const rows = walk(root);
228
+ const edits = rows.filter(row => row.echo_mode === 2 && row.visible && row.enabled);
229
+ if (edits.length !== 1) throw new Error('Expected exactly one visible password editor');
230
+ const text = Memory.allocUtf16String(value);
231
+ const string = Memory.alloc(Process.pointerSize);
232
+ stringCtor(string, text, value.length);
233
+ try { setText(ptr(edits[0].address), string); }
234
+ finally {
235
+ stringDtor(string);
236
+ text.writeByteArray(new Uint8Array((value.length + 1) * 2));
237
+ }
238
+ const count = value.length;
239
+ value = null;
240
+ if (submit) {
241
+ const login = fn(Process.getModuleByName('LoginDialog.dll'),
242
+ '?slot_loginClicked@CLoginDialog@TC@@QEAAXXZ', 'void', ['pointer']);
243
+ login(root);
244
+ }
245
+ return {filled: true, submitted: Boolean(submit), characters: count,
246
+ input_method: 'QLineEdit::setText', login_method: submit ? 'CLoginDialog::slot_loginClicked' : null};
247
+ });
248
+ }
249
+ };
@@ -0,0 +1,321 @@
1
+ """Explicit QMT component login for a configured binding (Windows, Qt 5.6.2 x64).
2
+
3
+ Run with --binding NAME. Password is requested only if needed, using --prompt
4
+ or stdin. This tool never starts/stops QMT; it replaces the login form contents.
5
+ The web auto-start service also uses login_running_qmt after starting a process.
6
+ """
7
+ import argparse
8
+ import getpass
9
+ import json
10
+ from pathlib import Path
11
+ import sys
12
+ import time
13
+
14
+
15
+ class QmtNotReady(RuntimeError):
16
+ pass
17
+
18
+
19
+ def verify_environment(directory):
20
+ """Validate dependencies and native ABI before stopping a running terminal."""
21
+ try:
22
+ from .qmt_dependencies import LOGIN_MODULES, import_login_module
23
+ except ImportError:
24
+ from qmt_dependencies import LOGIN_MODULES, import_login_module
25
+ modules = {name: import_login_module(name) for name in LOGIN_MODULES}
26
+ win32api = modules["win32api"]
27
+ directory = Path(directory)
28
+ for module in ("Qt5Core.dll", "Qt5Widgets.dll"):
29
+ version = win32api.GetFileVersionInfo(str(directory / module), "\\")
30
+ if (version["FileVersionMS"], version["FileVersionLS"]) != ((5 << 16) | 6, 2 << 16):
31
+ raise RuntimeError("QMT 组件自动登录当前仅支持 Qt 5.6.2,请先适配此客户端版本")
32
+ with (directory / "XtItClient.exe").open("rb") as stream:
33
+ stream.seek(0x3C)
34
+ offset = int.from_bytes(stream.read(4), "little")
35
+ stream.seek(offset + 4)
36
+ if int.from_bytes(stream.read(2), "little") != 0x8664:
37
+ raise RuntimeError("QMT 组件自动登录需要 64 位客户端")
38
+
39
+
40
+ def auto_login(backend, password_provider):
41
+ """Replace existing password contents and submit the supplied credential once."""
42
+ attempts = []
43
+ password_requested = False
44
+
45
+ def result(outcome):
46
+ return {"outcome": outcome, "login_succeeded": outcome in ("logged_in", "already_logged_in"),
47
+ "attempts": attempts, "password_requested": password_requested}
48
+
49
+ if backend.already_logged_in():
50
+ return result("already_logged_in")
51
+ state = backend.state()
52
+ if state.get("busy"):
53
+ return result("login_not_ready")
54
+ if state.get("notice") in ("challenge", "network_error"):
55
+ return result(state["notice"])
56
+ if state.get("notice") == "credential_error":
57
+ backend.acknowledge_failure()
58
+ password_requested = True
59
+ password = password_provider()
60
+ if not password:
61
+ return result("password_required")
62
+ try:
63
+ backend.fill(password)
64
+ finally:
65
+ password = None
66
+ if not backend.state()["login_enabled"]:
67
+ return result("login_not_ready")
68
+ backend.submit()
69
+ attempts.append("provided")
70
+ return result(backend.wait_outcome())
71
+
72
+
73
+ def select_binding(config_path, name):
74
+ config = json.loads(Path(config_path).read_text(encoding="utf-8"))
75
+ rows = config.get("account_configs") or {}
76
+ rows = rows.values() if isinstance(rows, dict) else rows
77
+ matches = [row for row in rows if row.get("display_name") == name]
78
+ if len(matches) != 1:
79
+ raise ValueError("Expected exactly one binding with that display name")
80
+ root = Path(matches[0]["qmt_dir"]).resolve()
81
+ for directory in (root, root / "bin.x64"):
82
+ if (directory / "XtItClient.exe").is_file():
83
+ return matches[0], directory
84
+ raise ValueError("Binding directory does not contain XtItClient.exe")
85
+
86
+
87
+ class QtLoginBackend:
88
+ def __init__(self, directory, timeout=40, expected_pid=None):
89
+ try:
90
+ from .qmt_dependencies import import_login_module
91
+ except ImportError:
92
+ from qmt_dependencies import import_login_module
93
+ import psutil
94
+ frida = import_login_module("frida")
95
+ win32api = import_login_module("win32api")
96
+ win32event = import_login_module("win32event")
97
+ win32gui = import_login_module("win32gui")
98
+ win32process = import_login_module("win32process")
99
+ self.gui, self.process = win32gui, win32process
100
+ self.timeout = timeout
101
+ self.hwnd = None
102
+ self.identity = None
103
+ self.failure_hwnd = None
104
+ self.session = None
105
+ self.mutex = None
106
+ self.messages = []
107
+ self.foreground_before = win32gui.GetForegroundWindow()
108
+ expected = (directory / "XtItClient.exe").resolve()
109
+ matches = []
110
+ for process in psutil.process_iter(["name"]):
111
+ try:
112
+ if (process.info["name"] or "").lower() == "xtitclient.exe" and Path(process.exe()).resolve() == expected:
113
+ matches.append(process)
114
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
115
+ continue
116
+ if not matches:
117
+ raise QmtNotReady("QMT process is not ready")
118
+ if len(matches) != 1:
119
+ raise RuntimeError("Expected exactly one running QMT for this binding directory")
120
+ self.pid = matches[0].pid
121
+ if expected_pid is not None and self.pid != expected_pid:
122
+ raise RuntimeError("QMT process changed; stopping this login attempt")
123
+ verify_environment(directory)
124
+ self.mutex = win32event.CreateMutex(None, False, "Local\\CFQuantQmtComponentLogin_" + str(self.pid))
125
+ acquired = win32event.WaitForSingleObject(self.mutex, 0)
126
+ if acquired not in (0, 0x80):
127
+ win32api.CloseHandle(self.mutex)
128
+ self.mutex = None
129
+ raise RuntimeError("Another component login is already running for this QMT")
130
+ try:
131
+ self.session = frida.attach(self.pid)
132
+ readiness = self.session.create_script("rpc.exports.ready = () => ['Qt5Core.dll', 'Qt5Widgets.dll', 'LoginDialog.dll'].every(n => Process.findModuleByName(n) !== null);")
133
+ readiness.load()
134
+ ready = readiness.exports_sync.ready()
135
+ readiness.unload()
136
+ if not ready:
137
+ raise QmtNotReady("QMT login modules are still loading")
138
+ script = self.session.create_script(Path(__file__).with_name("qmt_component_agent.js").read_text(encoding="utf-8"))
139
+ script.on("message", lambda message, data: self.messages.append("agent_error") if message.get("type") == "error" else None)
140
+ script.load()
141
+ self.script, self.api = script, script.exports_sync
142
+ self.existing_main = False
143
+ self.main_hwnd = None
144
+ for hwnd in self.windows():
145
+ state = self.api.status(str(hwnd))["result"]
146
+ if state.get("is_login"):
147
+ if self.hwnd is not None:
148
+ raise RuntimeError("Multiple login dialogs found")
149
+ self.hwnd = hwnd
150
+ self.identity = state["login_identity"]
151
+ elif state.get("is_main"):
152
+ self.existing_main = True
153
+ self.main_hwnd = hwnd
154
+ if self.hwnd is None and not self.existing_main:
155
+ raise QmtNotReady("No supported login or main window found yet")
156
+ self.minimized_before = bool(self.hwnd and self.gui.IsIconic(self.hwnd))
157
+ except Exception:
158
+ self.close()
159
+ raise
160
+
161
+ def windows(self):
162
+ result = []
163
+ def visit(hwnd, _):
164
+ if self.process.GetWindowThreadProcessId(hwnd)[1] == self.pid and self.gui.IsWindowVisible(hwnd) and self.gui.GetClassName(hwnd).startswith("Qt"):
165
+ result.append(hwnd)
166
+ self.gui.EnumWindows(visit, None)
167
+ return result
168
+
169
+ def already_logged_in(self):
170
+ return self.existing_main and self.hwnd is None
171
+
172
+ def state(self):
173
+ state = self.api.status(str(self.hwnd))["result"]
174
+ if not state.get("is_login") or not self.identity or state.get("login_identity") != self.identity:
175
+ raise RuntimeError("Login window or selected account changed; stopping")
176
+ state.pop("login_identity", None)
177
+ for hwnd in self.windows():
178
+ if hwnd != self.hwnd and hwnd != self.main_hwnd:
179
+ kind = self.api.notice(str(hwnd))["result"].get("kind")
180
+ if kind:
181
+ state["notice"] = kind
182
+ self.failure_hwnd = hwnd
183
+ break
184
+ return state
185
+
186
+ def select_account(self, account, replace=False):
187
+ if self.already_logged_in():
188
+ if account and not self.gui.GetWindowText(self.main_hwnd).startswith(account + " - "):
189
+ raise RuntimeError("QMT 已登录的用户与配置的登录账号不一致,请重启该绑定 QMT")
190
+ return
191
+ if account and (replace or account != self.identity):
192
+ self.api.selectaccount(str(self.hwnd), account, replace)
193
+ self.identity = account
194
+ # Managed login clears remembered contents even for the same user.
195
+ self.api.fill(str(self.hwnd), "", False)
196
+
197
+ def submit(self):
198
+ state = self.state()
199
+ if not state["has_password"] or not state["login_enabled"]:
200
+ raise RuntimeError("Login is not ready")
201
+ self.api.submit(str(self.hwnd))
202
+
203
+ def fill(self, password):
204
+ self.state()
205
+ self.api.fill(str(self.hwnd), password, False)
206
+
207
+ def wait_outcome(self):
208
+ deadline = time.monotonic() + self.timeout
209
+ while time.monotonic() < deadline:
210
+ windows = self.windows()
211
+ for hwnd in windows:
212
+ caption = self.gui.GetWindowText(hwnd)
213
+ if self.identity and caption.startswith(self.identity + " - "):
214
+ if self.api.status(str(hwnd))["result"].get("is_main"):
215
+ return "logged_in"
216
+ for hwnd in windows:
217
+ if hwnd == self.hwnd:
218
+ try:
219
+ state = self.state()
220
+ except Exception:
221
+ if not self.gui.IsWindow(hwnd) or not self.gui.IsWindowVisible(hwnd):
222
+ continue
223
+ raise
224
+ kind = state.get("notice")
225
+ if not state["login_enabled"]:
226
+ continue
227
+ else:
228
+ kind = self.api.notice(str(hwnd))["result"].get("kind")
229
+ if kind:
230
+ self.failure_hwnd = hwnd
231
+ return kind
232
+ time.sleep(0.5)
233
+ # A slow connection is not evidence of a wrong password. Never resubmit.
234
+ return "timeout"
235
+
236
+ def acknowledge_failure(self):
237
+ if self.failure_hwnd and self.failure_hwnd != self.hwnd:
238
+ self.api.acknowledge(str(self.failure_hwnd))
239
+ self.failure_hwnd = None
240
+
241
+ def close(self):
242
+ try:
243
+ from .qmt_dependencies import import_login_module
244
+ except ImportError:
245
+ from qmt_dependencies import import_login_module
246
+ win32api = import_login_module("win32api")
247
+ win32event = import_login_module("win32event")
248
+ try:
249
+ if self.session:
250
+ self.session.detach()
251
+ self.session = None
252
+ finally:
253
+ if self.mutex:
254
+ win32event.ReleaseMutex(self.mutex)
255
+ win32api.CloseHandle(self.mutex)
256
+ self.mutex = None
257
+
258
+
259
+ def main():
260
+ if hasattr(sys.stdout, "reconfigure"):
261
+ sys.stdout.reconfigure(encoding="utf-8")
262
+ parser = argparse.ArgumentParser(description=__doc__)
263
+ parser.add_argument("--binding", required=True)
264
+ parser.add_argument("--config", type=Path, default=Path(__file__).resolve().parents[1] / "runtime/config/cfquant_web_config.json")
265
+ parser.add_argument("--prompt", action="store_true", help="Read a needed password without echo; otherwise read one line of stdin")
266
+ parser.add_argument("--timeout", type=float, default=40)
267
+ parser.add_argument("--report", type=Path)
268
+ args = parser.parse_args()
269
+ if not 1 <= args.timeout <= 120:
270
+ parser.error("timeout must be between 1 and 120 seconds")
271
+ row, directory = select_binding(args.config, args.binding)
272
+ backend = QtLoginBackend(directory, args.timeout)
273
+ try:
274
+ backend.select_account((row.get("qmt_auto_login") or {}).get("login_account", ""), replace=True)
275
+ def read_password():
276
+ return getpass.getpass("QMT password (hidden): ") if args.prompt else sys.stdin.readline().rstrip("\r\n")
277
+ result = auto_login(backend, read_password)
278
+ result.update(binding=args.binding, process_id=backend.pid,
279
+ minimized_before=backend.minimized_before,
280
+ foreground_before=backend.foreground_before,
281
+ foreground_after=backend.gui.GetForegroundWindow(),
282
+ script_errors=backend.messages,
283
+ input_method="Qt native methods; no keyboard/mouse input")
284
+ output = json.dumps(result, ensure_ascii=False, indent=2)
285
+ if args.report:
286
+ args.report.write_text(output, encoding="utf-8")
287
+ print(output)
288
+ return 0 if result["outcome"] in ("logged_in", "already_logged_in") else 1
289
+ finally:
290
+ backend.close()
291
+
292
+
293
+ def login_running_qmt(directory, login_account="", password_provider=lambda: "", expected_pid=None,
294
+ startup_timeout=60, timeout=40):
295
+ """Wait for a freshly launched QMT, then execute one bounded login flow."""
296
+ deadline = time.monotonic() + startup_timeout
297
+ while True:
298
+ try:
299
+ backend = QtLoginBackend(Path(directory), timeout, expected_pid=expected_pid)
300
+ break
301
+ except QmtNotReady:
302
+ if time.monotonic() >= deadline:
303
+ return {"outcome": "startup_timeout", "login_succeeded": False, "attempts": []}
304
+ time.sleep(0.5)
305
+ try:
306
+ if not backend.already_logged_in() and backend.state().get("busy"):
307
+ outcome = backend.wait_outcome()
308
+ if outcome == "logged_in":
309
+ # Reopen only the automation session to verify the resulting user.
310
+ backend.close()
311
+ backend = QtLoginBackend(Path(directory), timeout, expected_pid=expected_pid)
312
+ elif outcome != "credential_error":
313
+ return {"outcome": outcome, "login_succeeded": False, "attempts": []}
314
+ backend.select_account(login_account, replace=True)
315
+ return auto_login(backend, password_provider)
316
+ finally:
317
+ backend.close()
318
+
319
+
320
+ if __name__ == "__main__":
321
+ sys.exit(main())