镜像自地址
https://github.com/a13zhy/a13rollcall.git
已同步 2026-10-20 11:27:47 +08:00
601 行
26 KiB
Python
601 行
26 KiB
Python
import sys
|
||
import os
|
||
import json
|
||
import shutil
|
||
import base64
|
||
import zipfile
|
||
import tempfile
|
||
from datetime import datetime
|
||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||
QLabel, QPushButton, QLineEdit, QTextEdit, QFileDialog,
|
||
QMessageBox, QTabWidget, QCheckBox, QSpinBox, QComboBox,
|
||
QListWidget, QListWidgetItem, QGroupBox, QFormLayout,
|
||
QScrollArea, QFrame, QSplitter)
|
||
from PyQt5.QtCore import Qt, QSize
|
||
from PyQt5.QtGui import QFont, QIcon, QColor, QPalette
|
||
|
||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
|
||
class ArcxPackager(QMainWindow):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.setWindowTitle("A13插件打包工具 - .arcx 生成器")
|
||
self.setMinimumSize(900, 700)
|
||
self.resize(1000, 750)
|
||
self._code_content = ""
|
||
self._pages = []
|
||
self._libs_dir = os.path.join(tempfile.gettempdir(), "a13_plugin_libs")
|
||
self._ensure_libs_dir()
|
||
self._setup_ui()
|
||
self._load_defaults()
|
||
|
||
def _ensure_libs_dir(self):
|
||
if not os.path.exists(self._libs_dir):
|
||
os.makedirs(self._libs_dir)
|
||
|
||
def _setup_ui(self):
|
||
central = QWidget()
|
||
self.setCentralWidget(central)
|
||
main_layout = QVBoxLayout(central)
|
||
main_layout.setContentsMargins(20, 16, 20, 16)
|
||
main_layout.setSpacing(12)
|
||
|
||
title = QLabel("🔧 A13插件打包工具")
|
||
title.setStyleSheet("font-size: 24px; font-weight: 800; color: #0067c0;")
|
||
main_layout.addWidget(title)
|
||
|
||
subtitle = QLabel("将 Python 插件代码打包成 .arcx 格式,支持多页面配置、默认设置、元数据编辑")
|
||
subtitle.setStyleSheet("font-size: 13px; color: #666666;")
|
||
main_layout.addWidget(subtitle)
|
||
|
||
self.tabs = QTabWidget()
|
||
self.tabs.setStyleSheet("""
|
||
QTabWidget::pane { border: 1px solid #e0e0e0; border-radius: 8px; }
|
||
QTabBar::tab { padding: 10px 20px; margin-right: 4px; border-radius: 6px 6px 0 0; font-size: 13px; }
|
||
QTabBar::tab:selected { background: #0067c0; color: white; }
|
||
QTabBar::tab:!selected { background: #f0f0f0; color: #333; }
|
||
""")
|
||
main_layout.addWidget(self.tabs, 1)
|
||
|
||
self._build_meta_tab()
|
||
self._build_code_tab()
|
||
self._build_pages_tab()
|
||
self._build_config_tab()
|
||
self._build_libs_tab()
|
||
self._build_preview_tab()
|
||
|
||
btn_layout = QHBoxLayout()
|
||
btn_layout.addStretch()
|
||
|
||
self.load_template_btn = QPushButton("📂 加载示例模板")
|
||
self.load_template_btn.setStyleSheet("QPushButton { background: #666; color: white; border: none; border-radius: 6px; padding: 12px 24px; font-weight: 600; font-size: 14px; }")
|
||
self.load_template_btn.clicked.connect(self._load_template)
|
||
btn_layout.addWidget(self.load_template_btn)
|
||
|
||
self.package_btn = QPushButton("📦 打包成 .arcx")
|
||
self.package_btn.setStyleSheet("QPushButton { background: #107c10; color: white; border: none; border-radius: 6px; padding: 12px 32px; font-weight: 700; font-size: 15px; } QPushButton:hover { background: #0e6c0e; }")
|
||
self.package_btn.clicked.connect(self._package)
|
||
btn_layout.addWidget(self.package_btn)
|
||
|
||
self.installer_btn = QPushButton("🚀 生成安装程序")
|
||
self.installer_btn.setStyleSheet("QPushButton { background: #0067c0; color: white; border: none; border-radius: 6px; padding: 12px 32px; font-weight: 700; font-size: 15px; } QPushButton:hover { background: #005a9e; }")
|
||
self.installer_btn.clicked.connect(self._generate_installer)
|
||
btn_layout.addWidget(self.installer_btn)
|
||
|
||
self.arcxpkg_btn = QPushButton("📚 打包含依赖(.arcxpkg)")
|
||
self.arcxpkg_btn.setStyleSheet("QPushButton { background: #ca5010; color: white; border: none; border-radius: 6px; padding: 12px 32px; font-weight: 700; font-size: 15px; } QPushButton:hover { background: #b04810; }")
|
||
self.arcxpkg_btn.clicked.connect(self._generate_arcxpkg)
|
||
btn_layout.addWidget(self.arcxpkg_btn)
|
||
|
||
main_layout.addLayout(btn_layout)
|
||
|
||
def _build_meta_tab(self):
|
||
tab = QWidget()
|
||
layout = QVBoxLayout(tab)
|
||
layout.setContentsMargins(20, 20, 20, 20)
|
||
layout.setSpacing(12)
|
||
|
||
group = QGroupBox("插件元数据")
|
||
group.setStyleSheet("QGroupBox { font-weight: 700; font-size: 14px; border: 1px solid #e0e0e0; border-radius: 8px; margin-top: 12px; padding-top: 16px; }")
|
||
form = QFormLayout(group)
|
||
form.setSpacing(10)
|
||
|
||
self.name_input = QLineEdit()
|
||
self.name_input.setPlaceholderText("例如:示例插件")
|
||
form.addRow("插件名称*:", self.name_input)
|
||
|
||
self.version_input = QLineEdit()
|
||
self.version_input.setText("1.0.0")
|
||
form.addRow("版本号:", self.version_input)
|
||
|
||
self.author_input = QLineEdit()
|
||
self.author_input.setPlaceholderText("例如:张三")
|
||
form.addRow("作者:", self.author_input)
|
||
|
||
self.desc_input = QTextEdit()
|
||
self.desc_input.setMaximumHeight(80)
|
||
self.desc_input.setPlaceholderText("简要描述插件的功能和用途")
|
||
form.addRow("描述:", self.desc_input)
|
||
|
||
self.type_combo = QComboBox()
|
||
self.type_combo.addItems(["general", "tool", "theme", "data", "ui"])
|
||
form.addRow("插件类型:", self.type_combo)
|
||
|
||
self.api_spin = QSpinBox()
|
||
self.api_spin.setRange(1, 10)
|
||
self.api_spin.setValue(2)
|
||
form.addRow("API版本:", self.api_spin)
|
||
|
||
layout.addWidget(group)
|
||
layout.addStretch()
|
||
self.tabs.addTab(tab, "📝 元数据")
|
||
|
||
def _build_code_tab(self):
|
||
tab = QWidget()
|
||
layout = QVBoxLayout(tab)
|
||
layout.setContentsMargins(20, 20, 20, 20)
|
||
layout.setSpacing(10)
|
||
|
||
btn_row = QHBoxLayout()
|
||
self.load_code_btn = QPushButton("📂 加载 Python 文件")
|
||
self.load_code_btn.clicked.connect(self._load_code_file)
|
||
btn_row.addWidget(self.load_code_btn)
|
||
|
||
self.paste_code_btn = QPushButton("📋 粘贴代码")
|
||
self.paste_code_btn.clicked.connect(self._paste_code)
|
||
btn_row.addWidget(self.paste_code_btn)
|
||
|
||
self.clear_code_btn = QPushButton("🗑️ 清空")
|
||
self.clear_code_btn.clicked.connect(lambda: self.code_editor.clear())
|
||
btn_row.addWidget(self.clear_code_btn)
|
||
btn_row.addStretch()
|
||
|
||
self.code_path_label = QLabel("未加载文件")
|
||
self.code_path_label.setStyleSheet("color: #888; font-size: 12px;")
|
||
btn_row.addWidget(self.code_path_label)
|
||
|
||
layout.addLayout(btn_row)
|
||
|
||
self.code_editor = QTextEdit()
|
||
self.code_editor.setStyleSheet("""
|
||
QTextEdit { background: #1e1e1e; color: #d4d4d4; border: 1px solid #3d3d3d; border-radius: 6px;
|
||
padding: 12px; font-family: Consolas, monospace; font-size: 12px; }
|
||
""")
|
||
self.code_editor.setPlaceholderText("在此粘贴或加载插件 Python 代码...\n\n必需函数:\ndef main(context):\n return {'status': 'success'}\n\n可选函数:\ndef build_page_settings(context, parent=None):\n # 构建设置页面\n return QWidget()")
|
||
layout.addWidget(self.code_editor, 1)
|
||
|
||
self.tabs.addTab(tab, "💻 代码编辑")
|
||
|
||
def _build_pages_tab(self):
|
||
tab = QWidget()
|
||
layout = QVBoxLayout(tab)
|
||
layout.setContentsMargins(20, 20, 20, 20)
|
||
layout.setSpacing(10)
|
||
|
||
hint = QLabel("配置插件的设置页面,每个页面对应代码中的 build_page_<key> 函数")
|
||
hint.setStyleSheet("color: #666; font-size: 12px;")
|
||
layout.addWidget(hint)
|
||
|
||
self.pages_list = QListWidget()
|
||
self.pages_list.setStyleSheet("QListWidget { background: #fafafa; border: 1px solid #e5e5e5; border-radius: 6px; padding: 4px; } QListWidget::item { padding: 10px; border-radius: 4px; }")
|
||
layout.addWidget(self.pages_list, 1)
|
||
|
||
btn_row = QHBoxLayout()
|
||
self.add_page_btn = QPushButton("➕ 添加页面")
|
||
self.add_page_btn.clicked.connect(self._add_page)
|
||
btn_row.addWidget(self.add_page_btn)
|
||
|
||
self.remove_page_btn = QPushButton("➖ 删除选中")
|
||
self.remove_page_btn.clicked.connect(self._remove_page)
|
||
btn_row.addWidget(self.remove_page_btn)
|
||
|
||
self.edit_page_btn = QPushButton("✏️ 编辑选中")
|
||
self.edit_page_btn.clicked.connect(self._edit_page)
|
||
btn_row.addWidget(self.edit_page_btn)
|
||
btn_row.addStretch()
|
||
layout.addLayout(btn_row)
|
||
|
||
self.tabs.addTab(tab, "📄 页面配置")
|
||
|
||
def _build_config_tab(self):
|
||
tab = QWidget()
|
||
layout = QVBoxLayout(tab)
|
||
layout.setContentsMargins(20, 20, 20, 20)
|
||
layout.setSpacing(10)
|
||
|
||
hint = QLabel("配置插件的默认设置项(JSON 格式),插件运行时可通过 context.get_config/set_config 访问")
|
||
hint.setStyleSheet("color: #666; font-size: 12px;")
|
||
layout.addWidget(hint)
|
||
|
||
self.config_editor = QTextEdit()
|
||
self.config_editor.setStyleSheet("""
|
||
QTextEdit { background: #1e1e1e; color: #d4d4d4; border: 1px solid #3d3d3d; border-radius: 6px;
|
||
padding: 12px; font-family: Consolas, monospace; font-size: 12px; }
|
||
""")
|
||
self.config_editor.setPlaceholderText('{\n "key1": "value1",\n "key2": true,\n "key3": 123\n}')
|
||
layout.addWidget(self.config_editor, 1)
|
||
|
||
btn_row = QHBoxLayout()
|
||
self.format_json_btn = QPushButton("✨ 格式化 JSON")
|
||
self.format_json_btn.clicked.connect(self._format_json)
|
||
btn_row.addWidget(self.format_json_btn)
|
||
btn_row.addStretch()
|
||
layout.addLayout(btn_row)
|
||
|
||
self.tabs.addTab(tab, "⚙️ 默认配置")
|
||
|
||
def _build_preview_tab(self):
|
||
tab = QWidget()
|
||
layout = QVBoxLayout(tab)
|
||
layout.setContentsMargins(20, 20, 20, 20)
|
||
layout.setSpacing(10)
|
||
|
||
hint = QLabel("预览打包后的 .arcx 文件结构(JSON 格式)")
|
||
hint.setStyleSheet("color: #666; font-size: 12px;")
|
||
layout.addWidget(hint)
|
||
|
||
self.preview_editor = QTextEdit()
|
||
self.preview_editor.setReadOnly(True)
|
||
self.preview_editor.setStyleSheet("""
|
||
QTextEdit { background: #f5f5f5; color: #333; border: 1px solid #e0e0e0; border-radius: 6px;
|
||
padding: 12px; font-family: Consolas, monospace; font-size: 11px; }
|
||
""")
|
||
layout.addWidget(self.preview_editor, 1)
|
||
|
||
btn_row = QHBoxLayout()
|
||
self.refresh_preview_btn = QPushButton("🔄 刷新预览")
|
||
self.refresh_preview_btn.clicked.connect(self._refresh_preview)
|
||
btn_row.addWidget(self.refresh_preview_btn)
|
||
btn_row.addStretch()
|
||
layout.addLayout(btn_row)
|
||
|
||
self.tabs.addTab(tab, "👁️ 预览")
|
||
|
||
def _load_defaults(self):
|
||
self._pages = [
|
||
{"key": "settings", "title": "插件设置", "icon": "settings"},
|
||
{"key": "about", "title": "关于插件", "icon": "info"},
|
||
]
|
||
self._refresh_pages_list()
|
||
self.config_editor.setPlainText('{\n "example_key": "example_value"\n}')
|
||
|
||
def _load_template(self):
|
||
template_path = os.path.join(BASE_DIR, "plugins", "示例插件.arcx")
|
||
if not os.path.exists(template_path):
|
||
QMessageBox.warning(self, "未找到模板", "示例插件模板不存在,请先运行主程序生成。")
|
||
return
|
||
try:
|
||
with open(template_path, "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
meta = data.get("meta", {})
|
||
self.name_input.setText(meta.get("name", ""))
|
||
self.version_input.setText(meta.get("version", "1.0.0"))
|
||
self.author_input.setText(meta.get("author", ""))
|
||
self.desc_input.setPlainText(meta.get("description", ""))
|
||
self.type_combo.setCurrentText(meta.get("type", "general"))
|
||
self.api_spin.setValue(meta.get("api_version", 2))
|
||
self.code_editor.setPlainText(data.get("code", ""))
|
||
self._pages = meta.get("pages", [])
|
||
self._refresh_pages_list()
|
||
self.config_editor.setPlainText(json.dumps(data.get("config", {}), indent=2, ensure_ascii=False))
|
||
self.code_path_label.setText("已加载:示例插件.arcx")
|
||
QMessageBox.information(self, "加载成功", "示例插件模板已加载,可以在此基础上修改。")
|
||
except Exception as e:
|
||
QMessageBox.warning(self, "加载失败", f"加载模板失败:{str(e)}")
|
||
|
||
def _load_code_file(self):
|
||
path, _ = QFileDialog.getOpenFileName(self, "选择 Python 文件", "", "Python Files (*.py);;All Files (*.*)")
|
||
if not path:
|
||
return
|
||
try:
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
content = f.read()
|
||
self.code_editor.setPlainText(content)
|
||
self.code_path_label.setText(f"已加载:{os.path.basename(path)}")
|
||
if not self.name_input.text():
|
||
self.name_input.setText(os.path.splitext(os.path.basename(path))[0])
|
||
except Exception as e:
|
||
QMessageBox.warning(self, "加载失败", f"加载文件失败:{str(e)}")
|
||
|
||
def _paste_code(self):
|
||
clipboard = QApplication.clipboard()
|
||
text = clipboard.text()
|
||
if text:
|
||
self.code_editor.setPlainText(text)
|
||
self.code_path_label.setText("已从剪贴板粘贴")
|
||
else:
|
||
QMessageBox.information(self, "剪贴板为空", "剪贴板中没有文本内容。")
|
||
|
||
def _add_page(self):
|
||
from PyQt5.QtWidgets import QDialog, QDialogButtonBox
|
||
dlg = QDialog(self)
|
||
dlg.setWindowTitle("添加页面")
|
||
dlg.setMinimumWidth(400)
|
||
layout = QVBoxLayout(dlg)
|
||
form = QFormLayout()
|
||
key_input = QLineEdit()
|
||
key_input.setPlaceholderText("例如:settings")
|
||
form.addRow("页面Key*:", key_input)
|
||
title_input = QLineEdit()
|
||
title_input.setPlaceholderText("例如:插件设置")
|
||
form.addRow("页面标题*:", title_input)
|
||
icon_input = QComboBox()
|
||
icon_input.addItems(["settings", "info", "zap", "puzzle", "chart", "book", "users", "palette", "database", "keyboard"])
|
||
form.addRow("图标:", icon_input)
|
||
layout.addLayout(form)
|
||
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||
buttons.accepted.connect(dlg.accept)
|
||
buttons.rejected.connect(dlg.reject)
|
||
layout.addWidget(buttons)
|
||
if dlg.exec_() == QDialog.Accepted:
|
||
key = key_input.text().strip()
|
||
title = title_input.text().strip()
|
||
if not key or not title:
|
||
QMessageBox.warning(self, "输入不完整", "请填写页面Key和标题。")
|
||
return
|
||
self._pages.append({"key": key, "title": title, "icon": icon_input.currentText()})
|
||
self._refresh_pages_list()
|
||
|
||
def _remove_page(self):
|
||
current = self.pages_list.currentRow()
|
||
if current < 0:
|
||
QMessageBox.warning(self, "未选中", "请先选择要删除的页面。")
|
||
return
|
||
self._pages.pop(current)
|
||
self._refresh_pages_list()
|
||
|
||
def _edit_page(self):
|
||
current = self.pages_list.currentRow()
|
||
if current < 0:
|
||
QMessageBox.warning(self, "未选中", "请先选择要编辑的页面。")
|
||
return
|
||
from PyQt5.QtWidgets import QDialog, QDialogButtonBox
|
||
page = self._pages[current]
|
||
dlg = QDialog(self)
|
||
dlg.setWindowTitle("编辑页面")
|
||
dlg.setMinimumWidth(400)
|
||
layout = QVBoxLayout(dlg)
|
||
form = QFormLayout()
|
||
key_input = QLineEdit(page["key"])
|
||
form.addRow("页面Key*:", key_input)
|
||
title_input = QLineEdit(page["title"])
|
||
form.addRow("页面标题*:", title_input)
|
||
icon_input = QComboBox()
|
||
icon_input.addItems(["settings", "info", "zap", "puzzle", "chart", "book", "users", "palette", "database", "keyboard"])
|
||
icon_input.setCurrentText(page.get("icon", "settings"))
|
||
form.addRow("图标:", icon_input)
|
||
layout.addLayout(form)
|
||
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||
buttons.accepted.connect(dlg.accept)
|
||
buttons.rejected.connect(dlg.reject)
|
||
layout.addWidget(buttons)
|
||
if dlg.exec_() == QDialog.Accepted:
|
||
page["key"] = key_input.text().strip()
|
||
page["title"] = title_input.text().strip()
|
||
page["icon"] = icon_input.currentText()
|
||
self._refresh_pages_list()
|
||
|
||
def _refresh_pages_list(self):
|
||
self.pages_list.clear()
|
||
for page in self._pages:
|
||
item = QListWidgetItem(f"📄 {page['title']} (key: {page['key']}, icon: {page.get('icon', 'settings')})")
|
||
self.pages_list.addItem(item)
|
||
|
||
def _format_json(self):
|
||
try:
|
||
content = self.config_editor.toPlainText()
|
||
if not content.strip():
|
||
return
|
||
data = json.loads(content)
|
||
self.config_editor.setPlainText(json.dumps(data, indent=2, ensure_ascii=False))
|
||
except json.JSONDecodeError as e:
|
||
QMessageBox.warning(self, "JSON 格式错误", f"JSON 格式错误:{str(e)}")
|
||
|
||
def _build_arcx_data(self):
|
||
name = self.name_input.text().strip()
|
||
if not name:
|
||
return None, "请填写插件名称"
|
||
code = self.code_editor.toPlainText()
|
||
if not code.strip():
|
||
return None, "请填写或加载插件代码"
|
||
try:
|
||
config = json.loads(self.config_editor.toPlainText()) if self.config_editor.toPlainText().strip() else {}
|
||
except json.JSONDecodeError as e:
|
||
return None, f"默认配置 JSON 格式错误:{str(e)}"
|
||
|
||
data = {
|
||
"meta": {
|
||
"name": name,
|
||
"version": self.version_input.text().strip() or "1.0.0",
|
||
"author": self.author_input.text().strip() or "匿名",
|
||
"description": self.desc_input.toPlainText().strip(),
|
||
"type": self.type_combo.currentText(),
|
||
"api_version": self.api_spin.value(),
|
||
"has_settings_page": len(self._pages) > 0,
|
||
"pages": self._pages,
|
||
"config": config
|
||
},
|
||
"code": code,
|
||
"config": config,
|
||
"enabled": True,
|
||
"installed_at": datetime.now().isoformat()
|
||
}
|
||
return data, None
|
||
|
||
def _refresh_preview(self):
|
||
data, error = self._build_arcx_data()
|
||
if error:
|
||
self.preview_editor.setPlainText(f"❌ {error}")
|
||
return
|
||
preview = json.dumps(data, indent=2, ensure_ascii=False)
|
||
if len(preview) > 5000:
|
||
preview = preview[:5000] + "\n\n... (代码过长,已截断)"
|
||
self.preview_editor.setPlainText(preview)
|
||
|
||
def _package(self):
|
||
data, error = self._build_arcx_data()
|
||
if error:
|
||
QMessageBox.warning(self, "打包失败", error)
|
||
return
|
||
default_name = f"{data['meta']['name']}.arcx"
|
||
default_path = os.path.join(BASE_DIR, "plugins", default_name)
|
||
path, _ = QFileDialog.getSaveFileName(self, "保存 .arcx 文件", default_path, "ARCX 插件文件 (*.arcx)")
|
||
if not path:
|
||
return
|
||
try:
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||
size = os.path.getsize(path)
|
||
QMessageBox.information(self, "打包成功",
|
||
f"✅ 插件打包成功!\n\n"
|
||
f"文件:{path}\n"
|
||
f"大小:{size / 1024:.1f} KB\n\n"
|
||
f"将此文件放到 plugins 目录,重启软件即可加载。")
|
||
except Exception as e:
|
||
QMessageBox.warning(self, "打包失败", f"保存文件失败:{str(e)}")
|
||
|
||
def _generate_installer(self):
|
||
data, error = self._build_arcx_data()
|
||
if error:
|
||
QMessageBox.warning(self, "生成失败", error)
|
||
return
|
||
|
||
meta = data['meta']
|
||
plugin_name = meta['name']
|
||
plugin_version = meta['version']
|
||
plugin_author = meta.get('author', '匿名')
|
||
plugin_desc = meta.get('description', '')
|
||
|
||
template_path = os.path.join(BASE_DIR, "plugin_installer_template.py")
|
||
if not os.path.exists(template_path):
|
||
QMessageBox.warning(self, "模板缺失", f"安装程序模板不存在:{template_path}")
|
||
return
|
||
|
||
try:
|
||
with open(template_path, "r", encoding="utf-8") as f:
|
||
template = f.read()
|
||
|
||
plugin_json = json.dumps(data, ensure_ascii=False)
|
||
plugin_data_b64 = base64.b64encode(plugin_json.encode('utf-8')).decode('utf-8')
|
||
|
||
installer_code = template
|
||
installer_code = installer_code.replace("__PLUGIN_NAME__", plugin_name)
|
||
installer_code = installer_code.replace("__PLUGIN_VERSION__", plugin_version)
|
||
installer_code = installer_code.replace("__PLUGIN_AUTHOR__", plugin_author)
|
||
installer_code = installer_code.replace("__PLUGIN_DESC__", plugin_desc)
|
||
installer_code = installer_code.replace('"__PLUGIN_DATA__"', f'"{plugin_data_b64}"')
|
||
|
||
default_name = f"{plugin_name}_installer.py"
|
||
default_path = os.path.join(BASE_DIR, default_name)
|
||
path, _ = QFileDialog.getSaveFileName(self, "保存安装程序", default_path, "Python 文件 (*.py)")
|
||
if not path:
|
||
return
|
||
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
f.write(installer_code)
|
||
|
||
size = os.path.getsize(path)
|
||
QMessageBox.information(self, "生成成功",
|
||
f"✅ 安装程序生成成功!\n\n"
|
||
f"文件:{path}\n"
|
||
f"大小:{size / 1024:.1f} KB\n\n"
|
||
f"运行方式:\n"
|
||
f"python {os.path.basename(path)}\n\n"
|
||
f"如需打包成 exe,请使用 PyInstaller:\n"
|
||
f"pyinstaller --onefile --windowed --name {plugin_name}安装程序 {os.path.basename(path)}")
|
||
except Exception as e:
|
||
import traceback
|
||
QMessageBox.warning(self, "生成失败", f"生成安装程序失败:{str(e)}\n\n{traceback.format_exc()}")
|
||
|
||
def _generate_arcxpkg(self):
|
||
data, error = self._build_arcx_data()
|
||
if error:
|
||
QMessageBox.warning(self, "打包失败", error)
|
||
return
|
||
|
||
meta = data['meta']
|
||
plugin_name = meta['name']
|
||
plugin_version = meta['version']
|
||
|
||
libs_dir = QFileDialog.getExistingDirectory(
|
||
self,
|
||
"选择第三方依赖库目录(libs文件夹)\n\n该目录下应包含需要随插件一起分发的Python库,例如:\n- requests/\n- urllib3/\n- numpy/\n\n如果没有依赖库,请取消此步骤",
|
||
BASE_DIR
|
||
)
|
||
|
||
import tempfile
|
||
temp_dir = tempfile.mkdtemp(prefix="a13_arcxpkg_")
|
||
|
||
try:
|
||
plugin_arcx_path = os.path.join(temp_dir, "plugin.arcx")
|
||
with open(plugin_arcx_path, "w", encoding="utf-8") as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
|
||
lib_count = 0
|
||
if libs_dir and os.path.exists(libs_dir):
|
||
target_libs = os.path.join(temp_dir, "libs")
|
||
shutil.copytree(libs_dir, target_libs)
|
||
lib_count = len(os.listdir(target_libs))
|
||
|
||
install_config = {
|
||
"plugin_name": plugin_name,
|
||
"plugin_version": plugin_version,
|
||
"has_libs": lib_count > 0,
|
||
"lib_count": lib_count,
|
||
"pack_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
"format_version": "1.0"
|
||
}
|
||
with open(os.path.join(temp_dir, "install.json"), "w", encoding="utf-8") as f:
|
||
json.dump(install_config, f, ensure_ascii=False, indent=2)
|
||
|
||
default_name = f"{plugin_name}_v{plugin_version}.arcxpkg"
|
||
default_path = os.path.join(BASE_DIR, default_name)
|
||
save_path, _ = QFileDialog.getSaveFileName(
|
||
self, "保存插件包", default_path, "插件包 (*.arcxpkg)"
|
||
)
|
||
if not save_path:
|
||
return
|
||
|
||
with zipfile.ZipFile(save_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||
for root, dirs, files in os.walk(temp_dir):
|
||
for file in files:
|
||
file_path = os.path.join(root, file)
|
||
arcname = os.path.relpath(file_path, temp_dir)
|
||
zf.write(file_path, arcname)
|
||
|
||
size = os.path.getsize(save_path)
|
||
lib_info = f"\n包含 {lib_count} 个第三方依赖库" if lib_count > 0 else "\n不包含第三方依赖库"
|
||
QMessageBox.information(
|
||
self, "打包成功",
|
||
f"✅ 插件包打包成功!\n\n"
|
||
f"文件:{save_path}\n"
|
||
f"大小:{size / 1024:.1f} KB"
|
||
f"{lib_info}\n\n"
|
||
f"安装方式:\n"
|
||
f"1. 运行 plugin_installer.py\n"
|
||
f"2. 选择此 .arcxpkg 文件\n"
|
||
f"3. 点击开始安装\n"
|
||
f"4. 重启主程序"
|
||
)
|
||
except Exception as e:
|
||
import traceback
|
||
QMessageBox.warning(self, "打包失败", f"生成插件包失败:{str(e)}\n\n{traceback.format_exc()}")
|
||
finally:
|
||
try:
|
||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def main():
|
||
app = QApplication(sys.argv)
|
||
app.setStyle("Fusion")
|
||
window = ArcxPackager()
|
||
window.show()
|
||
sys.exit(app.exec_())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|