重构: 切换存储至SQLite,启用INI配置与API Key校验

This commit is contained in:
2026-02-09 17:10:11 +08:00
parent d5edbc0723
commit b9828a1b13
30 changed files with 2721 additions and 612 deletions

View File

@ -0,0 +1,95 @@
"""
API Keys Blueprint
Handles API key management endpoints.
"""
from flask import Blueprint, request
from ..auth import create_api_key, revoke_api_key, list_api_keys, get_api_key_info
from ..shared import _format_response, log_performance, logger
from ..auth import require_api_key
# Create blueprint
api_keys_bp = Blueprint('api_keys', __name__, url_prefix='/api-keys')
@api_keys_bp.route('', methods=['POST'])
@log_performance
def create_key():
"""Create a new API key."""
logger.info("API key creation request")
try:
data = request.get_json()
if not data:
return _format_response(400, "Request body must be JSON")
description = data.get('description', '')
scopes = data.get('scopes', '*')
key_id, plain_key = create_api_key(description=description, scopes=scopes)
return _format_response(201, "API key created successfully", {
"key_id": key_id,
"key": plain_key, # Only returned once for security
"description": description,
"scopes": scopes
})
except Exception as e:
logger.error(f"Error creating API key: {str(e)}", exc_info=True)
return _format_response(500, "Internal server error")
@api_keys_bp.route('', methods=['GET'])
@log_performance
def list_keys():
"""List all API keys."""
logger.debug("API keys list request")
try:
keys = list_api_keys()
return _format_response(200, "API keys retrieved successfully", {
"keys": keys,
"total": len(keys)
})
except Exception as e:
logger.error(f"Error listing API keys: {str(e)}", exc_info=True)
return _format_response(500, "Internal server error")
@api_keys_bp.route('/<key_id>', methods=['GET'])
@log_performance
def get_key(key_id):
"""Get information about a specific API key."""
logger.debug(f"API key info request for: {key_id}")
try:
key_info = get_api_key_info(key_id)
if not key_info:
return _format_response(404, "API key not found")
return _format_response(200, "API key information retrieved successfully", key_info)
except Exception as e:
logger.error(f"Error retrieving API key info: {str(e)}", exc_info=True)
return _format_response(500, "Internal server error")
@api_keys_bp.route('/<key_id>', methods=['DELETE'])
@log_performance
def revoke_key(key_id):
"""Revoke an API key."""
logger.info(f"API key revocation request for: {key_id}")
try:
if revoke_api_key(key_id):
return _format_response(200, "API key revoked successfully", {
"key_id": key_id
})
else:
return _format_response(404, "API key not found")
except Exception as e:
logger.error(f"Error revoking API key: {str(e)}", exc_info=True)
return _format_response(500, "Internal server error")

View File

@ -7,6 +7,39 @@ from pathlib import Path
from flask import Blueprint, send_file, current_app
from ..shared import _format_response, log_performance, logger
from ..auth import require_api_key
def _mark_task_downloaded(task_id):
"""Mark task as downloaded and schedule deletion in database."""
import sqlite3
from pathlib import Path
# Use independent database connection (not from flask.g which may be closed)
from ..db import get_db_path as get_config_db_path
db_path = get_config_db_path(current_app)
try:
conn = sqlite3.connect(str(db_path), check_same_thread=False)
conn.execute("PRAGMA foreign_keys=ON")
conn.execute("PRAGMA busy_timeout=3000")
# Update downloaded timestamp and set deletion time (10 minutes later)
conn.execute("""
UPDATE tasks
SET downloaded_at = datetime('now', '+8 hours'),
delete_after_at = datetime('now', '+8 hours', '+10 minutes')
WHERE task_id = ?
""", (task_id,))
conn.commit()
logger.info(f"Task {task_id} marked as downloaded, scheduled for deletion in 10 minutes")
except Exception as e:
logger.error(f"Failed to mark task {task_id} as downloaded: {str(e)}", exc_info=True)
finally:
if 'conn' in locals():
conn.close()
# Create blueprint
download_bp = Blueprint('download', __name__, url_prefix='/download')
@ -20,6 +53,32 @@ def download_file(filename):
logger.info(f"Download request for file: {filename} from IP {request.remote_addr}")
# Check API key from header or query parameter
provided_key = request.headers.get('X-API-Key') or request.args.get('api_key')
if not provided_key:
logger.warning(f"API key missing from request: POST /download/{filename}")
return _format_response(401, "API key required")
# Validate API key
from ..auth import verify_api_key, get_db
db = get_db()
row = db.execute(
"SELECT key_hash, salt, revoked FROM api_keys WHERE key_id = ?",
(provided_key,)
).fetchone()
if not row:
logger.warning(f"Unknown API key used: POST /download/{filename}")
return _format_response(401, "Invalid API key")
if row['revoked']:
logger.warning(f"Revoked API key used: POST /download/{filename}")
return _format_response(401, "API key revoked")
if not verify_api_key(provided_key, row['key_hash'], row['salt']):
logger.warning(f"Invalid API key hash: POST /download/{filename}")
return _format_response(401, "Invalid API key")
try:
# 支持两种路径格式:
# 1. 绝对路径(以 / 开头,如 /full/path/to/file)
@ -27,6 +86,7 @@ def download_file(filename):
if filename.startswith('/'):
# 绝对路径 - 直接使用
file_path = Path(filename)
task_id = None # Can't determine task_id from absolute path
else:
# 相对路径 - 相对于 OUTPUT_FOLDER
output_folder = Path(current_app.config.get('OUTPUT_FOLDER') or '')
@ -61,7 +121,15 @@ def download_file(filename):
file_size = file_path.stat().st_size
logger.info(f"Serving file: {filename} ({file_size} bytes)")
return send_file(file_path)
# Mark download immediately before sending file
if task_id:
try:
_mark_task_downloaded(task_id)
except Exception as e:
logger.error(f"Failed to mark download for task {task_id}: {str(e)}")
response = send_file(file_path)
return response
except Exception as e:
logger.error(f"Error serving file {filename}: {str(e)}", exc_info=True)

