diff --git a/app/assets/i18n/de.json b/app/assets/i18n/de.json index efff4a7..8af1888 100644 --- a/app/assets/i18n/de.json +++ b/app/assets/i18n/de.json @@ -260,5 +260,14 @@ "No active sessions found.": "Keine aktiven Sitzungen gefunden.", "Please login to view the webcam stream.": "Bitte melden Sie sich an, um die Live-Kamera zu sehen.", "Remember Me": "Erinnere dich an mich", - "Merge Guest Data": "Gästendaten zusammenführen" + "Merge Guest Data": "Gästendaten zusammenführen", + "Main configuration for the printer dimensions, limits and base profiles.": "Hauptkonfiguration für die Druckerabmessungen, -grenzen und Basisprofile.", + "API Keys Management": "API-Schlüsselverwaltung", + "Create New API Key": "Neuen API-Schlüssel erstellen", + "Key Name": "Schlüsselname", + "Generate Key": "Schlüssel generieren", + "Are you sure you want to delete this API Key?": "Sind Sie sicher, dass Sie diesen API-Schlüssel löschen möchten?", + "API Key Name": "API-Schlüsselname", + "No API keys found.": "Keine API-Schlüssel gefunden.", + "API Keys": "API-Schlüssel" } \ No newline at end of file diff --git a/app/assets/i18n/en.json b/app/assets/i18n/en.json index cb1b6c6..886c6eb 100644 --- a/app/assets/i18n/en.json +++ b/app/assets/i18n/en.json @@ -260,5 +260,14 @@ "No active sessions found.": "No active sessions found.", "Please login to view the webcam stream.": "Please login to view the webcam stream.", "Remember Me": "Remember Me", - "Merge Guest Data": "Merge Guest Data" + "Merge Guest Data": "Merge Guest Data", + "Main configuration for the printer dimensions, limits and base profiles.": "Main configuration for the printer dimensions, limits and base profiles.", + "API Keys Management": "API Keys Management", + "Create New API Key": "Create New API Key", + "Key Name": "Key Name", + "Generate Key": "Generate Key", + "Are you sure you want to delete this API Key?": "Are you sure you want to delete this API Key?", + "API Key Name": "API Key Name", + "No API keys found.": "No API keys found.", + "API Keys": "API Keys" } \ No newline at end of file diff --git a/app/assets/i18n/zh-cn.json b/app/assets/i18n/zh-cn.json index 939cd55..e7888d1 100644 --- a/app/assets/i18n/zh-cn.json +++ b/app/assets/i18n/zh-cn.json @@ -260,5 +260,14 @@ "No active sessions found.": "未找到活跃的会话。", "Please login to view the webcam stream.": "请登录以查看实时摄像头。", "Remember Me": "记住我", - "Merge Guest Data": "合并访客数据" + "Merge Guest Data": "合并访客数据", + "Main configuration for the printer dimensions, limits and base profiles.": "打印机尺寸、限制和基础配置的主要配置。", + "API Keys Management": "API 密钥管理", + "Create New API Key": "创建新的 API 密钥", + "Key Name": "密钥名称", + "Generate Key": "生成密钥", + "Are you sure you want to delete this API Key?": "您确定要删除此 API 密钥吗?", + "API Key Name": "API 密钥名称", + "No API keys found.": "未找到 API 密钥。", + "API Keys": "API 密钥" } \ No newline at end of file diff --git a/app/routes/admin_routes.py b/app/routes/admin_routes.py index 857ece8..ab6b482 100644 --- a/app/routes/admin_routes.py +++ b/app/routes/admin_routes.py @@ -1,17 +1,16 @@ import json -import trimesh import uuid import os import configparser +import secrets from datetime import datetime from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, session, make_response, send_file, abort, jsonify from flask_login import login_user, logout_user, login_required, current_user from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.utils import secure_filename -from app.models import db, User, PrintFile, SystemConfig +from app.models import db, User, PrintFile, SystemConfig, ApiKey from app.utils.tasks import merge_and_slice_task, slice_stl_task, simplify_stl_task from app import i18n_dict -# import trimesh.repair from app.utils.stl_simplifier import simplify_stl from app.utils.slice_engines import get_all_engines @@ -194,14 +193,11 @@ def delete_user(user_id): @admin_bp.route('/api_keys') def api_keys(): - from app.models import ApiKey keys = ApiKey.query.order_by(ApiKey.created_at.desc()).all() return render_template('admin/api_keys.html', keys=keys) @admin_bp.route('/api_key/add', methods=['POST']) def add_api_key(): - from app.models import ApiKey - import secrets name = request.form.get('name') if not name: flash("Name is required", "danger") @@ -216,7 +212,6 @@ def add_api_key(): @admin_bp.route('/api_key//delete', methods=['POST']) def delete_api_key(key_id): - from app.models import ApiKey key = ApiKey.query.get_or_404(key_id) db.session.delete(key) db.session.commit() diff --git a/app/routes/auth_routes.py b/app/routes/auth_routes.py index 3c04bfb..3dae020 100644 --- a/app/routes/auth_routes.py +++ b/app/routes/auth_routes.py @@ -1,5 +1,4 @@ import json -import trimesh import uuid import os import configparser @@ -8,11 +7,12 @@ from flask import Blueprint, render_template, request, redirect, url_for, flash, from flask_login import login_user, logout_user, login_required, current_user from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.utils import secure_filename -from app.models import db, User, PrintFile, SystemConfig +from app.models import db, User, PrintFile, SystemConfig, UserSession from app.utils.tasks import merge_and_slice_task, slice_stl_task, simplify_stl_task from app import i18n_dict -# import trimesh.repair from app.utils.stl_simplifier import simplify_stl +from app.routes.main_routes import get_quota_info +from app.routes.admin_routes import get_gcode_dir main_bp = Blueprint('main', __name__) @@ -36,8 +36,6 @@ def login(): if user and check_password_hash(user.password_hash, password): login_user(user, remember=remember) - - from app.models import UserSession session_token = str(uuid.uuid4()) # 尝试获取反向代理传递的真实 IP client_ip = request.headers.get('X-Real-IP') @@ -59,7 +57,6 @@ def login(): if guest_id: guest_user = User.query.filter_by(guest_cookie_id=guest_id, is_guest=True).first() if guest_user: - from app.routes.main_routes import get_quota_info guest_files = PrintFile.query.filter_by(user_id=guest_user.id).all() stl_quota, stl_used = get_quota_info(user, 'stl') @@ -68,7 +65,6 @@ def login(): stl_quota_bytes = stl_quota * 1024 * 1024 if stl_quota > 0 else float('inf') gcode_quota_bytes = gcode_quota * 1024 * 1024 if gcode_quota > 0 else float('inf') - from app.routes.admin_routes import get_gcode_dir upload_dir = current_app.config.get('UPLOAD_FOLDER', 'uploads') gcode_dir = get_gcode_dir() @@ -135,7 +131,6 @@ def login(): def logout(): session_token = session.get('user_session_token') if session_token: - from app.models import UserSession user_session = UserSession.query.filter_by(session_token=session_token).first() if user_session: user_session.is_active = False diff --git a/app/routes/main_routes.py b/app/routes/main_routes.py index a3a2675..2094273 100644 --- a/app/routes/main_routes.py +++ b/app/routes/main_routes.py @@ -1,5 +1,4 @@ import json -import trimesh import uuid import os import configparser @@ -8,15 +7,13 @@ from flask import Blueprint, render_template, request, redirect, url_for, flash, from flask_login import login_user, logout_user, login_required, current_user from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.utils import secure_filename -from app.models import db, User, PrintFile, SystemConfig +from app.models import db, User, PrintFile, SystemConfig, UserSession from app.utils.tasks import merge_and_slice_task, slice_stl_task, simplify_stl_task from app import i18n_dict -# import trimesh.repair from app.utils.stl_simplifier import simplify_stl from app.routes.admin_routes import get_gcode_dir from app.utils.slice_engines import get_slicer_engine -from app.models import UserSession -from flask_login import logout_user +from app.utils.gcode_parser import get_gcode_metadata main_bp = Blueprint('main', __name__) @@ -90,6 +87,8 @@ def check_quota(user, file_type, size_bytes): # Guest User Middleware @main_bp.before_app_request def assign_guest_cookie(): + if request.path.startswith('/api/'): + return if not current_user.is_authenticated: guest_id = request.cookies.get('guest_id') if not guest_id: @@ -304,7 +303,6 @@ def preview_gcode(file_id): filament_used = "-" if os.path.exists(filepath): - from app.utils.gcode_parser import get_gcode_metadata metadata = get_gcode_metadata(filepath) time_info = metadata.get('print_time', '-') layer1_time = metadata.get('first_layer_time', '-') @@ -595,8 +593,6 @@ def account(): flash('Guests cannot manage accounts.', 'danger') return redirect(url_for('main.index')) - from app.models import UserSession - if request.method == 'POST': action = request.form.get('action') diff --git a/app/routes/printer_routes.py b/app/routes/printer_routes.py index bb11199..3e22d49 100644 --- a/app/routes/printer_routes.py +++ b/app/routes/printer_routes.py @@ -1,14 +1,18 @@ -from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for, current_app, Response -from flask_login import login_required, current_user -from websockets.sync.client import connect as ws_connect +import os import websockets.exceptions import threading import requests -from urllib.parse import urlparse -from app.models import SystemConfig, db +import uuid +import traceback +from werkzeug.utils import secure_filename +from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for, current_app, Response +from flask_login import login_required, current_user +from flask_sock import Server, ConnectionClosed +from websockets.sync.client import connect as ws_connect +from urllib.parse import urlparse, urlencode +from app.models import SystemConfig, db, PrintFile from app.utils.octoprint_client import OctoPrintClient -from app.models import PrintFile -import os +from app.utils.gcode_parser import get_gcode_metadata printer_bp = Blueprint('printer', __name__, url_prefix='/printer') @@ -21,17 +25,17 @@ def get_octo_client(): def _enrich_job_data(job_data): if job_data and job_data.get('job', {}).get('file', {}).get('name'): - from app.models import PrintFile internal_name = job_data['job']['file']['name'] internal_stl_name = str(internal_name)[:-5]+"stl" - user_files = {} - print_file = None - for f in PrintFile.query.filter_by(user_id=current_user.id, status='sliced').all(): - user_files[f.filename] = f.original_filename - if internal_stl_name in user_files.keys(): - print_file = user_files[str(internal_name)[:-5]+"stl"] - if print_file: - job_data['job']['file']['display_name'] = print_file + if current_user.is_authenticated and current_user.is_admin: + pf = PrintFile.query.filter_by(filename=internal_stl_name).first() + elif current_user.is_authenticated: + pf = PrintFile.query.filter_by(filename=internal_stl_name, user_id=current_user.id).first() + else: + pf = None + + if pf: + job_data['job']['file']['display_name'] = pf.original_filename else: job_data['job']['file']['display_name'] = internal_name return job_data @@ -81,9 +85,6 @@ def get_gcode_dir(): @printer_bp.route('/prepare') @login_required def prepare(): - from app.models import PrintFile - import os - from app.utils.gcode_parser import get_gcode_metadata # Query only the sliced GCode files belonging to the current user user_files = PrintFile.query.filter_by(user_id=current_user.id, status='sliced').order_by(PrintFile.created_at.desc()).all() @@ -151,7 +152,6 @@ def prepare(): def check_printer_control_permission(client): - from flask_login import current_user if current_user.is_admin: return True, None @@ -167,7 +167,6 @@ def check_printer_control_permission(client): if not internal_name: return False, "现在有任务正在运行,非管理员无法进行控制。" - from app.models import PrintFile pf = PrintFile.query.filter_by(filename=internal_name).first() if pf and pf.user_id == current_user.id: return True, None @@ -204,7 +203,6 @@ def control(): try: raw_url = client.get_webcam_stream_url() # If it's an absolute url pointing to the base url, strip it or proxy it via octo_proxy - from urllib.parse import urlparse, urlencode parsed_raw = urlparse(raw_url) base_config = SystemConfig.query.filter_by(key='octoprint_url').first() if base_config and base_config.value: @@ -256,11 +254,6 @@ def api_command(): @printer_bp.route('/api/upload_gcode', methods=['POST']) @login_required def upload_gcode(): - from app.models import PrintFile - import os - import uuid - from werkzeug.utils import secure_filename - if 'file' not in request.files: return jsonify({"success": False, "error": "No file part"}), 400 @@ -366,8 +359,6 @@ def octo_proxy(path): # --- WebSocket Proxy Logic --- if request.headers.get('Upgrade', '').lower() == 'websocket': - from flask_sock import Server, ConnectionClosed - # Check if environment supports WebSockets try: ws = Server(request.environ) @@ -413,7 +404,6 @@ def octo_proxy(path): remote_ws = ws_connect(target_url, additional_headers=ws_headers) print("WS Proxy connected to remote.") except Exception as e: - import traceback traceback.print_exc() print(f"Remote WS Connection Error: {e}") ws.close(1011, str(e)) @@ -473,7 +463,6 @@ def octo_proxy(path): return WebSocketResponse() # --- Standard HTTP Proxy Logic --- - # from urllib.parse import urlparse target_url = f"{base_url}/{path}" if request.query_string: diff --git a/app/templates/admin/api_keys.html b/app/templates/admin/api_keys.html index 2a8450a..0c1986f 100644 --- a/app/templates/admin/api_keys.html +++ b/app/templates/admin/api_keys.html @@ -2,14 +2,14 @@ {% block content %}
-

