功能描述:查询指定任务的实时状态和进度,支持轮询或WebSocket替代方案。
路径参数
task_id |
string |
是 |
UUID格式,任务唯一标识 |
响应示例(处理中)
{
"task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "processing",
"mode": "pvp",
"format": "xlsx",
"progress": "65% - 正在统计玩家数据...",
"result_path": null,
"start_time": "1736509800.123",
"elapsed_seconds": 45.2
}
响应示例(已完成)
{
"task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "completed",
"mode": "pvp",
"format": "xlsx",
"progress": "100% - 分析完成",
"result_path": "/web_results/PvP_Report_20250110_143000.xlsx",
"start_time": "1736509800.123",
"elapsed_seconds": 89.5,
"file_size": "2.3MB"
}
响应示例(失败)
{
"task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "failed",
"mode": "pvp",
"format": "xlsx",
"progress": "❌ 分析失败: 未解析到任何有效事件",
"error": "日志文件不包含有效的PvP击杀记录",
"result_path": null,
"audit_logged": true
}
💡 WebSocket替代方案(推荐)
当WebSocket可用时,建议优先使用WebSocket接收实时推送,减少服务器轮询压力。连接 /ws/progress 后自动接收JSON消息,无需手动查询。
📚 多语言代码示例
选择您的编程语言查看完整集成示例(含WebSocket自动重连):
Python
JavaScript
Java
C#
Go
Rust
PHP
cURL
Python完整示例(含WebSocket自动重连)
import asyncio
import json
import requests
import websocket
import time
class MCLogAnalyzerClient:
def __init__(self, base_url="http://localhost:8000"):
self.base_url = base_url
self.task_id = None
self.ws = None
self.ws_url = base_url.replace("http", "ws")
def upload_files(self, file_paths: list[str]) -> str:
"""上传文件并启动分析"""
files = [('files', open(fp, 'rb')) for fp in file_paths]
# 1. 上传文件
resp = requests.post(f"{self.base_url}/api/upload", files=files)
resp.raise_for_status()
upload_data = resp.json()
self.task_id = upload_data['task_id']
print(f"✅ 上传成功: {upload_data['file_count']} 个文件")
# 2. 启动分析
analyze_resp = requests.post(
f"{self.base_url}/api/analyze/{self.task_id}",
data={"mode": "pvp", "output_format": "xlsx"}
)
analyze_resp.raise_for_status()
print(f"🚀 分析已启动,任务ID: {self.task_id}")
return self.task_id
def watch_progress_ws(self):
"""WebSocket监听进度(带自动重连)"""
def on_message(ws, message):
data = json.loads(message)
if data.get("task_id") == self.task_id:
print(f"📊 进度: {data['progress']}")
if "100%" in data['progress']:
ws.close()
# 查询最终状态并下载
self.download_result()
def on_error(ws, error):
print(f"[WebSocket] 错误: {error}")
def on_close(ws, close_status_code, close_msg):
print(f"[WebSocket] 连接关闭({close_status_code}),5秒后重连...")
time.sleep(5)
self.watch_progress_ws()
def on_open(ws):
print("[WebSocket] 连接已建立")
ws.send(json.dumps({"type": "ping"}))
self.ws = websocket.WebSocketApp(
f"{self.ws_url}/ws/progress",
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close
)
self.ws.run_forever(ping_interval=30, ping_timeout=10)
def get_status(self) -> dict:
"""轮询获取状态(WebSocket备选)"""
resp = requests.get(f"{self.base_url}/api/status/{self.task_id}")
resp.raise_for_status()
return resp.json()
def download_result(self):
"""下载结果文件"""
status = self.get_status()
if status['status'] == 'completed':
filename = status['result_path'].split('/')[-1]
resp = requests.get(f"{self.base_url}/api/download/{filename}")
resp.raise_for_status()
with open(filename, 'wb') as f:
f.write(resp.content)
print(f"📥 下载完成: {filename} ({status.get('file_size', '未知大小')})")
else:
print("❌ 任务尚未完成")
# 使用示例
if __name__ == "__main__":
client = MCLogAnalyzerClient("http://localhost:8000")
client.upload_files(["pvp1.log", "pvp2.log.gz"])
client.watch_progress_ws() # WebSocket模式
# 或使用轮询模式
# while True:
# status = client.get_status()
# print(status['progress'])
# if status['status'] in ['completed', 'failed']:
# break
# time.sleep(2)
JavaScript浏览器端(原生实现)
class MCLogAnalyzer {
constructor(baseUrl = '') {
this.baseUrl = baseUrl;
this.taskId = null;
this.ws = null;
this.reconnectTimer = null;
}
async uploadAndAnalyze(files, mode = 'pvp', format = 'xlsx') {
// 上传文件
const formData = new FormData();
files.forEach(file => formData.append('files', file));
const uploadRes = await fetch(`${this.baseUrl}/api/upload`, {
method: 'POST',
body: formData
});
const uploadData = await uploadRes.json();
this.taskId = uploadData.task_id;
console.log(`✅ 上传成功: ${uploadData.file_count} 个文件`);
// 启动分析
const analyzeForm = new FormData();
analyzeForm.append('mode', mode);
analyzeForm.append('output_format', format);
await fetch(`${this.baseUrl}/api/analyze/${this.taskId}`, {
method: 'POST',
body: analyzeForm
});
console.log('🚀 分析已启动');
// 启动WebSocket监听
this.connectWebSocket();
return this.taskId;
}
connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
this.ws = new WebSocket(`${protocol}//${window.location.host}/ws/progress`);
this.ws.onopen = () => {
console.log('[WebSocket] 连接已建立');
this.ws.send(JSON.stringify({ type: 'ping' }));
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.task_id === this.taskId) {
console.log(`📊 进度: ${data.progress}`);
this.updateProgressUI(data.progress);
if (data.progress.includes('100%')) {
this.ws.close();
this.downloadResult();
}
}
};
this.ws.onclose = () => {
console.log('[WebSocket] 连接关闭,5秒后重连...');
this.reconnectTimer = setTimeout(() => this.connectWebSocket(), 5000);
};
}
updateProgressUI(progressText) {
const progressBar = document.getElementById('progress-bar');
const percent = parseInt(progressText);
if (!isNaN(percent)) {
progressBar.style.width = `${percent}%`;
progressBar.textContent = progressText;
}
}
async downloadResult() {
const statusRes = await fetch(`${this.baseUrl}/api/status/${this.taskId}`);
const status = await statusRes.json();
if (status.status === 'completed') {
const filename = status.result_path.split('/').pop();
const downloadRes = await fetch(`/api/download/${filename}`);
const blob = await downloadRes.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
}
}
async pollStatus(interval = 2000) {
// WebSocket不可用时的降级方案
const checkStatus = async () => {
const response = await fetch(`${this.baseUrl}/api/status/${this.taskId}`);
const status = await response.json();
console.log(`📊 状态: ${status.status} - ${status.progress}`);
if (status.status === 'completed') {
clearInterval(poller);
this.downloadResult();
} else if (status.status === 'failed') {
clearInterval(poller);
console.error('❌ 分析失败:', status.error);
}
};
const poller = setInterval(checkStatus, interval);
}
}
Java完整示例(Spring RestTemplate + Java-WebSocket)
import java.io.*;
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import org.json.*;
import org.springframework.core.io.FileSystemResource;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
public class MCLogAnalyzerClient {
private final String baseUrl;
private String taskId;
private WebSocketClient wsClient;
private final RestTemplate restTemplate;
public MCLogAnalyzerClient(String baseUrl) {
this.baseUrl = baseUrl;
this.restTemplate = new RestTemplate();
}
public String uploadFiles(List filePaths) throws Exception {
// 构建multipart请求
MultiValueMap body = new LinkedMultiValueMap<>();
for (String path : filePaths) {
body.add("files", new FileSystemResource(new File(path)));
}
// 上传文件
String uploadResponse = restTemplate.postForObject(
baseUrl + "/api/upload",
body,
String.class
);
JSONObject json = new JSONObject(uploadResponse);
this.taskId = json.getString("task_id");
System.out.println("✅ 上传成功: " + json.getInt("file_count") + " 个文件");
// 启动分析
MultiValueMap analyzeBody = new LinkedMultiValueMap<>();
analyzeBody.add("mode", "pvp");
analyzeBody.add("output_format", "xlsx");
restTemplate.postForObject(
baseUrl + "/api/analyze/" + taskId,
analyzeBody,
String.class
);
System.out.println("🚀 分析已启动,任务ID: " + taskId);
System.out.println("并发限制: 3个任务,当前队列位置: " + json.optString("concurrent_info", "未知"));
return taskId;
}
public void connectWebSocket() throws Exception {
WebSocketClient client = new StandardWebSocketClient();
this.wsClient = client.doHandshake(
new WebSocketHandler() {
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage> message) {
try {
JSONObject msg = new JSONObject(message.getPayload().toString());
if (msg.getString("task_id").equals(taskId)) {
System.out.println("📊 进度: " + msg.getString("progress"));
if (msg.getString("progress").contains("100%")) {
try {
session.close();
downloadResult();
} catch (Exception e) {
e.printStackTrace();
}
}
}
} catch (JSONException e) {
System.err.println("消息解析失败: " + e.getMessage());
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
System.out.println("[WebSocket] 连接关闭,5秒后重连...");
try {
TimeUnit.SECONDS.sleep(5);
connectWebSocket();
} catch (Exception e) {
e.printStackTrace();
}
}
},
new WebSocketHttpHeaders(),
URI.create(baseUrl.replace("http", "ws") + "/ws/progress")
).get();
}
public void downloadResult() throws Exception {
String statusJson = restTemplate.getForObject(
baseUrl + "/api/status/" + taskId,
String.class
);
JSONObject status = new JSONObject(statusJson);
if (status.getString("status").equals("completed")) {
String filename = status.getString("result_path").split("/")[2];
System.out.println("📥 下载: " + filename);
byte[] fileData = restTemplate.getForObject(
baseUrl + "/api/download/" + filename,
byte[].class
);
Files.write(Paths.get(filename), fileData);
System.out.println("✅ 下载完成: " + filename);
}
}
}
C#完整示例(HttpClient + Websocket.Client)
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Websocket.Client;
public class MCLogAnalyzerClient
{
private readonly HttpClient httpClient;
private readonly string baseUrl;
private string taskId;
private IWebsocketClient wsClient;
public MCLogAnalyzerClient(string baseUrl)
{
this.baseUrl = baseUrl;
this.httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
}
public async Task<string> UploadAndAnalyzeAsync(List<string> filePaths)
{
// 上传文件
using var content = new MultipartFormDataContent();
foreach (var path in filePaths)
{
var fileStream = File.OpenRead(path);
content.Add(new StreamContent(fileStream), "files", Path.GetFileName(path));
}
var uploadResponse = await httpClient.PostAsync($"{baseUrl}/api/upload", content);
uploadResponse.EnsureSuccessStatusCode();
var uploadJson = await uploadResponse.Content.ReadAsStringAsync();
using var uploadDoc = JsonDocument.Parse(uploadJson);
taskId = uploadDoc.RootElement.GetProperty("task_id").GetString();
Console.WriteLine($"✅ 上传成功: {uploadDoc.RootElement.GetProperty("file_count").GetInt32()} 个文件");
// 启动分析
var analyzeContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("mode", "pvp"),
new KeyValuePair<string, string>("output_format", "xlsx")
});
await httpClient.PostAsync($"{baseUrl}/api/analyze/{taskId}", analyzeContent);
Console.WriteLine($"🚀 分析已启动,任务ID: {taskId}");
return taskId;
}
public async Task ConnectWebSocketAsync()
{
var wsUrl = baseUrl.Replace("http", "ws");
wsClient = new WebsocketClient(new Uri($"{wsUrl}/ws/progress"));
wsClient.ReconnectTimeout = TimeSpan.FromSeconds(30);
wsClient.ReconnectionHappened.Subscribe(info => {
Console.WriteLine($"[WebSocket] 重连发生: {info.Type}");
if (info.Type == ReconnectionType.Initial) {
wsClient.Send("ping");
}
});
wsClient.MessageReceived.Subscribe(msg => {
try {
using var doc = JsonDocument.Parse(msg.Text);
var root = doc.RootElement;
if (root.GetProperty("task_id").GetString() == taskId) {
var progress = root.GetProperty("progress").GetString();
Console.WriteLine($"📊 进度: {progress}");
if (progress.Contains("100%")) {
wsClient.Stop(WebSocketCloseStatus.NormalClosure, "Analysis complete");
DownloadResultAsync().Wait();
}
}
} catch (JsonException ex) {
Console.WriteLine($"消息解析失败: {ex.Message}");
}
});
await wsClient.Start();
}
public async Task DownloadResultAsync()
{
var statusJson = await httpClient.GetStringAsync($"{baseUrl}/api/status/{taskId}");
using var statusDoc = JsonDocument.Parse(statusJson);
var root = statusDoc.RootElement;
if (root.GetProperty("status").GetString() == "completed")
{
var filename = root.GetProperty("result_path").GetString().Split('/')[2];
Console.WriteLine($"📥 下载: {filename}");
var fileData = await httpClient.GetByteArrayAsync($"{baseUrl}/api/download/{filename}");
await File.WriteAllBytesAsync(filename, fileData);
Console.WriteLine($"✅ 下载完成: {filename}");
}
}
}
Go完整示例(net/http + gorilla/websocket)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"time"
"github.com/gorilla/websocket"
)
type MCLogAnalyzerClient struct {
baseURL string
taskID string
}
func NewClient(baseURL string) *MCLogAnalyzerClient {
return &MCLogAnalyzerClient{baseURL: baseURL}
}
func (c *MCLogAnalyzerClient) UploadAndAnalyze(filePaths []string) error {
// 上传文件
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
for _, path := range filePaths {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
part, err := writer.CreateFormFile("files", filepath.Base(path))
if err != nil {
return err
}
io.Copy(part, file)
}
writer.Close()
resp, err := http.Post(c.baseURL+"/api/upload", writer.FormDataContentType(), body)
if err != nil {
return err
}
defer resp.Body.Close()
var uploadData map[string]interface{}
json.NewDecoder(resp.Body).Decode(&uploadData)
c.taskID = uploadData["task_id"].(string)
fmt.Printf("✅ 上传成功: %v 个文件\n", uploadData["file_count"])
// 启动分析
analyzeBody := fmt.Sprintf("mode=pvp&output_format=xlsx")
analyzeResp, err := http.Post(
c.baseURL+"/api/analyze/"+c.taskID,
"application/x-www-form-urlencoded",
bytes.NewBufferString(analyzeBody),
)
if err != nil {
return err
}
defer analyzeResp.Body.Close()
fmt.Println("🚀 分析已启动")
return nil
}
func (c *MCLogAnalyzerClient) ConnectWebSocket() error {
wsURL := c.baseURL[:4] + "ws" + c.baseURL[4:]
dialer := websocket.Dialer{
HandshakeTimeout: 10 * time.Second,
}
conn, _, err := dialer.Dial(wsURL+"/ws/progress", nil)
if err != nil {
return err
}
defer conn.Close()
// 自动重连机制
reconnect := func() {
time.Sleep(5 * time.Second)
c.ConnectWebSocket()
}
for {
_, message, err := conn.ReadMessage()
if err != nil {
fmt.Println("[WebSocket] 连接断开,5秒后重连...")
go reconnect()
return err
}
var msg map[string]interface{}
json.Unmarshal(message, &msg)
if msg["task_id"] == c.taskID {
fmt.Printf("📊 进度: %s\n", msg["progress"])
if progress, ok := msg["progress"].(string); ok && bytes.Contains([]byte(progress), []byte("100%")) {
conn.Close()
c.downloadResult()
break
}
}
}
return nil
}
func (c *MCLogAnalyzerClient) downloadResult() {
resp, err := http.Get(c.baseURL + "/api/status/" + c.taskID)
if err != nil {
return
}
defer resp.Body.Close()
var status map[string]interface{}
json.NewDecoder(resp.Body).Decode(&status)
if status["status"] == "completed" {
filename := filepath.Base(status["result_path"].(string))
fmt.Printf("📥 下载: %s\n", filename)
fileResp, err := http.Get(c.baseURL + "/api/download/" + filename)
if err != nil {
return
}
defer fileResp.Body.Close()
out, _ := os.Create(filename)
defer out.Close()
io.Copy(out, fileResp.Body)
fmt.Println("✅ 下载完成")
}
}
Rust完整示例(reqwest + tokio-tungstenite)
use std::fs::File;
use std::io::Read;
use reqwest::Client;
use tokio::net::TcpStream;
use tokio_tungstenite::{connect_async, tungstenite::Message};
use serde_json::Value;
use futures_util::StreamExt;
pub struct MCLogAnalyzerClient {
base_url: String,
task_id: Option,
}
impl MCLogAnalyzerClient {
pub fn new(base_url: String) -> Self {
Self {
base_url,
task_id: None,
}
}
pub async fn upload_and_analyze(&mut self, file_paths: Vec<&str>) -> Result> {
let client = Client::new();
// 构建multipart表单
let mut form = reqwest::multipart::Form::new();
for path in file_paths {
let mut file = File::open(path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
let part = reqwest::multipart::Part::bytes(buffer)
.file_name(path.to_string());
form = form.part("files".to_string(), part);
}
// 上传文件
let upload_resp = client
.post(&format!("{}/api/upload", self.base_url))
.multipart(form)
.send()
.await?;
let upload_json: Value = upload_resp.json().await?;
self.task_id = Some(upload_json["task_id"].as_str().unwrap().to_string());
println!("✅ 上传成功: 任务ID: {}", self.task_id.as_ref().unwrap());
// 启动分析
let analyze_params = [("mode", "pvp"), ("output_format", "xlsx")];
client
.post(&format!("{}/api/analyze/{}", self.base_url, self.task_id.as_ref().unwrap()))
.form(&analyze_params)
.send()
.await?;
println!("🚀 分析已启动");
Ok(self.task_id.as_ref().unwrap().clone())
}
pub async fn connect_websocket(&self) -> Result<(), Box> {
let ws_url = self.base_url.replace("http", "ws");
let (ws_stream, _) = connect_async(format!("{}/ws/progress", ws_url)).await?;
let (_, mut read) = ws_stream.split();
while let Some(msg) = read.next().await {
let msg_str = msg?.to_text()?;
let json: Value = serde_json::from_str(&msg_str)?;
if json["task_id"] == self.task_id.as_ref().unwrap().as_str() {
println!("📊 进度: {}", json["progress"]);
if json["progress"].as_str().unwrap().contains("100%") {
drop(read);
self.download().await?;
break;
}
}
}
Ok(())
}
pub async fn download(&self) -> Result<(), Box> {
let client = Client::new();
let resp = client
.get(&format!("{}/api/status/{}", self.base_url, self.task_id.as_ref().unwrap()))
.send()
.await?;
let status: Value = resp.json().await?;
if status["status"] == "completed" {
let filename = status["result_path"].as_str().unwrap().split('/').last().unwrap();
let file_resp = client
.get(&format!("{}/api/download/{}", self.base_url, filename))
.send()
.await?;
let bytes = file_resp.bytes().await?;
std::fs::write(filename, &bytes)?;
println!("📥 下载完成: {}", filename);
}
Ok(())
}
}
PHP完整示例(GuzzleHttp + WebSocket)
baseURL = $baseURL;
$this->httpClient = new Client([
'base_uri' => $baseURL,
'timeout' => 300.0,
'verify' => false, // 生产环境设为true并配置SSL
]);
}
public function uploadAndAnalyze($filePaths) {
// 上传文件
$multipart = [];
foreach ($filePaths as $path) {
$multipart[] = [
'name' => 'files',
'contents' => fopen($path, 'r'),
'filename' => basename($path)
];
}
$response = $this->httpClient->post('/api/upload', [
'multipart' => $multipart
]);
$uploadData = json_decode($response->getBody(), true);
$this->taskId = $uploadData['task_id'];
echo "✅ 上传成功: {$uploadData['file_count']} 个文件\n";
// 启动分析
$this->httpClient->post("/api/analyze/{$this->taskId}", [
'form_params' => [
'mode' => 'pvp',
'output_format' => 'xlsx'
]
]);
echo "🚀 分析已启动\n";
return $this->taskId;
}
public function connectWebSocket() {
$wsURL = str_replace('http', 'ws', $this->baseURL);
$this->wsClient = new WSClient("{$wsURL}/ws/progress", [
'timeout' => 60,
'reconnect' => true,
'reconnect_interval' => 5,
]);
$this->wsClient->on('message', function($message) {
$data = json_decode($message, true);
if ($data['task_id'] === $this->taskId) {
echo "📊 进度: {$data['progress']}\n";
if (strpos($data['progress'], '100%') !== false) {
$this->wsClient->close();
$this->download();
}
}
});
$this->wsClient->on('close', function() {
echo "[WebSocket] 连接关闭,尝试重连...\n";
});
$this->wsClient->loop();
}
public function download() {
$statusRes = $this->httpClient->get("/api/status/{$this->taskId}");
$status = json_decode($statusRes->getBody(), true);
if ($status['status'] === 'completed') {
$filename = basename($status['result_path']);
echo "📥 下载: {$filename}\n";
$response = $this->httpClient->get("/api/download/{$filename}");
file_put_contents($filename, $response->getBody());
echo "✅ 下载完成\n";
}
}
}
// 使用示例
$client = new MCLogAnalyzerClient('http://localhost:8000');
$client->uploadAndAnalyze(['pvp1.log', 'pvp2.log.gz']);
$client->connectWebSocket();
?>
cURL命令行完整脚本(Bash)
#!/bin/bash
# MC部落日志分析器 - cURL调用脚本 v2.1.2
# 支持: Linux/macOS/Windows(Git Bash)
# 依赖: curl, jq (可选,用于JSON解析)
set -e # 遇到错误立即退出
BASE_URL="${MC_ANALYZER_URL:-http://localhost:8000}"
TASK_ID=""
FILENAME=""
MAX_RETRIES=5
# 颜色输出
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m'
echo -e "${GREEN}🎯 MC部落日志分析器 cURL客户端 v2.1.2${NC}"
echo -e "${BLUE}服务器: ${BASE_URL}${NC}\n"
# 检查依赖
command -v curl >/dev/null 2>&1 || { echo -e "${RED}❌ 未找到curl命令${NC}"; exit 1; }
# 1. 上传文件
echo -e "${YELLOW}📤 正在上传文件...${NC}"
if [ $# -eq 0 ]; then
echo -e "${RED}❌ 请提供日志文件路径${NC}"
echo "用法: $0 [file2.log.gz] [...]"
exit 1
fi
# 构建curl命令
UPLOAD_CMD="curl -s -X POST"
for file in "$@"; do
if [ ! -f "$file" ]; then
echo -e "${RED}❌ 文件不存在: $file${NC}"
exit 1
fi
UPLOAD_CMD="$UPLOAD_CMD -F \"files=@$file\""
done
UPLOAD_CMD="$UPLOAD_CMD \"${BASE_URL}/api/upload\""
# 执行上传
UPLOAD_RESPONSE=$(eval $UPLOAD_CMD)
# 检查响应
if echo "$UPLOAD_RESPONSE" | grep -q '"task_id"'; then
TASK_ID=$(echo "$UPLOAD_RESPONSE" | sed -n 's/.*"task_id":"\([^"]*\)".*/\1/p')
FILE_COUNT=$(echo "$UPLOAD_RESPONSE" | sed -n 's/.*"file_count":\([0-9]*\).*/\1/p')
echo -e "${GREEN}✅ 上传成功!任务ID: $TASK_ID${NC}"
echo -e "${GREEN}📦 文件数量: $FILE_COUNT${NC}"
else
echo -e "${RED}❌ 上传失败${NC}"
echo "$UPLOAD_RESPONSE" | jq . 2>/dev/null || echo "$UPLOAD_RESPONSE"
exit 1
fi
# 2. 启动分析
echo -e "\n${YELLOW}🚀 启动分析任务...${NC}"
ANALYZE_RESPONSE=$(curl -s -X POST \
-d "mode=pvp" \
-d "output_format=xlsx" \
"${BASE_URL}/api/analyze/${TASK_ID}")
if echo "$ANALYZE_RESPONSE" | grep -q "任务已提交"; then
echo -e "${GREEN}✅ 分析任务已提交${NC}"
else
echo -e "${RED}❌ 启动分析失败${NC}"
echo "$ANALYZE_RESPONSE" | jq . 2>/dev/null || echo "$ANALYZE_RESPONSE"
exit 1
fi
# 3. 轮询状态(简化版)
echo -e "\n${YELLOW}⏳ 等待分析完成...${NC}"
RETRY_COUNT=0
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
STATUS_RESPONSE=$(curl -s "${BASE_URL}/api/status/${TASK_ID}")
if echo "$STATUS_RESPONSE" | grep -q '"status"'; then
STATUS=$(echo "$STATUS_RESPONSE" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p')
PROGRESS=$(echo "$STATUS_RESPONSE" | sed -n 's/.*"progress":"\([^"]*\)".*/\1/p')
echo -ne "${YELLOW}\r📊 $PROGRESS${NC}"
if [ "$STATUS" == "completed" ]; then
FILENAME=$(echo "$STATUS_RESPONSE" | sed -n 's/.*"result_path":"[^"]*\/\([^"]*\)".*/\1/p')
ELAPSED=$(echo "$STATUS_RESPONSE" | sed -n 's/.*"elapsed_seconds":\([0-9.]*\).*/\1/p')
echo -e "\n${GREEN}✅ 分析完成!耗时: ${ELAPSED}秒${NC}"
break
elif [ "$STATUS" == "failed" ]; then
echo -e "\n${RED}❌ 分析失败${NC}"
ERROR=$(echo "$STATUS_RESPONSE" | sed -n 's/.*"error":"\([^"]*\)".*/\1/p')
echo -e "${RED}错误: $ERROR${NC}"
exit 1
fi
else
RETRY_COUNT=$((RETRY_COUNT + 1))
echo -e "${YELLOW}⚠️ 无法获取状态,重试 $RETRY_COUNT/$MAX_RETRIES${NC}"
fi
sleep 2
done
if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
echo -e "${RED}❌ 达到最大重试次数${NC}"
exit 1
fi
# 4. 下载文件
echo -e "\n${YELLOW}📥 下载结果文件...${NC}"
DOWNLOAD_URL="${BASE_URL}/api/download/${FILENAME}"
curl -L -O --progress-bar "$DOWNLOAD_URL"
if [ -f "$FILENAME" ]; then
FILE_SIZE=$(ls -lh "$FILENAME" | awk '{print $5}')
echo -e "${GREEN}✅ 下载完成: $FILENAME (${FILE_SIZE})${NC}"
# 5. 可选:验证文件完整性
if [[ "$FILENAME" == *.xlsx ]]; then
echo -e "${BLUE}📊 文件类型: Excel表格${NC}"
elif [[ "$FILENAME" == *.pdf ]]; then
echo -e "${BLUE}📄 文件类型: PDF报告${NC}"
elif [[ "$FILENAME" == *.png ]]; then
echo -e "${BLUE}🖼️ 文件类型: PNG图表${NC}"
elif [[ "$FILENAME" == *.json ]]; then
echo -e "${BLUE}📋 文件类型: JSON数据${NC}"
fi
echo -e "\n${GREEN}🎉 全部完成!${NC}"
else
echo -e "${RED}❌ 下载失败${NC}"
exit 1
fi
# 使用帮助函数
show_help() {
echo "MC部落日志分析器 - cURL客户端"
echo "用法: $0 [file2] [...]"
echo "环境变量:"
echo " MC_ANALYZER_URL - 服务器地址 (默认: http://localhost:8000)"
echo "示例:"
echo " $0 pvp_logs.log"
echo " $0 *.log.gz"
echo " MC_ANALYZER_URL=https://mclans.sakurain.net $0 shop_2025*.log"
}