View File

@ -9,7 +9,7 @@ import logging
from flask import Blueprint, request
from ..app import Config, stats_collector
from ..shared import task_status, TASK_STATUS_PENDING, TASK_STATUS_PROCESSING
from ..shared import TASK_STATUS_PENDING, TASK_STATUS_PROCESSING
from ..shared import _format_response, log_performance, logger
# Create blueprint

View File

@ -8,7 +8,7 @@ from pathlib import Path
from flask import Blueprint, request, current_app
from ..shared import _get_file_type, _format_response, log_performance, logger, task_status
from ..shared import _get_file_type, _format_response, log_performance, logger
from ..app import Config
# Create blueprint
@ -26,16 +26,16 @@ def list_reports():
try:
page = int(request.args.get('page', 1))
if page < 1:
return _format_response(400, "Invalid parameter: page must be >= 1")
return _format_response(400, "无效参数: 页码必须大于等于1")
except (ValueError, TypeError):
return _format_response(400, "Invalid parameter: page must be a valid integer")
return _format_response(400, "无效参数: 页码必须是有效的整数")
try:
per_page = int(request.args.get('per_page', 20))
if per_page < 1 or per_page > 100:
return _format_response(400, "Invalid parameter: per_page must be between 1 and 100")
return _format_response(400, "无效参数: 每页数量必须在1-100之间")
except (ValueError, TypeError):
return _format_response(400, "Invalid parameter: per_page must be a valid integer")
return _format_response(400, "无效参数: 每页数量必须是有效的整数")
sort_by = request.args.get('sort_by', 'created_at')
sort_order = request.args.get('sort_order', 'desc')
@ -44,14 +44,14 @@ def list_reports():
# Validate sort parameters
valid_sort_fields = ['created_at', 'task_id', 'file_size', 'processing_time']
if sort_by not in valid_sort_fields:
return _format_response(400, f"Invalid parameter: sort_by must be one of {valid_sort_fields}")
return _format_response(400, f"无效参数: 排序字段必须是以下之一: {', '.join(valid_sort_fields)}")
if sort_order not in ['asc', 'desc']:
return _format_response(400, "Invalid parameter: sort_order must be 'asc' or 'desc'")
return _format_response(400, "无效参数: 排序顺序必须是 'asc' 或 'desc'")
# Validate status filter
valid_statuses = ['completed', 'failed', None]
if status_filter is not None and status_filter not in ['completed', 'failed']:
return _format_response(400, "Invalid parameter: status must be 'completed', 'failed', or not specified")
return _format_response(400, "无效参数: 状态必须是 'completed'、'failed' 或不指定")
# 兼容缺省:优先 app.config,其次 Config.OUTPUT_FOLDER
output_root = current_app.config.get('OUTPUT_FOLDER') or getattr(Config, 'OUTPUT_FOLDER', None)
@ -72,9 +72,10 @@ def list_reports():
continue
task_id = task_dir.name
# Get task information from global task_status
task_info = task_status.get(task_id, {})
task_status_value = task_info.get('status')
# Get task information from database
from ..shared import get_task_status
task_info = get_task_status(task_id)
task_status_value = task_info.get('status') if task_info.get('status') != 'not_found' else None
# Log task status for debugging
logger.debug(f"Task {task_id}: status from memory={task_status_value}, info={task_info}")

