暂存-说明文档(部分)

This commit is contained in:
2026-05-16 00:45:51 +08:00
parent 91bedce2d7
commit 9c8de5e664
63 changed files with 2818 additions and 92 deletions

View File

@@ -56,8 +56,10 @@ def create_app():
app.config['REMEMBER_COOKIE_NAME'] = 'aio_remember'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///../instance/aio_3d.db'
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {'connect_args': {'timeout': 15}}
app.config['UPLOAD_FOLDER'] = os.path.abspath(os.path.join(app.root_path, '..', 'uploads'))
app.config['PRINT_CONFIG_FOLDER'] = os.path.abspath(os.path.join(app.root_path, '..', 'print_config'))
app.config['UPLOAD_FOLDER'] = os.environ.get('UPLOAD_FOLDER', os.path.abspath(os.path.join(app.root_path, '..', 'uploads')))
app.config['PRINT_CONFIG_FOLDER'] = os.environ.get('PRINT_CONFIG_FOLDER', os.path.abspath(os.path.join(app.root_path, '..', 'print_config')))
app.config['PRUSA_SLICE_BIN'] = os.environ.get('PRUSA_SLICE_BIN', os.path.abspath(os.path.join(app.root_path, '..', 'prusaslicer', 'PrusaSlicer-2.9.4-aarch64-full.AppImage')))
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

View File

@@ -0,0 +1,58 @@
# Drucker-Helfer — Kurzanleitung
## Inhaltsverzeichnis
- Druckerstatus
- Druck vorbereiten
- Steuerung
- Drucker-Helfer (diese Seite)
- Systemkonfiguration (Admin)
- OctoPrint-Panel (Admin)
---
## Druckerstatus
Zeigt aktuellen Druckerzustand, Temperaturen und aktive Aufgaben an.
![Druckerstatus Platzhalterbild](assets/doc/images/printer_status_de.png)
---
## Druck vorbereiten
GCode an den Drucker senden, Temperaturen setzen und mit `Druck vorbereiten` bzw. `Jetzt drucken` starten.
![Druck vorbereiten Platzhalterbild](assets/doc/images/printer_prepare_de.png)
---
## Steuerung
Manuelle Grundsteuerungen: Achsen homing, Düsen/Betten bewegen, Pause/Fortsetzen, Druck abbrechen.
![Steuerung Platzhalterbild](assets/doc/images/printer_control_de.png)
---
## Drucker-Helfer (diese Seite)
Tipps zur Fehlerbehebung (Netzwerk, Filament, Bettleveling) und Checkliste vor dem Drucken.
![Drucker Helfer Platzhalterbild](assets/doc/images/printer_helper_de.png)
---
## Systemkonfiguration (Admin)
Admin-Einstellungen für Druckerabmessungen, Limits, Basisprofile und Verbindungsdaten.
![Systemkonfiguration Platzhalterbild](assets/doc/images/printer_system_de.png)
---
## OctoPrint-Panel (Admin)
Eingebettetes OctoPrint-Panel: `OctoPrint Basis-URL` und API-Key konfigurieren, Live-Panel verwenden.
![OctoPrint Platzhalterbild](assets/doc/images/printer_octoprint_de.png)

View File

@@ -0,0 +1,64 @@
# Printer Helper — Quick Guide
## Table of Contents
- Printer Status
- Prepare Print
- Control
- Printer Helper (this page)
- System Configuration (Admin)
- OctoPrint Panel (Admin)
---
## Printer Status
Shows current printer state, temperatures and active job information.
![Printer Status placeholder image](assets/doc/images/printer_status_en.png)
Use this page to monitor `Printer Status` and `Active Print Job`.
---
## Prepare Print
Send prepared GCode to the printer, set temperatures and start a print using `Prepare Print` and `Print Now`.
![Prepare Print placeholder image](assets/doc/images/printer_prepare_en.png)
---
## Control
Basic manual controls: home axes, move nozzle/bed, pause/resume and cancel print.
![Control placeholder image](assets/doc/images/printer_control_en.png)
---
## Printer Helper (this page)
Guides common troubleshooting steps (connectivity, filament, bed leveling) and quick checks before printing.
![Printer Helper placeholder image](assets/doc/images/printer_helper_en.png)
---
## System Configuration (Admin)
Admin-only settings for printer dimensions, limits, shared profiles and connection settings.
![System Config placeholder image](assets/doc/images/printer_system_en.png)
---
## OctoPrint Panel (Admin)
Embedded OctoPrint panel: configure `OctoPrint Base URL`, API key and use the live panel when available.
![OctoPrint Panel placeholder image](assets/doc/images/printer_octoprint_en.png)
---
If you want I can add annotated screenshots for specific printer models.

