Selaa lähdekoodia

一套很基本的观察模式。

skyfffire 5 kuukautta sitten
vanhempi
commit
44a3b36603
2 muutettua tiedostoa jossa 268 lisäystä ja 0 poistoa
  1. 266 0
      arbitrage_monitor.html
  2. 2 0
      arbitrage_system.py

+ 266 - 0
arbitrage_monitor.html

@@ -0,0 +1,266 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>套利流程监控</title>
+    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
+    <style>
+        body {
+            font-family: sans-serif;
+            padding: 20px;
+            background-color: #f4f7f6; /* Lighter background for the page */
+        }
+        .task-card {
+            margin-bottom: 15px; /* Slightly reduced margin */
+            /* border: 1px solid #ddd; */ /* Border is handled by Bootstrap card */
+            /* border-radius: 5px; */ /* Bootstrap card has rounded corners */
+        }
+        .task-card .card-header { /* Styling for the clickable header */
+            padding: 0; /* Remove padding to let button fill it */
+        }
+        .task-card .card-header button {
+            /* text-decoration: none !important; */ /* Remove underline from btn-link if used */
+            color: #333; /* Darker text for better readability on light bg */
+        }
+        .task-card .card-header button:hover {
+            background-color: #e9ecef; /* Slightly darker on hover */
+        }
+        .state-flow-table th, .state-flow-table td {
+            font-size: 0.9em;
+            vertical-align: middle;
+        }
+        .status-success { color: green; font-weight: bold; }
+        .status-fail { color: red; font-weight: bold; }
+        .status-pending { color: orange; }
+        .status-info { color: blue; }
+
+        .log-msg {
+            white-space: pre-wrap;
+            word-break: break-all;
+            max-height: 150px; /* Increased max height for logs */
+            overflow-y: auto;
+            font-size: 0.85em;
+            background-color: #fdfdfd; /* Slightly off-white for log background */
+            padding: 8px;
+            border: 1px solid #eee;
+            border-radius: 3px;
+            margin-top: 5px;
+        }
+        .section-title {
+            display: flex;
+            align-items: center;
+            margin-bottom: 1rem;
+        }
+        .section-title h2 {
+            margin-bottom: 0;
+            margin-right: 0.5rem;
+        }
+    </style>
+</head>
+<body>
+    <div class="container-fluid">
+        <h1 class="mb-4">套利流程监控</h1>
+        <button id="refreshButton" class="btn btn-primary mb-4">刷新数据</button>
+        
+        <div class="row">
+            <div class="col-md-6">
+                <div class="section-title">
+                    <h2>正在处理</h2>
+                    <span id="processing-count" class="badge badge-info badge-pill" style="font-size: 1rem;">0</span>
+                </div>
+                <div id="processing-list" class="accordion">
+                    <p>加载中...</p>
+                </div>
+            </div>
+            <div class="col-md-6">
+                <div class="section-title">
+                    <h2>历史记录</h2>
+                    <span id="history-count" class="badge badge-secondary badge-pill" style="font-size: 1rem;">0</span>
+                </div>
+                <div id="history-list" class="accordion">
+                    <p>加载中...</p>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        const API_BASE_URL = 'http://localhost:5002'; // 修改为你的 Flask API 地址
+
+        function getStatusClass(status) { // For text color in stateFlow
+            status = status ? status.toLowerCase() : 'info';
+            if (status.includes('success') || status.includes('completed')) return 'status-success';
+            if (status.includes('fail') || status.includes('failed') || status.includes('error')) return 'status-fail';
+            if (status.includes('pending') || status.includes('waiting') || status.includes('buying') || status_includes_any(status, ['received', 'processing', 'starting', 'pending_start'])) return 'status-pending';
+            return 'status-info';
+        }
+        
+        function status_includes_any(status, keywords) {
+            for (const keyword of keywords) {
+                if (status.includes(keyword)) return true;
+            }
+            return false;
+        }
+
+        function getStatusBadgeClass(status) { // For Bootstrap badge component in card header
+            status = status ? status.toLowerCase() : 'info';
+            if (status.includes('success') || status.includes('completed')) return 'badge-success';
+            if (status.includes('fail') || status.includes('failed') || status.includes('error')) return 'badge-danger';
+            if (status_includes_any(status, ['pending', 'waiting', 'buying', 'received', 'processing', 'starting', 'pending_start'])) return 'badge-warning';
+            return 'badge-info';
+        }
+
+        function formatStateFlow(stateFlow) {
+            if (!stateFlow || stateFlow.length === 0) {
+                return '<p><em>无状态流转记录。</em></p>';
+            }
+            let tableHtml = `
+                <table class="table table-sm table-hover table-bordered state-flow-table mt-2">
+                    <thead class="thead-light">
+                        <tr>
+                            <th>时间戳</th>
+                            <th>状态名</th>
+                            <th>消息</th>
+                            <th>结果</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+            `;
+            stateFlow.forEach(state => {
+                let actualStatus = state.status;
+                if (Array.isArray(state.status)) { 
+                    actualStatus = state.status[state.status.length-1];
+                }
+                tableHtml += `
+                    <tr>
+                        <td style="min-width: 140px;">${state.timestamp || 'N/A'}</td>
+                        <td>${state.stateName || 'N/A'}</td>
+                        <td><div class="log-msg">${state.msg || ''}</div></td>
+                        <td class="${getStatusClass(actualStatus)}">${actualStatus || 'N/A'}</td>
+                    </tr>
+                `;
+            });
+            tableHtml += '</tbody></table>';
+            return tableHtml;
+        }
+
+        function createTaskCard(task, listIdForAccordion) {
+            // Generate a safer ID for HTML attributes, especially if task.id could have unusual chars
+            const safeId = `task-${task.id.replace(/[^a-zA-Z0-9-_]/g, '')}`;
+            const headingId = `heading-${safeId}`;
+            const collapseId = `collapse-${safeId}`;
+
+            const card = document.createElement('div');
+            card.className = 'card task-card shadow-sm'; // Added shadow-sm for a bit of depth
+
+            // Card Header (Clickable Toggle)
+            const cardHeader = document.createElement('div');
+            cardHeader.className = 'card-header'; // Bootstrap handles padding now
+            cardHeader.id = headingId;
+            cardHeader.innerHTML = `
+                <h2 class="mb-0">
+                    <button class="btn btn-light btn-block text-left p-3" type="button" data-toggle="collapse" data-target="#${collapseId}" aria-expanded="false" aria-controls="${collapseId}">
+                        <div class="d-flex justify-content-between align-items-center w-100">
+                            <span style="font-size: 1.1rem; font-weight: bold; color: #007bff;">${task.symbol || 'N/A'}</span>
+                            <span class="badge ${getStatusBadgeClass(task.currentState)} p-2" style="font-size: 0.8rem;">${task.currentState || 'N/A'}</span>
+                        </div>
+                        <div style="font-size: 0.85em;" class="text-muted mt-2">
+                            <span>创建: ${task.creationTime || 'N/A'}</span>
+                            <span class="mx-2">|</span>
+                            <span>利润: ${task.profit || 'N/A'} (阈值: ${task.profitLimit || 'N/A'})</span>
+                        </div>
+                    </button>
+                </h2>
+            `;
+
+            // Card Body (Collapsible Content)
+            const collapseDiv = document.createElement('div');
+            collapseDiv.id = collapseId;
+            collapseDiv.className = 'collapse'; // Default collapsed
+            collapseDiv.setAttribute('aria-labelledby', headingId);
+            collapseDiv.setAttribute('data-parent', `#${listIdForAccordion}`); // For accordion behavior
+
+            collapseDiv.innerHTML = `
+                <div class="card-body">
+                    <p class="mb-1"><small class="text-muted">ID: ${task.id}</small></p>
+                    <hr class="my-2">
+                    <p class="mb-1"><strong>来源代币:</strong> ${task.fromTokenAmountHuman || 'N/A'} ${task.fromToken || ''}</p>
+                    <p><strong>目标代币:</strong> ${task.toTokenAmountHuman || 'N/A'} ${task.toToken || ''}</p>
+                    <h6 class="mt-3">状态流转:</h6>
+                    ${formatStateFlow(task.stateFlow)}
+                </div>
+            `;
+
+            card.appendChild(cardHeader);
+            card.appendChild(collapseDiv);
+            return card;
+        }
+
+        async function fetchData(endpoint, targetElementId, countElementId) {
+            const targetElement = document.getElementById(targetElementId);
+            const countElement = document.getElementById(countElementId);
+            if (!targetElement || !countElement) {
+                console.error(`Element not found: ${targetElementId} or ${countElementId}`);
+                return;
+            }
+            targetElement.innerHTML = '<p class="text-muted"><em>加载中...</em></p>';
+            try {
+                const response = await fetch(`${API_BASE_URL}${endpoint}`);
+                if (!response.ok) {
+                    throw new Error(`HTTP error! status: ${response.status}`);
+                }
+                const data = await response.json();
+
+                // 对数据按 creationTime 倒序排序
+                if (data && data.length > 0) {
+                    data.sort((a, b) => {
+                        // 假设 creationTime 是 'YYYY-MM-DD HH:MM:SS,ms' 格式
+                        // 如果不是有效字符串,则将其视为较早的时间
+                        const timeA = a.creationTime || '0'; 
+                        const timeB = b.creationTime || '0';
+                        if (timeA < timeB) {
+                            return 1; // b 在 a 之前,所以 b 应该排在前面(对于倒序)
+                        }
+                        if (timeA > timeB) {
+                            return -1; // a 在 b 之前,所以 a 应该排在前面(对于倒序)
+                        }
+                        return 0; // 时间相同
+                    });
+                }
+                
+                targetElement.innerHTML = ''; 
+                if (data && data.length > 0) {
+                    data.forEach(task => {
+                        // Pass targetElementId to createTaskCard for the data-parent attribute of accordion
+                        targetElement.appendChild(createTaskCard(task, targetElementId));
+                    });
+                } else {
+                    targetElement.innerHTML = '<p class="text-muted"><em>没有任务。</em></p>';
+                }
+                countElement.textContent = data.length;
+
+            } catch (error) {
+                console.error(`获取 ${endpoint} 数据失败:`, error);
+                targetElement.innerHTML = `<p class="text-danger">加载数据失败: ${error.message}</p>`;
+                countElement.textContent = 'ERR';
+            }
+        }
+
+        function refreshAllData() {
+            fetchData('/processing', 'processing-list', 'processing-count');
+            fetchData('/history', 'history-list', 'history-count');
+        }
+
+        document.getElementById('refreshButton').addEventListener('click', refreshAllData);
+
+        document.addEventListener('DOMContentLoaded', () => {
+            refreshAllData();
+            // setInterval(refreshAllData, 10000); // 每10秒刷新一次 (可选)
+        });
+    </script>
+    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
+    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
+</body>
+</html>

+ 2 - 0
arbitrage_system.py

@@ -11,6 +11,7 @@ log.setLevel(logging.ERROR)
 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
 from flask import Flask, request, jsonify
 from flask import Flask, request, jsonify
+from flask_cors import CORS # 导入
 from web3_py_client import EthClient # 你特定的客户端
 from web3_py_client import EthClient # 你特定的客户端
 
 
 
 
@@ -49,6 +50,7 @@ except Exception as e:
 
 
 # --- Flask 应用 ---
 # --- Flask 应用 ---
 app = Flask(__name__)
 app = Flask(__name__)
+CORS(app) # 在创建 app 实例后启用 CORS
 
 
 def arbitrage_process_flow(process_item):
 def arbitrage_process_flow(process_item):
     """
     """