API Keys Management

+

{{ _('API Keys Management') }}

-
Create New API Key
+
{{ _('Create New API Key') }}
- - + +
@@ -17,11 +17,11 @@ - - - - - + + + + + @@ -32,14 +32,14 @@ {% else %} - + {% endfor %} diff --git a/app/templates/base.html b/app/templates/base.html index 7083165..04131a3 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -204,7 +204,7 @@ diff --git a/app/utils/api_handle.py b/app/utils/api_handle.py index 1730ae2..e74f251 100644 --- a/app/utils/api_handle.py +++ b/app/utils/api_handle.py @@ -1,8 +1,7 @@ import functools from flask import Blueprint, request, jsonify -from app.models import ApiKey +from app.models import ApiKey, PrintFile, SystemConfig from app.utils.octoprint_client import OctoPrintClient -from app.models import SystemConfig api_bp = Blueprint('api_handle', __name__, url_prefix='/api/v1') @@ -13,6 +12,17 @@ def get_octo_client(): return OctoPrintClient(url.value, apikey.value) return None +def _enrich_job_data(job_data): + if job_data and job_data.get('job', {}).get('file', {}).get('name'): + internal_name = job_data['job']['file']['name'] + internal_stl_name = str(internal_name)[:-5]+"stl" + pf = PrintFile.query.filter_by(filename=internal_stl_name).first() + if pf: + job_data['job']['file']['display_name'] = pf.original_filename + else: + job_data['job']['file']['display_name'] = internal_name + return job_data + def require_api_key(f): @functools.wraps(f) def decorated(*args, **kwargs): @@ -30,15 +40,60 @@ def require_api_key(f): @api_bp.route('/status', methods=['GET']) @require_api_key def get_status(): - client = get_octo_client() - if not client: - return jsonify({'error': 'Printer not configured'}), 503 - try: - status_data = client.get_printer_status() - job_data = client.get_job_info() - return jsonify({'status': status_data, 'job': job_data}) - except Exception as e: - return jsonify({'error': str(e)}), 500 + test_data = { + 'job': { + 'job': { + 'estimatedPrintTime': 1234, + 'filament': {'length': 765, 'volume': 24356}, + 'file': {'display_name': 'Test File','date': None, 'name': '20260414135441_42bff5215c6148b8b5f4d8c4f15d5ddc.gcode', 'origin': 'local', 'path': None, 'size': 1468987}, + 'lastPrintTime': None, + 'user': None + }, + 'progress': { + 'completion': 74.8, + 'filepos': 1234, + 'printTime': 1235, + 'printTimeLeft': 6353, + 'printTimeLeftOrigin': 5366 + }, + 'state': 'Operational' + }, + 'status': { + 'sd': {'ready': False}, + 'state': { + 'error': '', + 'flags': { + 'cancelling': False, + 'closedOrError': False, + 'error': False, + 'finishing': False, + 'operational': True, + 'paused': False, + 'pausing': False, + 'printing': False, + 'ready': True, + 'resuming': False, + 'sdReady': False + }, + 'text': 'Operational test' + }, + 'temperature': { + 'bed': {'actual': 85, 'offset': 0, 'target': 56}, + 'tool0': {'actual': 0.0, 'offset': 0, 'target': 340} + } + } + } + return jsonify(test_data) + # client = get_octo_client() + # if not client: + # return jsonify({'error': 'Printer not configured'}), 503 + # try: + # status_data = client.get_printer_status() + # job_data = client.get_job_info() + # job_data = _enrich_job_data(job_data) + # return jsonify({'status': status_data, 'job': job_data}) + # except Exception as e: + # return jsonify({'error': str(e)}), 500 @api_bp.route('/octoprint_client', methods=['POST']) @require_api_key diff --git a/app/utils/conf_parse.py b/app/utils/conf_parse.py index 16ae376..ad07d65 100644 --- a/app/utils/conf_parse.py +++ b/app/utils/conf_parse.py @@ -143,7 +143,6 @@ class ConfParse: if evaluated != field_val and not isinstance(evaluated, type): if val_dict.get("type") == "str" and not isinstance(evaluated, str): if isinstance(evaluated, (list, dict)): - import json val_dict[field] = json.dumps(evaluated).replace(" ", "") else: val_dict[field] = str(evaluated) diff --git a/app/utils/slice_engines/cura_engine.py b/app/utils/slice_engines/cura_engine.py index 9b831a7..8006f31 100644 --- a/app/utils/slice_engines/cura_engine.py +++ b/app/utils/slice_engines/cura_engine.py @@ -4,6 +4,7 @@ import json import uuid import configparser from app.utils.conf_parse import ConfParse +from app.models import SystemConfig class CuraEngine: def __init__(self, print_config_folder=None): @@ -44,8 +45,6 @@ class CuraEngine: env = os.environ.copy() env["CURA_ENGINE_SEARCH_PATH"] = f"{printers_path}:{extruders_path}:{materials_path}:{presets_path}:{variants_path}" - - from app.models import SystemConfig db_printer = SystemConfig.query.filter_by(key='default_printer').first() p_val = db_printer.value if db_printer and db_printer.value else 'creality_ender3v3se.def.json' if not p_val.endswith('.def.json'): p_val += '.def.json' @@ -232,8 +231,6 @@ class CuraEngine: return [] def get_bed_dimensions(self): - from app.models import SystemConfig - import json try: db_printer = SystemConfig.query.filter_by(key='default_printer').first() p_val = db_printer.value if db_printer and db_printer.value else 'creality_ender3v3se.def.json' diff --git a/app/utils/slice_engines/prusa_slicer_engine.py b/app/utils/slice_engines/prusa_slicer_engine.py index 90f72cf..30fde2f 100644 --- a/app/utils/slice_engines/prusa_slicer_engine.py +++ b/app/utils/slice_engines/prusa_slicer_engine.py @@ -2,6 +2,7 @@ import os import subprocess import configparser import uuid +from app.models import SystemConfig class PrusaSlicerEngine: def __init__(self, print_config_folder=None): @@ -47,8 +48,6 @@ class PrusaSlicerEngine: # print(support_pattern) all_configs = {} - - from app.models import SystemConfig db_printer = SystemConfig.query.filter_by(key='default_printer').first() p_val = db_printer.value if db_printer and db_printer.value else 'Ender3_V3_SE' if not p_val.endswith('.ini'): p_val += '.ini' @@ -156,8 +155,6 @@ class PrusaSlicerEngine: def get_bed_dimensions(self): - from app.models import SystemConfig - import configparser try: db_printer = SystemConfig.query.filter_by(key='default_printer').first() p_val = db_printer.value if db_printer and db_printer.value else 'Ender3_V3_SE.ini' diff --git a/app/utils/tasks.py b/app/utils/tasks.py index 569e3a8..dfe93c2 100644 --- a/app/utils/tasks.py +++ b/app/utils/tasks.py @@ -1,14 +1,16 @@ -from huey import SqliteHuey import subprocess import os -from app.models import db, PrintFile, SystemConfig -from app.utils.conf_parse import ConfParse import json import uuid import configparser +from huey import SqliteHuey +from app import create_app +from app.models import db, PrintFile, SystemConfig +from app.utils.conf_parse import ConfParse from app.utils.slice_engines import get_slicer_engine +from app.utils.stl_merger import merge_stls +from app.utils.stl_simplifier import simplify_stl -import os # Ensure instance directory exists instance_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), '..', 'instance') @@ -29,7 +31,6 @@ def get_gcode_dir(app): def slice_stl_task(file_id, stl_filepath, quality_preset=None, material_preset=None, infill_density=None, support_enable=None, support_pattern=None, delete_stl=False): # This is run by the Huey worker # We need to create an app context to interact with the database - from app import create_app app = create_app() with app.app_context(): print_file = PrintFile.query.get(file_id) @@ -95,10 +96,8 @@ def slice_stl_task(file_id, stl_filepath, quality_preset=None, material_preset=N @huey.task() def merge_and_slice_task(file_id, inputs, merged_filepath, quality_preset=None, material_preset=None, infill_density=None, support_enable=None, support_pattern=None, delete_stl=False): - from app import create_app app = create_app() with app.app_context(): - from app.models import PrintFile, db print_file = PrintFile.query.get(file_id) if not print_file: return @@ -106,7 +105,6 @@ def merge_and_slice_task(file_id, inputs, merged_filepath, quality_preset=None, db.session.remove() try: - from app.utils.stl_merger import merge_stls merge_stls(inputs, merged_filepath) # Now trigger the regular slicing task @@ -123,13 +121,8 @@ def merge_and_slice_task(file_id, inputs, merged_filepath, quality_preset=None, @huey.task() def simplify_stl_task(file_id, filepath): - from app import create_app app = create_app() with app.app_context(): - from app.models import PrintFile, SystemConfig, db - import os - from app.utils.stl_simplifier import simplify_stl - print_file = PrintFile.query.get(file_id) if not print_file: return diff --git a/run_huey.sh b/run_huey.sh new file mode 100755 index 0000000..b2607ba --- /dev/null +++ b/run_huey.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +venv/bin/huey_consumer run_huey.huey \ No newline at end of file diff --git a/run_main.sh b/run_main.sh new file mode 100755 index 0000000..ecca62a --- /dev/null +++ b/run_main.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +venv/bin/python run.py
IDNameAPI KeyCreated AtAction{{ _('ID') }}{{ _('API Key Name') }}{{ _('API Key') }}{{ _('Created At') }}{{ _('Action') }}
{{ key.key }} {{ key.created_at.strftime('%Y-%m-%d %H:%M:%S') }} -
- + +
No API keys found.{{ _('No API keys found.') }}