View File

@@ -0,0 +1,60 @@
# 打印助手 — 快速指南
## 目录
- 打印机状态
- 准备打印
- 控制
- 打印助手(本页)
- 系统配置(管理员)
- OctoPrint 面板(管理员)
---
## 打印机状态
显示当前打印机状态、温度和任务信息。
![打印机状态 占位图](assets/doc/images/printer_status_zh-cn.png)
可在此查看 `打印机状态``当前打印任务`
---
## 准备打印
将准备好的 GCode 发送到打印机,设置温度并使用 `准备打印``立即打印` 开始。
![准备打印 占位图](assets/doc/images/printer_prepare_zh-cn.png)
---
## 控制
手动控制:回原点、移动喷嘴/平台、暂停/恢复与取消打印。
![控制 占位图](assets/doc/images/printer_control_zh-cn.png)
---
## 打印助手(本页)
提供常见故障排查步骤(网络、挤出机、床平整)和打印前检查清单。
![打印助手 占位图](assets/doc/images/printer_helper_zh-cn.png)
---
## 系统配置(管理员)
管理员设置打印机尺寸、限制、基础配置和连接信息。
![系统配置 占位图](assets/doc/images/printer_system_zh-cn.png)
---
## OctoPrint 面板(管理员)
内嵌 OctoPrint 面板:配置 `OctoPrint 基础 URL`、API 密钥并使用可用的实时面板。
![OctoPrint 占位图](assets/doc/images/printer_octoprint_zh-cn.png)

View File

@@ -0,0 +1,82 @@
# Slice-Helfer — Kurzanleitung
## Inhaltsverzeichnis
- Startseite
- Meine Dateien
- Plater (Bauteilplatte)
- Konto-Verwaltung
- Slice-Helfer (diese Seite)
- Systemeinstellungen (Admin)
- Benutzerverwaltung (Admin)
- API-Schlüssel (Admin)
---
## Startseite
Übersicht des Slicer-Dashboards und schnelle Aktionen.
![Startseite Bild](../../assets/img/slice_helper/home_de.png)
Benutzen Sie die Navigation, um `Startseite` zu öffnen und über `STL Hochladen & Slicen` einen neuen Slice zu starten.
---
## Meine Dateien
Verwalten Sie hochgeladene STL- und GCode-Dateien: hochladen, herunterladen, löschen.
![Meine Dateien Bild](../../assets/img/slice_helper/my-files_de.png)
Wichtige Aktionen: `STL hochladen`, `GCode Herunterladen`, `Löschen`.
---
## Plater (Bauteilplatte)
Modelle auf der Bauteilplatte anordnen, verschieben, drehen und skalieren. Vor dem Slicen `Zusammenführen & Slicen`.
![Plater Bild](../../assets/img/slice_helper/plater_de.png)
---
## Konto-Verwaltung
Für angemeldete Benutzer: Profil, Passwort ändern und aktive Sitzungen verwalten.
![Konto Bild](../../assets/img/slice_helper/account-management_de.png)
---
## Slice-Helfer (diese Seite)
Erklärung der empfohlenen Slicing-Schritte: `Qualitätsprofil` wählen, `Support` und `Fülldichte` konfigurieren, dann `Hochladen & Slicen`.
![Slice Helfer Bild](../../assets/img/slice_helper/slice-helper_de.png)
Statusmeldungen: `Wartend`, `Slicen`, `Gesliced`, `Fehlgeschlagen`.
---
## Systemeinstellungen (Admin)
Admins konfigurieren globale Slicer-Engines und Standardprofile.
![Systemeinstellungen Bild](../../assets/img/slice_helper/system-settings_de.png)
---
## Benutzerverwaltung (Admin)
Admins können Benutzer hinzufügen/ändern und Quoten sowie Rollen setzen (`Benutzer`, `Admin`).
![Benutzerverwaltung Bild](../../assets/img/slice_helper/user-management_de.png)
---
## API-Schlüssel (Admin)
Verwalten Sie API-Schlüssel für externe Integrationen: `Neuen API-Schlüssel erstellen` und `Schlüssel generieren`.
![API Schlüssel Bild](../../assets/img/slice_helper/api-keys_de.png)

View File