View File

@ -1,5 +1,5 @@
"""
Statistics Blueprint
Statistics Blueprintoutput_dir
Provides API statistics and monitoring endpoints.
"""
@ -7,7 +7,7 @@ import time
from flask import Blueprint, current_app
from ..shared import _format_response, log_performance, logger,stats_collector, task_status
from ..shared import _format_response, log_performance, logger, stats_collector
# Create blueprint
stats_bp = Blueprint('stats', __name__, url_prefix='/stats')
@ -46,17 +46,33 @@ def get_stats():
logger.warning(f"Failed to collect system metrics: {e}")
stats_data['system'] = {'error': str(e)}
# Add recent task information
# Add recent task information from database
recent_tasks = []
current_time = time.time()
for task_id, task_info in list(task_status.items())[-20:]: # Last 20 tasks
age = current_time - task_info.get('updated_at', 0)
recent_tasks.append({
'task_id': task_id,
'status': task_info.get('status'),
'age_seconds': round(age, 1),
'message': task_info.get('message', '')[:100] # Truncate long messages
})
try:
from ..db import get_db
db = get_db()
current_time = time.time()
rows = db.execute("""
SELECT task_id, status, message, updated_at
FROM tasks
WHERE deleted_at IS NULL
ORDER BY updated_at DESC
LIMIT 20
""").fetchall()
for row in rows:
updated_at = row[3] if row[3] else 0
age = current_time - updated_at
recent_tasks.append({
'task_id': row[0], # task_id
'status': row[1], # status
'age_seconds': round(age, 1),
'message': (row[2] or '')[:100] # message, truncate long messages
})
except Exception as e:
logger.warning(f"Failed to get recent tasks from database: {e}")
recent_tasks = []
stats_data['recent_tasks'] = recent_tasks

View File

