brain.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. /**
  2. * BRAIN API 集成模块
  3. * 处理 WorldQuant BRAIN 的身份验证、运算符和数据字段
  4. * 现在使用本地 Python 代理服务器来绕过 CORS 限制
  5. */
  6. // BRAIN 会话和数据存储
  7. let brainSession = null;
  8. let brainOperators = null;
  9. let brainDataFields = null;
  10. let brainSessionId = localStorage.getItem('brain_session_id');
  11. // Flask 应用 API 端点
  12. const PROXY_BASE = '';
  13. // 打开 BRAIN 登录模态框
  14. function openBrainLoginModal() {
  15. const modal = document.getElementById('brainLoginModal');
  16. const statusDiv = document.getElementById('brainLoginStatus');
  17. statusDiv.innerHTML = '';
  18. statusDiv.className = 'login-status';
  19. // 清除之前的输入
  20. document.getElementById('brainUsername').value = '';
  21. document.getElementById('brainPassword').value = '';
  22. modal.style.display = 'block';
  23. document.getElementById('brainUsername').focus();
  24. }
  25. // 关闭 BRAIN 登录模态框
  26. function closeBrainLoginModal() {
  27. const modal = document.getElementById('brainLoginModal');
  28. const loginBtn = document.getElementById('loginBtn');
  29. // 如果登录正在进行中,不允许关闭
  30. if (loginBtn.disabled) {
  31. return;
  32. }
  33. modal.style.display = 'none';
  34. }
  35. // 通过代理服务器与 BRAIN 进行身份验证
  36. async function authenticateBrain() {
  37. const username = document.getElementById('brainUsername').value.trim();
  38. const password = document.getElementById('brainPassword').value;
  39. const statusDiv = document.getElementById('brainLoginStatus');
  40. const loginBtn = document.getElementById('loginBtn');
  41. const spinner = document.getElementById('loginSpinner');
  42. const modal = document.getElementById('brainLoginModal');
  43. if (!username || !password) {
  44. showLoginStatus('请输入用户名和密码。', 'error');
  45. return;
  46. }
  47. // 禁用所有输入框和按钮
  48. document.getElementById('brainUsername').disabled = true;
  49. document.getElementById('brainPassword').disabled = true;
  50. document.getElementById('cancelBtn').disabled = true;
  51. loginBtn.disabled = true;
  52. loginBtn.textContent = '连接中...';
  53. // 显示加载指示器
  54. spinner.style.display = 'block';
  55. // 禁用模态框关闭
  56. modal.querySelector('.close').style.display = 'none';
  57. // 显示加载状态
  58. showLoginStatus('正在连接到代理服务器...', 'loading');
  59. try {
  60. showLoginStatus('正在与 BRAIN 进行身份验证...', 'loading');
  61. // 通过代理服务器进行身份验证
  62. const authResponse = await fetch(`${PROXY_BASE}/api/authenticate`, {
  63. method: 'POST',
  64. headers: {
  65. 'Content-Type': 'application/json'
  66. },
  67. body: JSON.stringify({
  68. username: username,
  69. password: password
  70. })
  71. });
  72. if (!authResponse.ok) {
  73. const errorData = await authResponse.json();
  74. throw new Error(errorData.error || '身份验证失败');
  75. }
  76. const authData = await authResponse.json();
  77. brainSessionId = authData.session_id;
  78. brainSession = { authenticated: true, username: username };
  79. // 将会话 ID 存储在 localStorage 中供其他页面使用
  80. localStorage.setItem('brain_session_id', brainSessionId);
  81. // 立即获取运算符以支持 "Op" 按钮功能
  82. showLoginStatus('正在加载运算符...', 'loading');
  83. brainOperators = await getUserOperators();
  84. // 更新 UI 显示已连接状态
  85. updateConnectedState();
  86. showLoginStatus(`连接成功!已加载 ${brainOperators.length} 个运算符。`, 'success');
  87. // 禁用按钮以防止进一步点击
  88. loginBtn.disabled = true;
  89. document.getElementById('brainUsername').disabled = true;
  90. document.getElementById('brainPassword').disabled = true;
  91. // 短暂延迟后关闭模态框
  92. setTimeout(() => {
  93. // 在关闭前重新启用所有控件
  94. document.getElementById('brainUsername').disabled = false;
  95. document.getElementById('brainPassword').disabled = false;
  96. document.getElementById('cancelBtn').disabled = false;
  97. loginBtn.disabled = false;
  98. loginBtn.textContent = '连接';
  99. spinner.style.display = 'none';
  100. modal.querySelector('.close').style.display = 'block';
  101. closeBrainLoginModal();
  102. }, 1500);
  103. } catch (error) {
  104. console.error('BRAIN 身份验证失败:', error);
  105. showLoginStatus(`连接失败: ${error.message}`, 'error');
  106. brainSession = null;
  107. brainSessionId = null;
  108. } finally {
  109. // 重新启用所有控件
  110. document.getElementById('brainUsername').disabled = false;
  111. document.getElementById('brainPassword').disabled = false;
  112. document.getElementById('cancelBtn').disabled = false;
  113. loginBtn.disabled = false;
  114. loginBtn.textContent = '连接';
  115. spinner.style.display = 'none';
  116. modal.querySelector('.close').style.display = 'block';
  117. }
  118. }
  119. // 通过代理服务器获取用户运算符
  120. async function getUserOperators() {
  121. if (!brainSession || !brainSessionId) {
  122. throw new Error('未通过 BRAIN 身份验证');
  123. }
  124. try {
  125. const response = await fetch(`${PROXY_BASE}/api/operators`, {
  126. method: 'GET',
  127. headers: {
  128. 'Session-ID': brainSessionId
  129. }
  130. });
  131. if (!response.ok) {
  132. const errorData = await response.json();
  133. throw new Error(errorData.error || '获取运算符失败');
  134. }
  135. const operators = await response.json();
  136. console.log(`从 BRAIN API 接收到 ${operators.length} 个运算符`);
  137. // 记录类别以验证我们拥有所有运算符类型
  138. const categories = [...new Set(operators.map(op => op.category))].sort();
  139. console.log(`运算符类别: ${categories.join(', ')}`);
  140. return operators;
  141. } catch (error) {
  142. console.error('获取运算符失败:', error);
  143. throw error;
  144. }
  145. }
  146. // 通过代理服务器获取数据字段
  147. async function getDataFields(region = 'USA', delay = 1, universe = 'TOP3000', datasetId = 'fundamental6') {
  148. if (!brainSession || !brainSessionId) {
  149. throw new Error('未通过 BRAIN 身份验证');
  150. }
  151. try {
  152. const params = new URLSearchParams({
  153. region: region,
  154. delay: delay.toString(),
  155. universe: universe,
  156. dataset_id: datasetId
  157. });
  158. const response = await fetch(`${PROXY_BASE}/api/datafields?${params}`, {
  159. method: 'GET',
  160. headers: {
  161. 'Session-ID': brainSessionId
  162. }
  163. });
  164. if (!response.ok) {
  165. const errorData = await response.json();
  166. throw new Error(errorData.error || '获取数据字段失败');
  167. }
  168. return await response.json();
  169. } catch (error) {
  170. console.error('获取数据字段失败:', error);
  171. throw error;
  172. }
  173. }
  174. // 更新 UI 显示已连接状态
  175. function updateConnectedState() {
  176. const connectBtn = document.getElementById('connectToBrain');
  177. connectBtn.textContent = '已连接到 BRAIN';
  178. connectBtn.className = 'btn btn-brain connected';
  179. // 在语法错误区域显示连接信息
  180. const errorsDiv = document.getElementById('grammarErrors');
  181. errorsDiv.innerHTML = `<div class="success-message">
  182. ✓ 已成功连接到 WorldQuant BRAIN<br>
  183. <strong>用户名:</strong> ${brainSession.username}<br>
  184. <strong>已加载运算符:</strong> ${brainOperators ? brainOperators.length : 0}<br>
  185. <em>数据字段将在需要时加载。</em>
  186. </div>`;
  187. // 5 秒后自动隐藏消息
  188. setTimeout(() => {
  189. if (errorsDiv.innerHTML.includes('已成功连接')) {
  190. errorsDiv.innerHTML = '';
  191. }
  192. }, 5000);
  193. }
  194. // 显示登录状态消息
  195. function showLoginStatus(message, type) {
  196. const statusDiv = document.getElementById('brainLoginStatus');
  197. statusDiv.textContent = message;
  198. statusDiv.className = `login-status ${type}`;
  199. }
  200. // 检查是否已连接到 BRAIN
  201. function isConnectedToBrain() {
  202. return brainSession !== null && brainSessionId !== null;
  203. }
  204. // 获取所有可用运算符(按需获取)
  205. async function getAllOperators() {
  206. if (!brainOperators && isConnectedToBrain()) {
  207. try {
  208. brainOperators = await getUserOperators();
  209. } catch (error) {
  210. console.error('按需获取运算符失败:', error);
  211. return [];
  212. }
  213. }
  214. return brainOperators || [];
  215. }
  216. // 同步获取已加载的运算符(用于 UI 组件)
  217. function getLoadedOperators() {
  218. return brainOperators || [];
  219. }
  220. // 获取所有可用数据字段(按需获取)
  221. async function getAllDataFields() {
  222. if (!brainDataFields && isConnectedToBrain()) {
  223. try {
  224. brainDataFields = await getDataFields();
  225. } catch (error) {
  226. console.error('按需获取数据字段失败:', error);
  227. return [];
  228. }
  229. }
  230. return brainDataFields || [];
  231. }
  232. // 按类别获取运算符(支持按需加载)
  233. async function getOperatorsByCategory(category) {
  234. const operators = await getAllOperators();
  235. return operators.filter(op => op.category === category);
  236. }
  237. // 搜索运算符(支持按需加载)
  238. async function searchOperators(searchTerm) {
  239. const operators = await getAllOperators();
  240. const term = searchTerm.toLowerCase();
  241. return operators.filter(op =>
  242. op.name.toLowerCase().includes(term) ||
  243. op.category.toLowerCase().includes(term)
  244. );
  245. }
  246. // 搜索数据字段(支持按需加载)
  247. async function searchDataFields(searchTerm) {
  248. const dataFields = await getAllDataFields();
  249. const term = searchTerm.toLowerCase();
  250. return dataFields.filter(field =>
  251. field.id.toLowerCase().includes(term) ||
  252. field.description.toLowerCase().includes(term)
  253. );
  254. }
  255. // 从 BRAIN 注销
  256. async function logoutFromBrain() {
  257. if (brainSessionId) {
  258. try {
  259. await fetch(`${PROXY_BASE}/api/logout`, {
  260. method: 'POST',
  261. headers: {
  262. 'Session-ID': brainSessionId
  263. }
  264. });
  265. } catch (error) {
  266. console.warn('从代理服务器注销失败:', error);
  267. }
  268. }
  269. // 清除本地会话数据
  270. brainSession = null;
  271. brainSessionId = null;
  272. brainOperators = null;
  273. brainDataFields = null;
  274. // 清除 localStorage
  275. localStorage.removeItem('brain_session_id');
  276. // 更新 UI
  277. const connectBtn = document.getElementById('connectToBrain');
  278. connectBtn.textContent = '连接到 BRAIN';
  279. connectBtn.className = 'btn btn-brain';
  280. }
  281. // 在页面加载时检查会话有效性
  282. async function checkSessionValidity() {
  283. if (brainSessionId) {
  284. try {
  285. const response = await fetch(`${PROXY_BASE}/api/status`, {
  286. method: 'GET',
  287. headers: {
  288. 'Session-ID': brainSessionId
  289. }
  290. });
  291. if (response.ok) {
  292. const data = await response.json();
  293. if (data.valid) {
  294. brainSession = { authenticated: true, username: data.username };
  295. // 更新 UI 显示已连接状态
  296. updateConnectedState();
  297. } else {
  298. // 会话已过期,清除它
  299. localStorage.removeItem('brain_session_id');
  300. brainSessionId = null;
  301. }
  302. }
  303. } catch (error) {
  304. console.warn('检查会话有效性失败:', error);
  305. }
  306. }
  307. }
  308. // 在页面加载时初始化
  309. document.addEventListener('DOMContentLoaded', checkSessionValidity);
  310. // 导出函数供其他模块使用
  311. window.brainAPI = {
  312. openBrainLoginModal,
  313. closeBrainLoginModal,
  314. authenticateBrain,
  315. isConnectedToBrain,
  316. getAllOperators,
  317. getAllDataFields,
  318. getDataFields,
  319. getOperatorsByCategory,
  320. searchOperators,
  321. searchDataFields,
  322. logoutFromBrain,
  323. getLoadedOperators
  324. };