@@ -0,0 +1,88 @@
# Slice Helper — Quick Guide
## Table of Contents
- Home
- My Files
- Plater (Build Plate)
- Account Management
- Slice Helper (this page)
- System Settings (Admin)
- User Management (Admin)
- API Keys (Admin)
---
## Home
Overview of the slicer dashboard and quick actions.
![Home image](../../assets/img/slice_helper/home_en.png)
Use the top navigation to open `Home` and start a new slice via `Upload & Slice STL`.
---
## My Files
Manage uploaded STL and GCode files. You can upload, delete and download sliced GCode.
![My Files image](../../assets/img/slice_helper/my-files_en.png)
Common actions: `Upload STL`, `Download GCode`, `Delete`.
---
## Plater (Build Plate)
Arrange models on the build plate before slicing. Use translate/rotate/scale tools and `Merge & Slice`.
![Plater image](../../assets/img/slice_helper/plater_en.png)
Tip: ensure all models fit the printable area before slicing.
---
## Account Management
Available when logged in. Update profile, change password, and manage active sessions.
![Account image](../../assets/img/slice_helper/account-management_en.png)
---
## Slice Helper (this page)
This page explains slice workflows and recommended settings. Choose a `Quality Profile`, set `Support` and `Infill Density` then `Upload & Slice`.
![Slice Helper image](../../assets/img/slice_helper/slice-helper_en.png)
Status messages: `Waiting`, `Slicing`, `Sliced`, `Failed`.
---
## System Settings (Admin)
Admins can configure global slicer engines and default profiles under `System Settings`.
![System Settings image](../../assets/img/slice_helper/system-settings_en.png)
---
## User Management (Admin)
Admins can add/edit users, set quotas and roles (`User`, `Admin`).
![User Management image](../../assets/img/slice_helper/user-management_en.png)
---
## API Keys (Admin)
Manage API keys used by external tools. `Create New API Key`, name it and `Generate Key`.
![API Keys image](../../assets/img/slice_helper/api-keys_en.png)
---
If you need example workflows or screenshots, tell me which page to expand.

View File

@@ -0,0 +1,84 @@
# 切片助手 — 快速指南
## 目录
- 主页
- 我的文件
- 构建板 (Plater)
- 账号管理
- 切片助手(本页)
- 系统设置(管理员)
- 用户管理(管理员)
- API 密钥(管理员)
---
## 主页
切片仪表盘概览与快速操作入口。
![主页 图片](../../assets/img/slice_helper/home_zh-cn.png)
使用导航栏进入“主页”,通过 `上传并切片 STL` 开始新切片。
---
## 我的文件
管理已上传的 STL 与 GCode可上传、下载或删除文件。
![我的文件 图片](../../assets/img/slice_helper/my-files_zh-cn.png)
常用操作:`上传STL``下载 GCode``删除`
---
## 构建板 (Plater)
在构建板上放置与调整模型(平移/旋转/缩放),确认位置后使用 `合并并切片`
![构建板 图片](../../assets/img/slice_helper/plater_zh-cn.png)
提示:切片前确保模型均在可打印范围内。
---
## 账号管理
登录用户可在此更新资料、修改密码并管理活跃会话。
![账号管理 图片](../../assets/img/slice_helper/account-management_zh-cn.png)
---
## 切片助手(本页)
本页说明推荐的切片流程与设置:选择 `质量配置`、设置 `支撑``填充密度`,然后 `上传 & 切片`
![切片助手 图片](../../assets/img/slice_helper/slice-helper_zh-cn.png)
状态提示:`等待中``切片中``已切片``失败`
---
## 系统设置(管理员)
管理员可在此配置全局切片引擎与默认配置文件。
![系统设置 图片](../../assets/img/slice_helper/system-settings_zh-cn.png)
---
## 用户管理(管理员)
管理员可添加/编辑用户并设置配额与角色(`普通用户``管理员`)。
![用户管理 图片](../../assets/img/slice_helper/user-management_zh-cn.png)
---
## API 密钥(管理员)
管理外部工具使用的 API 密钥;点击 `创建新的 API 密钥`,输入名称并 `生成密钥`
![API 密钥 图片](../../assets/img/slice_helper/api-keys_zh-cn.png)

View File

@@ -269,5 +269,13 @@
"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"
"API Keys": "API-Schlüssel",
"Slice Helper": "Slice-Helfer",
"Printer Helper": "Drucker-Helfer",
"For security reasons, please change your default admin password.": "Aus Sicherheitsgründen ändern Sie bitte Ihr Standard-Administratorpasswort.",
"Your new password cannot be the default \"admin123\".": "Ihr neues Passwort darf nicht das Standardpasswort \"admin123\" sein.",
"Current password is incorrect.": "Das aktuelle Passwort ist falsch.",
"New passwords do not match.": "Die neuen Passwörter stimmen nicht überein.",
"New password must be at least 6 characters.": "Das neue Passwort muss mindestens 6 Zeichen lang sein.",
"Password updated successfully.": "Passwort erfolgreich aktualisiert."
}