@ -12,17 +12,39 @@ from ..shared import (
_format_response,
log_performance,
logger,
task_status,
TASK_STATUS_PENDING,
TASK_STATUS_PROCESSING,
TASK_STATUS_COMPLETED,
TASK_STATUS_FAILED
)
from ..auth import require_api_key
# Create blueprint
task_pool_bp = Blueprint('task_pool', __name__, url_prefix='/tasks')
def _localize_message(msg):
"""Translate known English status messages to Chinese for consistent frontend display."""
if not msg:
return msg
replacements = {
"Task queued for processing": "任务已加入处理队列",
"Starting data processing...": "开始处理数据...",
"Configuration loaded, starting preprocessing...": "配置已加载,开始预处理...",
"Preprocessing completed, starting GasFlux analysis...": "预处理完成,开始GasFlux分析...",
"Configuration loaded, starting GasFlux analysis...": "配置已加载,开始GasFlux分析...",
"GasFlux analysis completed, generating reports...": "GasFlux分析完成,正在生成报告...",
"Processing completed successfully": "处理成功完成",
"Processing failed:": "处理失败:",
}
for eng, zh in replacements.items():
if msg.startswith(eng):
return msg.replace(eng, zh, 1)
return msg
def _build_simple_downloads_from_results(results: list[dict]) -> dict:
"""
Build direct download shortcuts for common files, based on task results.
@ -71,21 +93,43 @@ def _lean_task_summary(task_summary: dict) -> dict:
lean = {
'task_id': task_id,
'status': status,
'message': task_summary.get('message'),
'message': _localize_message(task_summary.get('message')),
'updated_at': task_summary.get('updated_at'),
}
if status == TASK_STATUS_COMPLETED and task_id:
full_task_info = task_status.get(task_id, {})
from ..shared import get_task_status
full_task_info = get_task_status(task_id)
results = full_task_info.get('results', []) or []
# Extract volume from results
volume = None
for result in results:
if isinstance(result, dict) and 'krig_params' in result:
krig_params = result.get('krig_params', {})
if isinstance(krig_params, dict) and 'volume' in krig_params:
volume = krig_params['volume']
break
# Add volume to response if found
if volume is not None:
lean['volume'] = float(volume) # Ensure it's a number
downloads = _build_simple_downloads_from_results(results)
if downloads:
lean['downloads'] = downloads
elif status == TASK_STATUS_FAILED and task_id:
from ..shared import get_task_status
full_task_info = get_task_status(task_id)
error = full_task_info.get('error')
if error:
lean['error'] = error
return lean
@task_pool_bp.route('', methods=['GET'])
@require_api_key
@log_performance
def list_tasks():
"""Get paginated list of tasks with optional filtering."""
@ -143,6 +187,7 @@ def list_tasks():
@task_pool_bp.route('/stats', methods=['GET'])
@require_api_key
@log_performance
def get_pool_stats():
"""Get task pool statistics."""
@ -162,6 +207,7 @@ def get_pool_stats():
@task_pool_bp.route('/active', methods=['GET'])
@require_api_key
@log_performance
def get_active_tasks():
"""Get list of currently active (processing) tasks."""
@ -195,6 +241,7 @@ def get_active_tasks():
@task_pool_bp.route('/queue', methods=['GET'])
@require_api_key
@log_performance
def get_queued_tasks():
"""Get list of queued (pending) tasks."""

View File

@ -12,28 +12,28 @@ from ..shared import (
_format_response,
log_performance,
logger,
task_status,
_build_simple_downloads_from_results,
TASK_STATUS_COMPLETED,
TASK_STATUS_FAILED,
TASK_STATUS_PROCESSING,
TASK_STATUS_PENDING,
)
from ..auth import require_api_key
# Create blueprint
tasks_bp = Blueprint('tasks', __name__, url_prefix='/task')
@tasks_bp.route('/<task_id>', methods=['GET'])
@require_api_key
@log_performance
def get_task_status_endpoint(task_id):
"""Get the status of a processing task."""
logger.debug(f"Status request for task {task_id}")
try:
# Note: cleanup_old_tasks() is disabled for individual task queries
# to preserve historical task data for task pool management
# cleanup_old_tasks()
# Note: cleanup_old_tasks() is enabled to prevent memory leaks from old tasks
cleanup_old_tasks()
task_info = get_task_status(task_id)
if task_info.get("status") == "not_found":
@ -72,6 +72,7 @@ def get_task_status_endpoint(task_id):
@tasks_bp.route('/<task_id>', methods=['PUT'])
@require_api_key
@log_performance
def update_task(task_id):
"""Update task status and information."""
@ -125,10 +126,6 @@ def update_task(task_id):
# Only update message
update_task_status(task_id, current_status, message)
# Update priority if provided
if 'priority' in updates:
task_status[task_id]['priority'] = updates['priority']
# Get updated task info
updated_task = get_task_status(task_id)
@ -152,6 +149,7 @@ def update_task(task_id):
@tasks_bp.route('/<task_id>', methods=['DELETE'])
@require_api_key
@log_performance
def delete_task(task_id):
"""Delete a task and its associated files."""
@ -197,10 +195,7 @@ def delete_task(task_id):
logger.error(f"Error deleting task folder {task_folder}: {str(e)}")
return _format_response(500, f"删除任务文件失败: {str(e)}")
# Remove from task status tracking
if task_id in task_status:
del task_status[task_id]
logger.info(f"Removed task {task_id} from status tracking")
# Task status is now managed by database only
data = {
"task_id": task_id,

View File

@ -13,13 +13,15 @@ from io import BytesIO
from ..app import process_data_async
from ..shared import _format_response, log_performance, logger, ALLOWED_DATA_EXTENSIONS, ALLOWED_CONFIG_EXTENSIONS, allowed_file,update_task_status, TASK_STATUS_PENDING, TASK_STATUS_FAILED
from ..shared import _format_response, log_performance, logger, ALLOWED_DATA_EXTENSIONS, ALLOWED_CONFIG_EXTENSIONS, allowed_file, update_task_status, TASK_STATUS_PENDING, TASK_STATUS_FAILED
from ..auth import require_api_key
# Create blueprint
upload_bp = Blueprint('upload', __name__, url_prefix='/upload')
@upload_bp.route('', methods=['POST'])
@require_api_key
@log_performance
def upload_file():
logger.info("Received upload request")
@ -49,8 +51,8 @@ def upload_file():
return _format_response(400, "无效的数据文件类型。只允许 .xlsx 和 .xls 格式。")
# Generate unique job ID
job_id = str(uuid.uuid4())
logger.info(f"Generated job ID: {job_id}")
task_id = str(uuid.uuid4())
logger.info(f"Generated job ID: {task_id}")
# 1) Parse config content (parse in memory without saving first)
if config_file and config_file.filename != '':
@ -69,13 +71,14 @@ def upload_file():
with open(default_config_path, 'r', encoding='utf-8') as f:
active_config = yaml.safe_load(f)
# 2) Create job directories based on config['output_dir']
output_base = Path(active_config['output_dir']).expanduser()
job_upload_dir = output_base / "uploads" / job_id
job_output_dir = output_base / "outputs" / job_id
# 2) Create job directories based on INI configuration
upload_base = Path(current_app.config['UPLOAD_FOLDER'])
output_base = Path(current_app.config['OUTPUT_FOLDER'])
job_upload_dir = upload_base / task_id
job_output_dir = output_base / task_id
job_upload_dir.mkdir(parents=True, exist_ok=True)
job_output_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"Job {job_id}: Created directories - Upload: {job_upload_dir}, Output: {job_output_dir}")
logger.info(f"Job {task_id}: Created directories - Upload: {job_upload_dir}, Output: {job_output_dir}")
# 3) Save data file to job_upload_dir
data_filename = secure_filename(data_file.filename)
@ -83,9 +86,9 @@ def upload_file():
try:
data_file.seek(0)
data_file.save(str(data_path))
logger.info(f"Job {job_id}: Data file saved successfully - Path: {data_path}")
logger.info(f"Job {task_id}: Data file saved successfully - Path: {data_path}")
except Exception as e:
logger.error(f"Job {job_id}: Failed to save data file {data_filename}: {str(e)}")
logger.error(f"Job {task_id}: Failed to save data file {data_filename}: {str(e)}")
return _format_response(500, "保存数据文件失败")
# 4) Save config file to job_upload_dir
@ -96,9 +99,9 @@ def upload_file():
config_file.seek(0)
config_file.save(str(config_path))
active_config_path = config_path
logger.info(f"Job {job_id}: Custom config saved successfully - Path: {config_path}")
logger.info(f"Job {task_id}: Custom config saved successfully - Path: {config_path}")
except Exception as e:
logger.error(f"Job {job_id}: Failed to save config file {config_filename}: {str(e)}")
logger.error(f"Job {task_id}: Failed to save config file {config_filename}: {str(e)}")
return _format_response(500, "保存配置文件失败")
else:
# Copy default config for record keeping
@ -106,29 +109,29 @@ def upload_file():
with open(config_path, 'w', encoding='utf-8') as f:
yaml.safe_dump(active_config, f, allow_unicode=True)
active_config_path = config_path
logger.info(f"Job {job_id}: Default config saved for record - Path: {config_path}")
logger.info(f"Job {task_id}: Default config saved for record - Path: {config_path}")
# Initialize task status
update_task_status(job_id, TASK_STATUS_PENDING, "Task queued for processing")
logger.info(f"Job {job_id}: Task status initialized as PENDING")
update_task_status(task_id, TASK_STATUS_PENDING, "任务已加入处理队列", output_dir=str(job_output_dir))
logger.info(f"Job {task_id}: Task status initialized as PENDING")
# Start background processing
try:
thread = threading.Thread(
target=process_data_async,
args=(job_id, data_path, active_config_path, job_output_dir)
args=(task_id, data_path, active_config_path, job_output_dir)
)
thread.daemon = True
thread.start()
logger.info(f"Job {job_id}: Background processing thread started successfully")
logger.info(f"Job {task_id}: Background processing thread started successfully")
except Exception as e:
logger.error(f"Job {job_id}: Failed to start background processing thread: {str(e)}")
update_task_status(job_id, TASK_STATUS_FAILED, error=str(e))
logger.error(f"Job {task_id}: Failed to start background processing thread: {str(e)}")
update_task_status(task_id, TASK_STATUS_FAILED, error=str(e))
return _format_response(500, "启动处理失败")
logger.info(f"Job {job_id}: Upload process completed successfully, returning job ID to client")
logger.info(f"Job {task_id}: Upload process completed successfully, returning job ID to client")
return _format_response(202, "任务已接受并加入处理队列", {
"status": "accepted",
"job_id": job_id,
"task_status_url": f"/task/{job_id}"
"task_id": task_id,
"task_status_url": f"/task/{task_id}"
})

View File

@ -78,6 +78,11 @@ def index():
<div class="upload-section">
<h2>新建处理任务</h2>
<form id="uploadForm" enctype=multipart/form-data>
<div class="form-group">
<label for="api_key">API Key (必需):</label>
<input type="password" name="api_key" id="api_key" placeholder="输入您的 API Key" required>
<small style="color: #666; font-size: 0.9em;">首次使用请先运行 <code>python create_api_key.py</code> 创建密钥</small>
</div>
<div class="form-group">
<label for="data_file">数据文件 (Excel):</label>
<input type="file" name="file" id="data_file" required>
@ -104,10 +109,10 @@ def index():
{% for report in reports %}
<div class="report-item">
<div class="report-info">
<a class="report-link" href="/download/{{ report }}" target="_blank">{{ report.split('/')[-1] }}</a>
<a class="report-link" href="/download/{{ report }}" target="_blank" onclick="addApiKeyToLink(this)">{{ report.split('/')[-1] }}</a>
<span class="report-path">任务 ID: {{ report.split('/')[0] }}</span>
</div>
<a href="/download/{{ report }}" download class="report-link" style="font-size: 0.9em;">下载</a>
<a href="/download/{{ report }}" download class="report-link" style="font-size: 0.9em;" onclick="addApiKeyToLink(this)">下载</a>
</div>
{% else %}
<p style="color: #95a5a6;">暂无已生成的报告。</p>
@ -117,12 +122,14 @@ def index():
<div class="api-docs">
<h3>API 调用指南 (开发者)</h3>
<p><strong>⚠️ 重要提醒:</strong> 所有 API 接口都需要 API Key 认证。请先运行 <code>python create_api_key.py</code> 创建密钥。</p>
<p><strong>认证方式:</strong> 在请求头中添加 <code>X-API-Key: &lt;your_key&gt;</code></p>
<p><strong>健康检查:</strong> <code>GET /health</code></p>
<p><strong>上传分析:</strong> <code>POST /upload</code></p>
<p><strong>查询任务状态:</strong> <code>GET /task/&lt;task_id&gt;</code></p>
<p>参数: <code>file</code> (Excel), <code>config</code> (YAML, 可选)</p>
<p>示例: <code>curl -X POST -F "file=@data.xlsx" http://localhost:5000/upload</code></p>
<p>状态查询: <code>curl http://localhost:5000/task/your-task-id</code></p>
<p>示例: <code>curl -X POST -H "X-API-Key: your_key" -F "file=@data.xlsx" http://localhost:5000/upload</code></p>
<p>状态查询: <code>curl -H "X-API-Key: your_key" http://localhost:5000/task/your-task-id</code></p>
</div>
<script>
@ -130,6 +137,7 @@ def index():
e.preventDefault();
const formData = new FormData(this);
const apiKey = document.getElementById('api_key').value;
const submitBtn = document.getElementById('submitBtn');
const taskStatus = document.getElementById('taskStatus');
const statusMessage = document.getElementById('statusMessage');
@ -137,6 +145,12 @@ def index():
const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');
// Check API key
if (!apiKey) {
alert('请先输入 API Key');
return;
}
// Disable form and show status
submitBtn.disabled = true;
submitBtn.textContent = '上传中...';
@ -149,6 +163,9 @@ def index():
statusMessage.textContent = '正在上传文件...';
const response = await fetch('/upload', {
method: 'POST',
headers: {
'X-API-Key': apiKey
},
body: formData
});
@ -157,7 +174,7 @@ def index():
}
const result = await response.json();
const taskId = result.data.job_id;
const taskId = result.data.task_id || result.data.job_id;
statusMessage.textContent = '文件上传成功,开始处理数据...';
taskIdElement.textContent = `任务ID: ${taskId}`;
@ -169,7 +186,11 @@ def index():
const pollStatus = async () => {
try {
const statusResponse = await fetch(`/task/${taskId}`);
const statusResponse = await fetch(`/task/${taskId}`, {
headers: {
'X-API-Key': apiKey
}
});
const status = await statusResponse.json();
if (status.data.status === 'completed') {
@ -226,6 +247,16 @@ def index():
submitBtn.disabled = false;
}
});
// Function to add API key to download links
function addApiKeyToLink(link) {
const apiKey = document.getElementById('api_key').value;
if (apiKey) {
const url = new URL(link.href, window.location.origin);
url.searchParams.set('api_key', apiKey);
link.href = url.toString();
}
}
</script>
</div>
</body>