View File

@@ -269,5 +269,13 @@
"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"
"API Keys": "API Keys",
"Slice Helper": "Slice Helper",
"Printer Helper": "Printer Helper",
"For security reasons, please change your default admin password.": "For security reasons, please change your default admin password.",
"Your new password cannot be the default \"admin123\".": "Your new password cannot be the default \"admin123\".",
"Current password is incorrect.": "Current password is incorrect.",
"New passwords do not match.": "New passwords do not match.",
"New password must be at least 6 characters.": "New password must be at least 6 characters.",
"Password updated successfully.": "Password updated successfully."
}

View File

@@ -269,5 +269,13 @@
"Are you sure you want to delete this API Key?": "您确定要删除此 API 密钥吗?",
"API Key Name": "API 密钥名称",
"No API keys found.": "未找到 API 密钥。",
"API Keys": "API 密钥"
"API Keys": "API 密钥",
"Slice Helper": "切片助手",
"Printer Helper": "打印助手",
"For security reasons, please change your default admin password.": "出于安全原因,请修改您的默认管理员密码。",
"Your new password cannot be the default \"admin123\".": "新密码不能设置为系统默认的 \"admin123\"。",
"Current password is incorrect.": "当前密码不正确。",
"New passwords do not match.": "新密码不匹配。",
"New password must be at least 6 characters.": "新密码必须至少6个字符。",
"Password updated successfully.": "密码更新成功。"
}

BIN
app/assets/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
app/assets/img/favicon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
app/assets/img/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

View File

@@ -90,12 +90,40 @@ def settings():
def users():
all_users = User.query.order_by(User.created_at.desc()).all()
user_quotas = {}
# Load defaults
def_guest_stl = SystemConfig.query.filter_by(key="default_guest_stl_quota_mb").first()
def_guest_stl_val = def_guest_stl.value if def_guest_stl else '0'
def_guest_gcode = SystemConfig.query.filter_by(key="default_guest_gcode_quota_mb").first()
def_guest_gcode_val = def_guest_gcode.value if def_guest_gcode else '0'
def_user_stl = SystemConfig.query.filter_by(key="default_user_stl_quota_mb").first()
def_user_stl_val = def_user_stl.value if def_user_stl else '0'
def_user_gcode = SystemConfig.query.filter_by(key="default_user_gcode_quota_mb").first()
def_user_gcode_val = def_user_gcode.value if def_user_gcode else '0'
for u in all_users:
if u.is_admin:
eff_stl = '0'
eff_gcode = '0'
elif u.is_guest:
eff_stl = def_guest_stl_val
eff_gcode = def_guest_gcode_val
else:
eff_stl = def_user_stl_val
eff_gcode = def_user_gcode_val
sq = SystemConfig.query.filter_by(key=f"user_{u.id}_stl_quota_mb").first()
gq = SystemConfig.query.filter_by(key=f"user_{u.id}_gcode_quota_mb").first()
user_stl = sq.value if sq else '0'
user_gcode = gq.value if gq else '0'
user_quotas[u.id] = {
'stl': sq.value if sq else '0',
'gcode': gq.value if gq else '0'
'stl': user_stl,
'gcode': user_gcode,
'eff_stl': eff_stl if user_stl == '0' else user_stl,
'eff_gcode': eff_gcode if user_gcode == '0' else user_gcode,
}
return render_template('admin/users.html', users=all_users, user_quotas=user_quotas)

View File

@@ -35,6 +35,9 @@ def login():
if user and check_password_hash(user.password_hash, password):
# Clear old password check flag
session.pop('pwd_check_done', None)
session.pop('must_change_password', None)
login_user(user, remember=remember)
session_token = str(uuid.uuid4())
# 尝试获取反向代理传递的真实 IP

View File

@@ -21,6 +21,8 @@ main_bp = Blueprint('main', __name__)
def check_user_session():
if current_user.is_authenticated and not current_user.is_guest:
session_token = session.get('user_session_token')
client_ip = request.headers.get('X-Real-IP') or request.remote_addr
if session_token:
user_session = UserSession.query.filter_by(session_token=session_token).first()
if not user_session or not user_session.is_active:
@@ -30,7 +32,36 @@ def check_user_session():
return redirect(url_for('auth.login'))
else:
user_session.last_active = datetime.utcnow()
user_session.ip_address = client_ip
db.session.commit()
else:
# Re-authenticated via remember me, but no session token
new_session_token = str(uuid.uuid4())
user_session = UserSession(
user_id=current_user.id,
session_token=new_session_token,
ip_address=client_ip,
user_agent=request.user_agent.string,
last_active=datetime.utcnow()
)
db.session.add(user_session)
db.session.commit()
session['user_session_token'] = new_session_token
# Check default admin password securely without checking hash on every request
if current_user.is_admin:
if session.get('pwd_check_done') is None:
session['pwd_check_done'] = True
if check_password_hash(current_user.password_hash, 'admin123'):
session['must_change_password'] = True
else:
session.pop('must_change_password', None)
if session.get('must_change_password'):
if request.endpoint and request.endpoint not in ['main.account', 'auth.logout', 'static']:
flash('For security reasons, please change your default admin password.', 'warning')
return redirect(url_for('main.account'))
@@ -317,7 +348,7 @@ def preview_gcode(file_id):
engine_name = SystemConfig.query.filter_by(key='slicer_engine').first()
if engine_name:
engine = get_slicer_engine(str(engine_name.value), current_app.config['PRINT_CONFIG_FOLDER'])
engine = get_slicer_engine(str(engine_name.value), current_app.config['PRINT_CONFIG_FOLDER'], current_app.config['PRUSA_SLICE_BIN'])
w, h, hd = engine.get_bed_dimensions()
configs = {c.key: c.value for c in SystemConfig.query.all()}
offset_x = float(configs.get('offset_x', '0.0'))
@@ -365,7 +396,7 @@ def plater():
engine_name = SystemConfig.query.filter_by(key='slicer_engine').first()
if engine_name:
engine = get_slicer_engine(str(engine_name.value), current_app.config['PRINT_CONFIG_FOLDER'])
engine = get_slicer_engine(str(engine_name.value), current_app.config['PRINT_CONFIG_FOLDER'], current_app.config['PRUSA_SLICE_BIN'])
w, h, hd = engine.get_bed_dimensions()
print(f"Bed dimensions: {w}x{h}x{hd}")
@@ -384,6 +415,27 @@ def plater():
models = [{'id': f.id, 'name': f.original_filename, 'status': f.status, 'url': url_for('main.serve_proxy_file', file_id=f.id), 'transform_matrix': f.transform_matrix} for f in user_files]
return render_template('slice/plater.html', w=w, h=h, hd=hd, last_quality=default_quality, last_material=default_material, models=models, offset_x=offset_x, offset_y=offset_y, default_infill=default_infill, default_support=default_support, default_support_pattern=default_support_pattern, quota_exceeded=quota_exceeded, configs=configs)
import re
import markdown
@main_bp.route('/helper_slice')
def helper_slice():
lang = request.cookies.get('lang', 'en')
filepath = os.path.join(current_app.root_path, 'assets', 'doc', f'slice_helper_{lang}.md')
if not os.path.exists(filepath):
filepath = os.path.join(current_app.root_path, 'assets', 'doc', 'slice_helper_en.md')
content_html = ""
if os.path.exists(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
md_text = f.read()
content_html = markdown.markdown(md_text, extensions=['fenced_code', 'tables'])
# Rewrite relative image links to /assets/doc/
content_html = re.sub(r'src="(?!http|/)([^"]+)"', r'src="/assets/doc/\1"', content_html)
return render_template('slice/helper_slice.html', content_html=content_html)
@main_bp.route('/file/<int:file_id>')
@login_required
def serve_file(file_id):
@@ -579,7 +631,7 @@ def build_plate_model():
@main_bp.route('/api/engine_options/<engine_name>')
@login_required
def engine_options(engine_name):
engine = get_slicer_engine(engine_name, current_app.config['PRINT_CONFIG_FOLDER'])
engine = get_slicer_engine(engine_name, current_app.config['PRINT_CONFIG_FOLDER'], current_app.config['PRUSA_SLICE_BIN'])
presets = engine.get_quality_presets()
patterns = engine.get_support_patterns()
materials = engine.get_materials() if hasattr(engine, 'get_materials') else []
@@ -607,9 +659,13 @@ def account():
flash('New passwords do not match.', 'danger')
elif len(new_pass) < 6:
flash('New password must be at least 6 characters.', 'danger')
elif current_user.is_admin and new_pass == 'admin123':
flash('Your new password cannot be the default "admin123".', 'danger')
else:
current_user.password_hash = generate_password_hash(new_pass)
db.session.commit()
# If they just changed it, clear the must change flag
session.pop('must_change_password', None)
flash('Password updated successfully.', 'success')
elif action == 'terminate_session':

View File

@@ -228,6 +228,26 @@ def control():
error = "OctoPrint is not configured."
return render_template('printer/control.html', webcam_url=webcam_url, error=error)
import re
import markdown
@printer_bp.route('/helper_printer')
def helper_printer():
lang = request.cookies.get('lang', 'en')
filepath = os.path.join(current_app.root_path, 'assets', 'doc', f'printer_helper_{lang}.md')
if not os.path.exists(filepath):
filepath = os.path.join(current_app.root_path, 'assets', 'doc', 'printer_helper_en.md')
content_html = ""
if os.path.exists(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
md_text = f.read()
content_html = markdown.markdown(md_text, extensions=['fenced_code', 'tables'])
# Rewrite relative image links to /assets/doc/
content_html = re.sub(r'src="(?!http|/)([^"]+)"', r'src="/assets/doc/\1"', content_html)
return render_template('printer/helper_printer.html', content_html=content_html)
@printer_bp.route('/api/command', methods=['POST'])
@login_required
def api_command():
@@ -456,7 +476,9 @@ def octo_proxy(path):
class WebSocketResponse(Response):
def __call__(self, *args, **kwargs):
print("WS Response __call__")
if getattr(ws, 'mode', 'werkzeug') == 'werkzeug':
if getattr(ws, 'mode', 'werkzeug') == 'gunicorn':
raise StopIteration()
elif getattr(ws, 'mode', 'werkzeug') == 'werkzeug':
return super().__call__(*args, **kwargs)
return []

View File

@@ -35,8 +35,25 @@
{% endif %}
</td>
<td>
<small class="text-muted d-block">STL: {{ user_quotas[user.id]['stl'] if user_quotas[user.id]['stl'] != '0' else _('Unlimited') }} MB</small>
<small class="text-muted d-block">GCode: {{ user_quotas[user.id]['gcode'] if user_quotas[user.id]['gcode'] != '0' else _('Unlimited') }} MB</small>
{% if user.is_admin %}
<small class="text-muted d-block">STL: {{ _('Unlimited') }}</small>
<small class="text-muted d-block">GCode: {{ _('Unlimited') }}</small>
{% else %}
<small class="text-muted d-block">STL:
{% if user_quotas[user.id]['stl'] == '0' %}
{{ _('Default') }} ({{ user_quotas[user.id]['eff_stl'] if user_quotas[user.id]['eff_stl'] != '0' else _('Unlimited') }} MB)
{% else %}
{{ user_quotas[user.id]['stl'] }} MB
{% endif %}
</small>
<small class="text-muted d-block">GCode:
{% if user_quotas[user.id]['gcode'] == '0' %}
{{ _('Default') }} ({{ user_quotas[user.id]['eff_gcode'] if user_quotas[user.id]['eff_gcode'] != '0' else _('Unlimited') }} MB)
{% else %}
{{ user_quotas[user.id]['gcode'] }} MB
{% endif %}
</small>
{% endif %}
</td>
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
@@ -66,11 +83,11 @@
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">{{ _('STL Quota') }} (MB) <small class="text-muted">(0 = {{ _('Unlimited') }})</small></label>
<label class="form-label">{{ _('STL Quota') }} (MB) <small class="text-muted">(0 = {{ _('Default') }})</small></label>
<input type="number" class="form-control" name="stl_quota_mb" value="{{ user_quotas[user.id]['stl'] }}" min="0">
</div>
<div class="mb-3">
<label class="form-label">{{ _('GCode Quota') }} (MB) <small class="text-muted">(0 = {{ _('Unlimited') }})</small></label>
<label class="form-label">{{ _('GCode Quota') }} (MB) <small class="text-muted">(0 = {{ _('Default') }})</small></label>
<input type="number" class="form-control" name="gcode_quota_mb" value="{{ user_quotas[user.id]['gcode'] }}" min="0">
</div>
</div>

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AIO 3D Slicer</title>
<link href="{{ url_for('static', filename='img/favicon.ico') }}" rel="icon" type="image/x-icon">
<!-- Bootstrap 5 CSS -->
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
<!-- Bootstrap Icons -->
@@ -131,6 +132,11 @@
<i class="bi bi-arrows-move me-2"></i>{{ _('Control') }}
</a>
</li>
<li class="nav-item mb-1">
<a class="nav-link text-dark {% if request.endpoint == 'printer.helper_printer' %}active text-white shadow-sm{% endif %}" href="{{ url_for('printer.helper_printer') }}">
<i class="bi bi-question-square me-2"></i>{{ _('Printer Helper') }}
</a>
</li>
<!-- {% if current_user.is_authenticated and not current_user.is_guest %}
<li class="nav-item mb-1">
<a class="nav-link text-dark {% if request.endpoint == 'main.account' %}active text-white shadow-sm{% endif %}" href="{{ url_for('main.account') }}">
@@ -185,6 +191,11 @@
</a>
</li>
{% endif %}
<li class="nav-item mb-1">
<a class="nav-link text-dark {% if request.endpoint == 'main.helper_slice' %}active text-white shadow-sm{% endif %}" href="{{ url_for('main.helper_slice') }}">
<i class="bi bi-question-square me-2"></i>{{ _('Slice Helper') }}
</a>
</li>
</ul>
{% if current_user.is_authenticated and current_user.is_admin %}

View File

@@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{{ _('Printer Helper') }}</h1>
</div>
<div class="row">
<!-- Helper Content -->
<div class="col-12 markdown-body p-4 bg-white rounded shadow-sm">
{% if content_html %}
{{ content_html|safe }}
{% else %}
<p class="text-muted">{{ _('Documentation not available.') }}</p>
{% endif %}
</div>
</div>
<style>
/* 简单的 Markdown 样式优化 */
.markdown-body h1, .markdown-body h2, .markdown-body h3 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
}
.markdown-body h1 {
font-size: 2em;
padding-bottom: .3em;
border-bottom: 1px solid #eaecef;
}
.markdown-body img {
max-width: 100%;
height: auto;
border-radius: 6px;
}
.markdown-body pre {
background-color: #f6f8fa;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
}
.markdown-body code {
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;
}
.markdown-body table {
width: 100%;
margin-bottom: 1rem;
color: #212529;
border-collapse: collapse;
}
.markdown-body table th, .markdown-body table td {
padding: 0.5rem;
border: 1px solid #dee2e6;
}
</style>
{% endblock %}

View File

@@ -84,7 +84,7 @@
<span class="badge bg-secondary font-monospace">{{ s.ip_address }}</span>
</td>
<td>
<small class="text-muted">{{ s.last_active.strftime('%Y-%m-%d %H:%M:%S') }}</small>
<small class="text-muted local-time" data-utc="{{ s.last_active.isoformat() }}Z">{{ s.last_active.strftime('%Y-%m-%d %H:%M:%S') }}</small>
</td>
<td class="text-end pe-4">
{% if s.session_token != current_token %}
@@ -111,4 +111,24 @@
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll('.local-time').forEach(function(el) {
const utcDate = new Date(el.getAttribute('data-utc'));
if (!isNaN(utcDate)) {
const pad = (n) => n.toString().padStart(2, '0');
const yyyy = utcDate.getFullYear();
const MM = pad(utcDate.getMonth() + 1);
const dd = pad(utcDate.getDate());
const HH = pad(utcDate.getHours());
const mm = pad(utcDate.getMinutes());
const ss = pad(utcDate.getSeconds());
el.textContent = `${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}`;
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{{ _('Slice Helper') }}</h1>
</div>
<div class="row">
<!-- Helper Content -->
<div class="col-12 markdown-body p-4 bg-white rounded shadow-sm">
{% if content_html %}
{{ content_html|safe }}
{% else %}
<p class="text-muted">{{ _('Documentation not available.') }}</p>
{% endif %}
</div>
</div>
<style>
/* 简单的 Markdown 样式优化 */
.markdown-body h1, .markdown-body h2, .markdown-body h3 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
}
.markdown-body h1 {
font-size: 2em;
padding-bottom: .3em;
border-bottom: 1px solid #eaecef;
}
.markdown-body img {
max-width: 100%;
height: auto;
border-radius: 6px;
}
.markdown-body pre {
background-color: #f6f8fa;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
}
.markdown-body code {
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;
}
.markdown-body table {
width: 100%;
margin-bottom: 1rem;
color: #212529;
border-collapse: collapse;
}
.markdown-body table th, .markdown-body table td {
padding: 0.5rem;
border: 1px solid #dee2e6;
}
</style>
{% endblock %}

View File

@@ -110,6 +110,10 @@ class OctoPrintClient:
"""Get information about the current print job and progress."""
return self._request("GET", "/api/job")
def get_printer_err_log(self):
"""Fetch the printer error log, if available."""
return self._request("GET", "/api/printer/error")
# -------------------------------------------------------------------------
# Printer Control
# -------------------------------------------------------------------------

View File

@@ -8,7 +8,7 @@ def get_all_engines():
PrusaSlicerEngine()
]
def get_slicer_engine(engine_name="prusa", print_config_folder=None):
def get_slicer_engine(engine_name="prusa", print_config_folder=None, config_slice_bin_path=None):
"""
Factory function to retrieve the requested slicing engine instance.
Valid names: 'cura', 'prusa_slicer'
@@ -18,7 +18,7 @@ def get_slicer_engine(engine_name="prusa", print_config_folder=None):
if engine_name in ['cura', 'cura_engine', 'curaengine']:
return CuraEngine(print_config_folder)
elif engine_name in ['prusa', 'prusa_slicer', 'prusaslicer']:
return PrusaSlicerEngine(print_config_folder)
return PrusaSlicerEngine(print_config_folder, config_slice_bin_path)
else:
# Default fallback
return PrusaSlicerEngine(print_config_folder)
return PrusaSlicerEngine(print_config_folder, config_slice_bin_path)

View File

@@ -20,8 +20,6 @@ class CuraEngine:
return result.returncode == 0 or b"Usage:" in result.stdout or b"Usage:" in result.stderr
except (FileNotFoundError, OSError):
return False
self.display_name = "UltiMaker Cura"
self.is_available = self._check_available()
def slice(self, app, stl_filepath, gcode_filepath, **kwargs):

View File

@@ -5,25 +5,19 @@ import uuid
import shutil
from app.models import SystemConfig
# Default PrusaSlicer AppImage (aarch64) download URL
PRUSA_DOWNLOAD_URL = "https://github.com/davidk/PrusaSlicer-ARM.AppImage/releases/download/version_2.9.4/PrusaSlicer-2.9.4-aarch64-full.AppImage"
class PrusaSlicerEngine:
def __init__(self, print_config_folder=None):
def __init__(self, print_config_folder=None, config_slice_bin_path=None):
self.name = "prusa_slicer"
self.display_name = "PrusaSlicer"
self.config_slice_bin_path = config_slice_bin_path
self.is_available = self._check_available()
self.print_config_folder = os.path.join(print_config_folder, 'prusa_slicer') if print_config_folder else None
def _check_available(self):
try:
# Prefer explicit environment variable, then PATH, then a bundled AppImage under the repo
prusa_bin = os.environ.get('PRUSA_SLICER_BIN') or shutil.which('prusa-slicer') or shutil.which('prusa-slicer.exe')
if not prusa_bin:
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
local_appimage = os.path.join(repo_root, 'prusaslicer', os.path.basename(PRUSA_DOWNLOAD_URL))
if os.path.isfile(local_appimage) and os.access(local_appimage, os.X_OK):
prusa_bin = local_appimage
prusa_bin = self.config_slice_bin_path or shutil.which('prusa-slicer') or shutil.which('prusa-slicer.exe')
if not prusa_bin:
return False
result = subprocess.run([prusa_bin, "--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@@ -44,30 +38,7 @@ class PrusaSlicerEngine:
Slices via prusa-slicer CLI mapping standard kwargs to PRUSA parameters where possible.
"""
try:
# Determine prusa-slicer binary location (env -> PATH -> bundled appimage in repo)
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
prusa_env = os.environ.get('PRUSA_SLICER_BIN')
candidates = []
if prusa_env:
candidates.append(prusa_env)
which_bin = shutil.which('prusa-slicer') or shutil.which('prusa-slicer.exe')
if which_bin:
candidates.append(which_bin)
candidates.extend([
os.path.join(repo_root, 'prusaslicer', 'prusa-slicer'),
os.path.join(repo_root, 'prusaslicer', os.path.basename(PRUSA_DOWNLOAD_URL)),
os.path.join(repo_root, os.path.basename(PRUSA_DOWNLOAD_URL))
])
prusa_bin = None
for c in candidates:
if c and os.path.isfile(c) and os.access(c, os.X_OK):
prusa_bin = c
break
if not prusa_bin:
# fallback to plain name so subprocess will try PATH and give a clear error
prusa_bin = 'prusa-slicer'
prusa_bin = self.config_slice_bin_path or shutil.which('prusa-slicer') or shutil.which('prusa-slicer.exe')
# Base command
command = [prusa_bin, '-g', stl_filepath, '--output', gcode_filepath]