Merge 406ac1df62 into 96096ea0ff
This commit is contained in:
commit
3112bf378d
|
|
@ -57,4 +57,7 @@ backend/logs/
|
||||||
backend/uploads/
|
backend/uploads/
|
||||||
|
|
||||||
# Docker 数据
|
# Docker 数据
|
||||||
data/
|
data/
|
||||||
|
|
||||||
|
# 安装程序输出目录
|
||||||
|
installer/output
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## V0.1.1
|
||||||
|
- Packaged the project into a Windows installer (.exe)
|
||||||
|
- Installer allows entering API keys during setup
|
||||||
|
- After installation, double-click to open browser UI
|
||||||
|
- One-click install & run experience on Windows
|
||||||
|
|
||||||
|
## Fork Notice
|
||||||
|
This project is a fork of MiroFish by 666ghj:
|
||||||
|
https://github.com/666ghj/MiroFish
|
||||||
|
|
@ -3,7 +3,9 @@ MiroFish Backend - Flask应用工厂
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
import warnings
|
import warnings
|
||||||
|
import threading
|
||||||
|
|
||||||
# 抑制 multiprocessing resource_tracker 的警告(来自第三方库如 transformers)
|
# 抑制 multiprocessing resource_tracker 的警告(来自第三方库如 transformers)
|
||||||
# 需要在所有其他导入之前设置
|
# 需要在所有其他导入之前设置
|
||||||
|
|
@ -48,6 +50,12 @@ def create_app(config_class=Config):
|
||||||
if should_log_startup:
|
if should_log_startup:
|
||||||
logger.info("已注册模拟进程清理函数")
|
logger.info("已注册模拟进程清理函数")
|
||||||
|
|
||||||
|
# 心跳状态
|
||||||
|
last_heartbeat = {"ts": time.time()}
|
||||||
|
|
||||||
|
def touch_heartbeat():
|
||||||
|
last_heartbeat["ts"] = time.time()
|
||||||
|
|
||||||
# 请求日志中间件
|
# 请求日志中间件
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def log_request():
|
def log_request():
|
||||||
|
|
@ -55,6 +63,8 @@ def create_app(config_class=Config):
|
||||||
logger.debug(f"请求: {request.method} {request.path}")
|
logger.debug(f"请求: {request.method} {request.path}")
|
||||||
if request.content_type and 'json' in request.content_type:
|
if request.content_type and 'json' in request.content_type:
|
||||||
logger.debug(f"请求体: {request.get_json(silent=True)}")
|
logger.debug(f"请求体: {request.get_json(silent=True)}")
|
||||||
|
if request.path.startswith('/api') or request.path == '/health':
|
||||||
|
touch_heartbeat()
|
||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def log_response(response):
|
def log_response(response):
|
||||||
|
|
@ -72,9 +82,26 @@ def create_app(config_class=Config):
|
||||||
@app.route('/health')
|
@app.route('/health')
|
||||||
def health():
|
def health():
|
||||||
return {'status': 'ok', 'service': 'MiroFish Backend'}
|
return {'status': 'ok', 'service': 'MiroFish Backend'}
|
||||||
|
|
||||||
|
@app.route('/api/heartbeat', methods=['GET', 'POST'])
|
||||||
|
def heartbeat():
|
||||||
|
touch_heartbeat()
|
||||||
|
return {'status': 'ok'}
|
||||||
|
|
||||||
|
def heartbeat_watch():
|
||||||
|
timeout = int(os.environ.get('MIROFISH_HEARTBEAT_TIMEOUT', '45'))
|
||||||
|
interval = int(os.environ.get('MIROFISH_HEARTBEAT_INTERVAL', '5'))
|
||||||
|
while True:
|
||||||
|
time.sleep(interval)
|
||||||
|
if time.time() - last_heartbeat["ts"] > timeout:
|
||||||
|
logger.warning("心跳超时,自动关闭后端服务")
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
if should_log_startup:
|
if should_log_startup:
|
||||||
logger.info("MiroFish Backend 启动完成")
|
logger.info("MiroFish Backend 启动完成")
|
||||||
|
monitor = threading.Thread(target=heartbeat_watch, daemon=True)
|
||||||
|
monitor.start()
|
||||||
|
logger.info("心跳监控已启动")
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,49 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from io import StringIO
|
||||||
|
from dotenv import load_dotenv, dotenv_values
|
||||||
|
|
||||||
# 加载项目根目录的 .env 文件
|
# 加载项目根目录的 .env 文件
|
||||||
# 路径: MiroFish/.env (相对于 backend/app/config.py)
|
# 路径: MiroFish/.env (相对于 backend/app/config.py)
|
||||||
project_root_env = os.path.join(os.path.dirname(__file__), '../../.env')
|
project_root_env = os.path.join(os.path.dirname(__file__), '../../.env')
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_env_bytes(data: bytes) -> str:
|
||||||
|
for encoding in ("utf-8", "utf-8-sig", "gbk", "gb2312"):
|
||||||
|
try:
|
||||||
|
return data.decode(encoding)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
return data.decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_env_with_fallback(path: str) -> None:
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return
|
||||||
|
data = _decode_env_bytes(open(path, "rb").read())
|
||||||
|
try:
|
||||||
|
values = dotenv_values(stream=StringIO(data))
|
||||||
|
for key, value in values.items():
|
||||||
|
if key and value is not None:
|
||||||
|
os.environ[key] = value
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
for line in data.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
os.environ[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
|
||||||
if os.path.exists(project_root_env):
|
if os.path.exists(project_root_env):
|
||||||
load_dotenv(project_root_env, override=True)
|
_load_env_with_fallback(project_root_env)
|
||||||
else:
|
else:
|
||||||
# 如果根目录没有 .env,尝试加载环境变量(用于生产环境)
|
default_env = os.path.join(os.getcwd(), '.env')
|
||||||
load_dotenv(override=True)
|
if os.path.exists(default_env):
|
||||||
|
_load_env_with_fallback(default_env)
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
MiroFish Backend PyInstaller 规格文件
|
||||||
|
用于将 Flask 后端打包成 Windows 可执行文件
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from PyInstaller.utils.hooks import collect_all, collect_data_files, collect_submodules
|
||||||
|
|
||||||
|
# 项目路径
|
||||||
|
backend_dir = os.path.abspath(os.path.dirname(SPECPATH))
|
||||||
|
|
||||||
|
# 收集 app 目录下的所有文件
|
||||||
|
app_datas = []
|
||||||
|
app_dir = os.path.join(backend_dir, 'app')
|
||||||
|
for root, dirs, files in os.walk(app_dir):
|
||||||
|
for file in files:
|
||||||
|
if not file.endswith('.pyc') and not file.endswith('__pycache__'):
|
||||||
|
src = os.path.join(root, file)
|
||||||
|
dst = os.path.relpath(root, backend_dir)
|
||||||
|
app_datas.append((src, dst))
|
||||||
|
|
||||||
|
# 隐藏导入列表
|
||||||
|
hidden_imports = [
|
||||||
|
# Flask 相关
|
||||||
|
'flask',
|
||||||
|
'flask_cors',
|
||||||
|
'werkzeug',
|
||||||
|
'jinja2',
|
||||||
|
'markupsafe',
|
||||||
|
|
||||||
|
# OpenAI / LLM
|
||||||
|
'openai',
|
||||||
|
'httpx',
|
||||||
|
'httpcore',
|
||||||
|
|
||||||
|
# Zep Cloud
|
||||||
|
'zep_cloud',
|
||||||
|
|
||||||
|
# Pydantic
|
||||||
|
'pydantic',
|
||||||
|
'pydantic.deprecated.decorator',
|
||||||
|
|
||||||
|
# 数据处理
|
||||||
|
'charset_normalizer',
|
||||||
|
'chardet',
|
||||||
|
'fitz', # PyMuPDF
|
||||||
|
|
||||||
|
# dotenv
|
||||||
|
'dotenv',
|
||||||
|
'python_dotenv',
|
||||||
|
|
||||||
|
# 其他依赖
|
||||||
|
'logging.handlers',
|
||||||
|
'email.mime.text',
|
||||||
|
'email.mime.multipart',
|
||||||
|
]
|
||||||
|
|
||||||
|
# 收集大型包的所有模块
|
||||||
|
datas = app_datas
|
||||||
|
binaries = []
|
||||||
|
|
||||||
|
# 分析配置
|
||||||
|
a = Analysis(
|
||||||
|
[os.path.join(backend_dir, 'run.py')],
|
||||||
|
pathex=[backend_dir],
|
||||||
|
binaries=binaries,
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hidden_imports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[
|
||||||
|
# 排除不需要的大型包以减小体积
|
||||||
|
'matplotlib',
|
||||||
|
'scipy',
|
||||||
|
'numpy.distutils',
|
||||||
|
'PIL',
|
||||||
|
'tkinter',
|
||||||
|
'test',
|
||||||
|
'unittest',
|
||||||
|
],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name='mirofish_backend',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
console=True, # 保留控制台以便调试
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
name='mirofish_backend',
|
||||||
|
)
|
||||||
|
|
@ -1435,7 +1435,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -1913,7 +1912,6 @@
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -2053,7 +2051,6 @@
|
||||||
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
|
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|
@ -2128,7 +2125,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
|
||||||
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
|
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.25",
|
"@vue/compiler-dom": "3.5.25",
|
||||||
"@vue/compiler-sfc": "3.5.25",
|
"@vue/compiler-sfc": "3.5.25",
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,11 @@ app.use(router)
|
||||||
app.use(i18n)
|
app.use(i18n)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
||||||
|
const apiBase = (import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:5001').replace(/\/$/, '')
|
||||||
|
const heartbeatUrl = `${apiBase}/api/heartbeat`
|
||||||
|
const heartbeat = () => {
|
||||||
|
fetch(heartbeatUrl, { method: 'POST', keepalive: true }).catch(() => {})
|
||||||
|
}
|
||||||
|
heartbeat()
|
||||||
|
setInterval(heartbeat, 10000)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,418 @@
|
||||||
|
; *** Inno Setup version 6.5.0+ Chinese Simplified messages ***
|
||||||
|
;
|
||||||
|
; To download user-contributed translations of this file, go to:
|
||||||
|
; https://jrsoftware.org/files/istrans/
|
||||||
|
;
|
||||||
|
; Note: When translating this text, do not add periods (.) to the end of
|
||||||
|
; messages that didn't have them already, because on those messages Inno
|
||||||
|
; Setup adds the periods automatically (appending a period would result in
|
||||||
|
; two periods being displayed).
|
||||||
|
;
|
||||||
|
; Maintained by Zhenghan Yang
|
||||||
|
; Email: 847320916@QQ.com
|
||||||
|
; Translation based on network resource
|
||||||
|
; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation
|
||||||
|
;
|
||||||
|
|
||||||
|
[LangOptions]
|
||||||
|
; The following three entries are very important. Be sure to read and
|
||||||
|
; understand the '[LangOptions] section' topic in the help file.
|
||||||
|
LanguageName=简体中文
|
||||||
|
; If Language Name display incorrect, uncomment next line
|
||||||
|
; LanguageName=<7B80><4F53><4E2D><6587>
|
||||||
|
; About LanguageID, to reference link:
|
||||||
|
; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c
|
||||||
|
LanguageID=$0804
|
||||||
|
; About CodePage, to reference link:
|
||||||
|
; https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
|
||||||
|
LanguageCodePage=936
|
||||||
|
; If the language you are translating to requires special font faces or
|
||||||
|
; sizes, uncomment any of the following entries and change them accordingly.
|
||||||
|
;DialogFontName=
|
||||||
|
;DialogFontSize=9
|
||||||
|
;DialogFontBaseScaleWidth=7
|
||||||
|
;DialogFontBaseScaleHeight=15
|
||||||
|
;WelcomeFontName=Segoe UI
|
||||||
|
;WelcomeFontSize=14
|
||||||
|
|
||||||
|
[Messages]
|
||||||
|
|
||||||
|
; *** 应用程序标题
|
||||||
|
SetupAppTitle=安装
|
||||||
|
SetupWindowTitle=安装 - %1
|
||||||
|
UninstallAppTitle=卸载
|
||||||
|
UninstallAppFullTitle=%1 卸载
|
||||||
|
|
||||||
|
; *** Misc. common
|
||||||
|
InformationTitle=信息
|
||||||
|
ConfirmTitle=确认
|
||||||
|
ErrorTitle=错误
|
||||||
|
|
||||||
|
; *** SetupLdr messages
|
||||||
|
SetupLdrStartupMessage=现在将安装 %1。您想要继续吗?
|
||||||
|
LdrCannotCreateTemp=无法创建临时文件。安装程序已中止
|
||||||
|
LdrCannotExecTemp=无法执行临时目录中的文件。安装程序已中止
|
||||||
|
HelpTextNote=
|
||||||
|
|
||||||
|
; *** 启动错误消息
|
||||||
|
LastErrorMessage=%1。%n%n错误 %2: %3
|
||||||
|
SetupFileMissing=安装目录中缺少文件 %1。请修正这个问题或者获取程序的新副本。
|
||||||
|
SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。
|
||||||
|
SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。
|
||||||
|
InvalidParameter=无效的命令行参数:%n%n%1
|
||||||
|
SetupAlreadyRunning=安装程序正在运行。
|
||||||
|
WindowsVersionNotSupported=此程序不支持当前计算机运行的 Windows 版本。
|
||||||
|
WindowsServicePackRequired=此程序需要 %1 服务包 %2 或更高版本。
|
||||||
|
NotOnThisPlatform=此程序不能在 %1 上运行。
|
||||||
|
OnlyOnThisPlatform=此程序只能在 %1 上运行。
|
||||||
|
OnlyOnTheseArchitectures=此程序只能安装到为下列处理器架构设计的 Windows 版本中:%n%n%1
|
||||||
|
WinVersionTooLowError=此程序需要 %1 版本 %2 或更高。
|
||||||
|
WinVersionTooHighError=此程序不能安装于 %1 版本 %2 或更高。
|
||||||
|
AdminPrivilegesRequired=在安装此程序时您必须以管理员身份登录。
|
||||||
|
PowerUserPrivilegesRequired=在安装此程序时您必须以管理员身份或有权限的用户组身份登录。
|
||||||
|
SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。
|
||||||
|
UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。
|
||||||
|
|
||||||
|
; *** 启动问题
|
||||||
|
PrivilegesRequiredOverrideTitle=选择安装程序模式
|
||||||
|
PrivilegesRequiredOverrideInstruction=选择安装模式
|
||||||
|
PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。
|
||||||
|
PrivilegesRequiredOverrideText2=%1 可以仅为您安装,或为所有用户安装(需要管理员权限)。
|
||||||
|
PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A)
|
||||||
|
PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项)
|
||||||
|
PrivilegesRequiredOverrideCurrentUser=仅为我安装(&M)
|
||||||
|
PrivilegesRequiredOverrideCurrentUserRecommended=仅为我安装(&M) (建议选项)
|
||||||
|
|
||||||
|
; *** 其他错误
|
||||||
|
ErrorCreatingDir=安装程序无法创建目录“%1”
|
||||||
|
ErrorTooManyFilesInDir=无法在目录“%1”中创建文件,因为里面包含太多文件
|
||||||
|
|
||||||
|
; *** 安装程序公共消息
|
||||||
|
ExitSetupTitle=退出安装程序
|
||||||
|
ExitSetupMessage=安装程序尚未完成。如果现在退出,将不会安装该程序。%n%n您之后可以再次运行安装程序完成安装。%n%n现在退出安装程序吗?
|
||||||
|
AboutSetupMenuItem=关于安装程序(&A)...
|
||||||
|
AboutSetupTitle=关于安装程序
|
||||||
|
AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4
|
||||||
|
AboutSetupNote=
|
||||||
|
TranslatorNote=简体中文翻译由Kira(847320916@qq.com)维护。项目地址:https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation
|
||||||
|
|
||||||
|
; *** 按钮
|
||||||
|
ButtonBack=< 上一步(&B)
|
||||||
|
ButtonNext=下一步(&N) >
|
||||||
|
ButtonInstall=安装(&I)
|
||||||
|
ButtonOK=确定
|
||||||
|
ButtonCancel=取消
|
||||||
|
ButtonYes=是(&Y)
|
||||||
|
ButtonYesToAll=全是(&A)
|
||||||
|
ButtonNo=否(&N)
|
||||||
|
ButtonNoToAll=全否(&O)
|
||||||
|
ButtonFinish=完成(&F)
|
||||||
|
ButtonBrowse=浏览(&B)...
|
||||||
|
ButtonWizardBrowse=浏览(&R)...
|
||||||
|
ButtonNewFolder=新建文件夹(&M)
|
||||||
|
|
||||||
|
; *** “选择语言”对话框消息
|
||||||
|
SelectLanguageTitle=选择安装语言
|
||||||
|
SelectLanguageLabel=选择安装时使用的语言。
|
||||||
|
|
||||||
|
; *** 公共向导文字
|
||||||
|
ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。
|
||||||
|
BeveledLabel=
|
||||||
|
BrowseDialogTitle=浏览文件夹
|
||||||
|
BrowseDialogLabel=在下面的列表中选择一个文件夹,然后点击“确定”。
|
||||||
|
NewFolderName=新建文件夹
|
||||||
|
|
||||||
|
; *** “欢迎”向导页
|
||||||
|
WelcomeLabel1=欢迎使用 [name] 安装向导
|
||||||
|
WelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n建议您在继续安装前关闭所有其他应用程序。
|
||||||
|
|
||||||
|
; *** “密码”向导页
|
||||||
|
WizardPassword=密码
|
||||||
|
PasswordLabel1=这个安装程序有密码保护。
|
||||||
|
PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。
|
||||||
|
PasswordEditLabel=密码(&P):
|
||||||
|
IncorrectPassword=您输入的密码不正确,请重新输入。
|
||||||
|
|
||||||
|
; *** “许可协议”向导页
|
||||||
|
WizardLicense=许可协议
|
||||||
|
LicenseLabel=请在继续安装前阅读以下重要信息。
|
||||||
|
LicenseLabel3=请仔细阅读下列许可协议。在继续安装前您必须同意这些协议条款。
|
||||||
|
LicenseAccepted=我同意此协议(&A)
|
||||||
|
LicenseNotAccepted=我不同意此协议(&D)
|
||||||
|
|
||||||
|
; *** “信息”向导页
|
||||||
|
WizardInfoBefore=信息
|
||||||
|
InfoBeforeLabel=请在继续安装前阅读以下重要信息。
|
||||||
|
InfoBeforeClickLabel=准备好继续安装后,点击“下一步”。
|
||||||
|
WizardInfoAfter=信息
|
||||||
|
InfoAfterLabel=请在继续安装前阅读以下重要信息。
|
||||||
|
InfoAfterClickLabel=准备好继续安装后,点击“下一步”。
|
||||||
|
|
||||||
|
; *** “用户信息”向导页
|
||||||
|
WizardUserInfo=用户信息
|
||||||
|
UserInfoDesc=请输入您的信息。
|
||||||
|
UserInfoName=用户名(&U):
|
||||||
|
UserInfoOrg=组织(&O):
|
||||||
|
UserInfoSerial=序列号(&S):
|
||||||
|
UserInfoNameRequired=您必须输入用户名。
|
||||||
|
|
||||||
|
; *** “选择目标目录”向导页
|
||||||
|
WizardSelectDir=选择目标位置
|
||||||
|
SelectDirDesc=您想将 [name] 安装在哪里?
|
||||||
|
SelectDirLabel3=安装程序将安装 [name] 到下面的文件夹中。
|
||||||
|
SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。
|
||||||
|
DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。
|
||||||
|
DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。
|
||||||
|
CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。
|
||||||
|
CannotInstallToUNCPath=安装程序无法安装到一个 UNC 路径。
|
||||||
|
InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\APP%n%n或UNC路径:%n%n\\server\share
|
||||||
|
InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选择其他位置。
|
||||||
|
DiskSpaceWarningTitle=磁盘空间不足
|
||||||
|
DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗?
|
||||||
|
DirNameTooLong=文件夹名称或路径太长。
|
||||||
|
InvalidDirName=文件夹名称无效。
|
||||||
|
BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1
|
||||||
|
DirExistsTitle=文件夹已存在
|
||||||
|
DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗?
|
||||||
|
DirDoesntExistTitle=文件夹不存在
|
||||||
|
DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗?
|
||||||
|
|
||||||
|
; *** “选择组件”向导页
|
||||||
|
WizardSelectComponents=选择组件
|
||||||
|
SelectComponentsDesc=您想安装哪些程序组件?
|
||||||
|
SelectComponentsLabel2=选中您想安装的组件;取消您不想安装的组件。然后点击“下一步”继续。
|
||||||
|
FullInstallation=完全安装
|
||||||
|
; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language)
|
||||||
|
CompactInstallation=简洁安装
|
||||||
|
CustomInstallation=自定义安装
|
||||||
|
NoUninstallWarningTitle=组件已存在
|
||||||
|
NoUninstallWarning=安装程序检测到下列组件已安装在您的电脑中:%n%n%1%n%n取消选中这些组件不会卸载它们。%n%n确定要继续吗?
|
||||||
|
ComponentSize1=%1 KB
|
||||||
|
ComponentSize2=%1 MB
|
||||||
|
ComponentsDiskSpaceGBLabel=当前选择的组件需要至少 [gb] GB 的磁盘空间。
|
||||||
|
ComponentsDiskSpaceMBLabel=当前选择的组件需要至少 [mb] MB 的磁盘空间。
|
||||||
|
|
||||||
|
; *** “选择附加任务”向导页
|
||||||
|
WizardSelectTasks=选择附加任务
|
||||||
|
SelectTasksDesc=您想要安装程序执行哪些附加任务?
|
||||||
|
SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。
|
||||||
|
|
||||||
|
; *** “选择开始菜单文件夹”向导页
|
||||||
|
WizardSelectProgramGroup=选择开始菜单文件夹
|
||||||
|
SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式?
|
||||||
|
SelectStartMenuFolderLabel3=安装程序将在下列“开始”菜单文件夹中创建程序的快捷方式。
|
||||||
|
SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。
|
||||||
|
MustEnterGroupName=您必须输入一个文件夹名。
|
||||||
|
GroupNameTooLong=文件夹名或路径太长。
|
||||||
|
InvalidGroupName=无效的文件夹名字。
|
||||||
|
BadGroupName=文件夹名不能包含下列任何字符:%n%n%1
|
||||||
|
NoProgramGroupCheck2=不创建开始菜单文件夹(&D)
|
||||||
|
|
||||||
|
; *** “准备安装”向导页
|
||||||
|
WizardReady=准备安装
|
||||||
|
ReadyLabel1=安装程序准备就绪,现在可以开始安装 [name] 到您的电脑。
|
||||||
|
ReadyLabel2a=点击“安装”继续此安装程序。如果您想重新考虑或修改任何设置,点击“上一步”。
|
||||||
|
ReadyLabel2b=点击“安装”继续此安装程序。
|
||||||
|
ReadyMemoUserInfo=用户信息:
|
||||||
|
ReadyMemoDir=目标位置:
|
||||||
|
ReadyMemoType=安装类型:
|
||||||
|
ReadyMemoComponents=已选择组件:
|
||||||
|
ReadyMemoGroup=开始菜单文件夹:
|
||||||
|
ReadyMemoTasks=附加任务:
|
||||||
|
|
||||||
|
; *** TExtractionWizardPage 向导页面与 ExtractArchive
|
||||||
|
ExtractingLabel=正在解压文件...
|
||||||
|
ButtonStopExtraction=停止解压(&S)
|
||||||
|
StopExtraction=您确定要停止解压吗?
|
||||||
|
ErrorExtractionAborted=解压已中止
|
||||||
|
ErrorExtractionFailed=解压失败:%1
|
||||||
|
|
||||||
|
; *** 压缩文件解压失败详情
|
||||||
|
ArchiveIncorrectPassword=压缩文件密码不正确
|
||||||
|
ArchiveIsCorrupted=压缩文件已损坏
|
||||||
|
ArchiveUnsupportedFormat=不支持的压缩文件格式
|
||||||
|
|
||||||
|
; *** TDownloadWizardPage 向导页面和 DownloadTemporaryFile
|
||||||
|
DownloadingLabel2=正在下载文件...
|
||||||
|
ButtonStopDownload=停止下载(&S)
|
||||||
|
StopDownload=您确定要停止下载吗?
|
||||||
|
ErrorDownloadAborted=下载已中止
|
||||||
|
ErrorDownloadFailed=下载失败:%1 %2
|
||||||
|
ErrorDownloadSizeFailed=获取下载大小失败:%1 %2
|
||||||
|
ErrorProgress=无效的进度:%1 / %2
|
||||||
|
ErrorFileSize=文件大小错误:预期 %1,实际 %2
|
||||||
|
|
||||||
|
; *** “正在准备安装”向导页
|
||||||
|
WizardPreparing=正在准备安装
|
||||||
|
PreparingDesc=安装程序正在准备安装 [name] 到您的电脑。
|
||||||
|
PreviousInstallNotCompleted=先前的程序安装或卸载未完成,您需要重启您的电脑以完成。%n%n在重启电脑后,再次运行安装程序以完成 [name] 的安装。
|
||||||
|
CannotContinue=安装程序不能继续。请点击“取消”退出。
|
||||||
|
ApplicationsFound=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。
|
||||||
|
ApplicationsFound2=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动这些应用程序。
|
||||||
|
CloseApplications=自动关闭应用程序(&A)
|
||||||
|
DontCloseApplications=不要关闭应用程序(&D)
|
||||||
|
ErrorCloseApplications=安装程序无法自动关闭所有应用程序。建议您在继续之前,关闭所有在使用需要由安装程序更新的文件的应用程序。
|
||||||
|
PrepareToInstallNeedsRestart=安装程序必须重启您的计算机。计算机重启后,请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动?
|
||||||
|
|
||||||
|
; *** “正在安装”向导页
|
||||||
|
WizardInstalling=正在安装
|
||||||
|
InstallingLabel=安装程序正在安装 [name] 到您的电脑,请稍候。
|
||||||
|
|
||||||
|
; *** “安装完成”向导页
|
||||||
|
FinishedHeadingLabel=[name] 安装完成
|
||||||
|
FinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。
|
||||||
|
FinishedLabel=安装程序已在您的电脑中安装了 [name]。您可以通过已安装的快捷方式运行此应用程序。
|
||||||
|
ClickFinish=点击“完成”退出安装程序。
|
||||||
|
FinishedRestartLabel=为完成 [name] 的安装,安装程序必须重新启动您的电脑。要立即重启吗?
|
||||||
|
FinishedRestartMessage=为完成 [name] 的安装,安装程序必须重新启动您的电脑。%n%n要立即重启吗?
|
||||||
|
ShowReadmeCheck=是,我想查阅自述文件
|
||||||
|
YesRadio=是,立即重启电脑(&Y)
|
||||||
|
NoRadio=否,稍后重启电脑(&N)
|
||||||
|
; used for example as 'Run MyProg.exe'
|
||||||
|
RunEntryExec=运行 %1
|
||||||
|
; used for example as 'View Readme.txt'
|
||||||
|
RunEntryShellExec=查阅 %1
|
||||||
|
|
||||||
|
; *** “安装程序需要下一张磁盘”提示
|
||||||
|
ChangeDiskTitle=安装程序需要下一张磁盘
|
||||||
|
SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。
|
||||||
|
PathLabel=路径(&P):
|
||||||
|
FileNotInDir2=“%2”中找不到文件“%1”。请插入正确的磁盘或选择其他文件夹。
|
||||||
|
SelectDirectoryLabel=请指定下一张磁盘的位置。
|
||||||
|
|
||||||
|
; *** 安装阶段消息
|
||||||
|
SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。
|
||||||
|
AbortRetryIgnoreSelectAction=选择操作
|
||||||
|
AbortRetryIgnoreRetry=重试(&T)
|
||||||
|
AbortRetryIgnoreIgnore=忽略错误并继续(&I)
|
||||||
|
AbortRetryIgnoreCancel=关闭安装程序
|
||||||
|
RetryCancelSelectAction=选择操作
|
||||||
|
RetryCancelRetry=重试(&T)
|
||||||
|
RetryCancelCancel=取消(&C)
|
||||||
|
|
||||||
|
; *** 安装状态消息
|
||||||
|
StatusClosingApplications=正在关闭应用程序...
|
||||||
|
StatusCreateDirs=正在创建目录...
|
||||||
|
StatusExtractFiles=正在提取文件...
|
||||||
|
StatusDownloadFiles=正在下载文件...
|
||||||
|
StatusCreateIcons=正在创建快捷方式...
|
||||||
|
StatusCreateIniEntries=正在创建 INI 条目...
|
||||||
|
StatusCreateRegistryEntries=正在创建注册表条目...
|
||||||
|
StatusRegisterFiles=正在注册文件...
|
||||||
|
StatusSavingUninstall=正在保存卸载信息...
|
||||||
|
StatusRunProgram=正在完成安装...
|
||||||
|
StatusRestartingApplications=正在重启应用程序...
|
||||||
|
StatusRollback=正在撤销更改...
|
||||||
|
|
||||||
|
; *** 其他错误
|
||||||
|
ErrorInternal2=内部错误:%1
|
||||||
|
ErrorFunctionFailedNoCode=%1 失败
|
||||||
|
ErrorFunctionFailed=%1 失败;错误代码 %2
|
||||||
|
ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3
|
||||||
|
ErrorExecutingProgram=无法执行文件:%n%1
|
||||||
|
|
||||||
|
; *** 注册表错误
|
||||||
|
ErrorRegOpenKey=打开注册表项时出错:%n%1\%2
|
||||||
|
ErrorRegCreateKey=创建注册表项时出错:%n%1\%2
|
||||||
|
ErrorRegWriteKey=写入注册表项时出错:%n%1\%2
|
||||||
|
|
||||||
|
; *** INI 错误
|
||||||
|
ErrorIniEntry=在文件“%1”中创建 INI 条目时出错。
|
||||||
|
|
||||||
|
; *** 文件复制错误
|
||||||
|
FileAbortRetryIgnoreSkipNotRecommended=跳过此文件(&S) (不推荐)
|
||||||
|
FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐)
|
||||||
|
SourceIsCorrupted=源文件已损坏
|
||||||
|
SourceDoesntExist=源文件“%1”不存在
|
||||||
|
SourceVerificationFailed=源文件验证失败: %1
|
||||||
|
VerificationSignatureDoesntExist=签名文件“%1”不存在
|
||||||
|
VerificationSignatureInvalid=签名文件“%1”无效
|
||||||
|
VerificationKeyNotFound=签名文件“%1”使用了未知密钥
|
||||||
|
VerificationFileNameIncorrect=文件名不正确
|
||||||
|
VerificationFileTagIncorrect=文件标签不正确
|
||||||
|
VerificationFileSizeIncorrect=文件大小不正确
|
||||||
|
VerificationFileHashIncorrect=文件哈希值不正确
|
||||||
|
ExistingFileReadOnly2=无法替换现有文件,它是只读的。
|
||||||
|
ExistingFileReadOnlyRetry=移除只读属性并重试(&R)
|
||||||
|
ExistingFileReadOnlyKeepExisting=保留现有文件(&K)
|
||||||
|
ErrorReadingExistingDest=尝试读取现有文件时出错:
|
||||||
|
FileExistsSelectAction=选择操作
|
||||||
|
FileExists2=文件已经存在。
|
||||||
|
FileExistsOverwriteExisting=覆盖已存在的文件(&O)
|
||||||
|
FileExistsKeepExisting=保留现有的文件(&K)
|
||||||
|
FileExistsOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)
|
||||||
|
ExistingFileNewerSelectAction=选择操作
|
||||||
|
ExistingFileNewer2=现有的文件比安装程序将要安装的文件还要新。
|
||||||
|
ExistingFileNewerOverwriteExisting=覆盖已存在的文件(&O)
|
||||||
|
ExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐)
|
||||||
|
ExistingFileNewerOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)
|
||||||
|
ErrorChangingAttr=尝试更改下列现有文件的属性时出错:
|
||||||
|
ErrorCreatingTemp=尝试在目标目录创建文件时出错:
|
||||||
|
ErrorReadingSource=尝试读取下列源文件时出错:
|
||||||
|
ErrorCopying=尝试复制下列文件时出错:
|
||||||
|
ErrorDownloading=下载文件时出错:
|
||||||
|
ErrorExtracting=解压压缩文件时出错:
|
||||||
|
ErrorReplacingExistingFile=尝试替换现有文件时出错:
|
||||||
|
ErrorRestartReplace=重启并替换失败:
|
||||||
|
ErrorRenamingTemp=尝试重命名下列目标目录中的一个文件时出错:
|
||||||
|
ErrorRegisterServer=无法注册 DLL/OCX:%1
|
||||||
|
ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1
|
||||||
|
ErrorRegisterTypeLib=无法注册类库:%1
|
||||||
|
|
||||||
|
; *** 卸载显示名字标记
|
||||||
|
; used for example as 'My Program (32-bit)'
|
||||||
|
UninstallDisplayNameMark=%1 (%2)
|
||||||
|
; used for example as 'My Program (32-bit, All users)'
|
||||||
|
UninstallDisplayNameMarks=%1 (%2, %3)
|
||||||
|
UninstallDisplayNameMark32Bit=32 位
|
||||||
|
UninstallDisplayNameMark64Bit=64 位
|
||||||
|
UninstallDisplayNameMarkAllUsers=所有用户
|
||||||
|
UninstallDisplayNameMarkCurrentUser=当前用户
|
||||||
|
|
||||||
|
; *** 安装后错误
|
||||||
|
ErrorOpeningReadme=尝试打开自述文件时出错。
|
||||||
|
ErrorRestartingComputer=安装程序无法重启电脑,请手动重启。
|
||||||
|
|
||||||
|
; *** 卸载消息
|
||||||
|
UninstallNotFound=文件“%1”不存在。无法卸载。
|
||||||
|
UninstallOpenError=文件“%1”不能被打开。无法卸载。
|
||||||
|
UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载
|
||||||
|
UninstallUnknownEntry=卸载日志中遇到一个未知条目 (%1)
|
||||||
|
ConfirmUninstall=您确认要完全移除 %1 及其所有组件吗?
|
||||||
|
UninstallOnlyOnWin64=仅允许在 64 位 Windows 中卸载此程序。
|
||||||
|
OnlyAdminCanUninstall=仅使用管理员权限的用户能完成此卸载。
|
||||||
|
UninstallStatusLabel=正在从您的电脑中移除 %1,请稍候。
|
||||||
|
UninstalledAll=已顺利从您的电脑中移除 %1。
|
||||||
|
UninstalledMost=%1 卸载完成。%n%n有部分内容未能被删除,但您可以手动删除它们。
|
||||||
|
UninstalledAndNeedsRestart=为完成 %1 的卸载,需要重启您的电脑。%n%n立即重启电脑吗?
|
||||||
|
UninstallDataCorrupted=文件“%1”已损坏。无法卸载
|
||||||
|
|
||||||
|
; *** 卸载状态消息
|
||||||
|
ConfirmDeleteSharedFileTitle=删除共享的文件吗?
|
||||||
|
ConfirmDeleteSharedFile2=系统表示下列共享的文件已不有其他程序使用。您希望卸载程序删除这些共享的文件吗?%n%n如果删除这些文件,但仍有程序在使用这些文件,则这些程序可能出现异常。如果您不能确定,请选择“否”,在系统中保留这些文件以免引发问题。
|
||||||
|
SharedFileNameLabel=文件名:
|
||||||
|
SharedFileLocationLabel=位置:
|
||||||
|
WizardUninstalling=卸载状态
|
||||||
|
StatusUninstalling=正在卸载 %1...
|
||||||
|
|
||||||
|
; *** Shutdown block reasons
|
||||||
|
ShutdownBlockReasonInstallingApp=正在安装 %1。
|
||||||
|
ShutdownBlockReasonUninstallingApp=正在卸载 %1。
|
||||||
|
|
||||||
|
; The custom messages below aren't used by Setup itself, but if you make
|
||||||
|
; use of them in your scripts, you'll want to translate them.
|
||||||
|
|
||||||
|
[CustomMessages]
|
||||||
|
|
||||||
|
NameAndVersion=%1 版本 %2
|
||||||
|
AdditionalIcons=附加快捷方式:
|
||||||
|
CreateDesktopIcon=创建桌面快捷方式(&D)
|
||||||
|
CreateQuickLaunchIcon=创建快速启动栏快捷方式(&Q)
|
||||||
|
ProgramOnTheWeb=%1 网站
|
||||||
|
UninstallProgram=卸载 %1
|
||||||
|
LaunchProgram=运行 %1
|
||||||
|
AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A)
|
||||||
|
AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联...
|
||||||
|
AutoStartProgramGroupDescription=启动:
|
||||||
|
AutoStartProgram=自动启动 %1
|
||||||
|
AddonHostProgramNotFound=您选择的文件夹中无法找到 %1。%n%n您要继续吗?
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 685 B |
|
|
@ -0,0 +1,135 @@
|
||||||
|
# MiroFish Windows 打包使用说明
|
||||||
|
|
||||||
|
本文档说明如何将 MiroFish 项目打包成 Windows 可执行安装程序。
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
| 工具 | 版本要求 | 用途 | 下载地址 |
|
||||||
|
|------|---------|------|----------|
|
||||||
|
| **Python** | 3.11+ | 后端打包 | https://python.org |
|
||||||
|
| **Node.js** | 18+ | 前端构建 | https://nodejs.org |
|
||||||
|
| **uv** | 最新版 | Python 包管理 | `pip install uv` |
|
||||||
|
| **Inno Setup** | 6.x | 创建安装程序 | https://jrsoftware.org/isinfo.php |
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 方式一:一键打包(推荐)
|
||||||
|
|
||||||
|
使用嵌入式 Python 模式打包(体积较小,适合大多数情况):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\installer\build.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式二:PyInstaller 打包
|
||||||
|
|
||||||
|
使用 PyInstaller 完全打包(体积大,但完全独立):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\installer\build.ps1 -PyInstaller
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ 注意:PyInstaller 模式打包时间长,输出文件可能超过 1GB
|
||||||
|
|
||||||
|
### 方式三:分步执行
|
||||||
|
|
||||||
|
如果只需要部分步骤,可以使用参数跳过:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 跳过前端构建(如果前端没有修改)
|
||||||
|
.\installer\build.ps1 -SkipFrontend
|
||||||
|
|
||||||
|
# 跳过后端处理(如果后端没有修改)
|
||||||
|
.\installer\build.ps1 -SkipBackend
|
||||||
|
|
||||||
|
# 跳过安装程序创建(只生成可执行文件)
|
||||||
|
.\installer\build.ps1 -SkipInstaller
|
||||||
|
|
||||||
|
# 清理旧构建后重新开始
|
||||||
|
.\installer\build.ps1 -Clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## 输出文件
|
||||||
|
|
||||||
|
打包完成后,会生成以下文件(嵌入式 Python 模式):
|
||||||
|
|
||||||
|
```
|
||||||
|
MiroFish_exe/
|
||||||
|
├── dist/
|
||||||
|
│ └── MiroFish/ # 可直接运行的目录
|
||||||
|
│ ├── MiroFish.exe # 主启动器(双击运行)
|
||||||
|
│ ├── python/ # 嵌入式 Python 运行时
|
||||||
|
│ │ ├── python.exe
|
||||||
|
│ │ ├── Lib/
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── backend/ # 后端源代码
|
||||||
|
│ │ ├── run.py
|
||||||
|
│ │ ├── app/
|
||||||
|
│ │ └── .env # 安装时生成的配置
|
||||||
|
│ └── frontend/
|
||||||
|
│ └── dist/ # 静态文件
|
||||||
|
│
|
||||||
|
└── installer/
|
||||||
|
└── output/
|
||||||
|
└── MiroFish_Setup_0.1.0.exe # 安装程序
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安装程序功能
|
||||||
|
|
||||||
|
生成的安装程序 `MiroFish_Setup_0.1.0.exe` 包含:
|
||||||
|
|
||||||
|
1. **欢迎页面**:显示应用信息
|
||||||
|
2. **许可协议**:AGPL-3.0 许可证
|
||||||
|
3. **安装目录选择**:用户可自定义安装位置
|
||||||
|
4. **API 配置页面**:
|
||||||
|
- LLM API Key(必填)
|
||||||
|
- LLM Base URL(默认:阿里百炼)
|
||||||
|
- LLM Model Name(默认:qwen-plus)
|
||||||
|
- ZEP API Key(必填)
|
||||||
|
5. **安装进度**:显示文件复制进度
|
||||||
|
6. **完成页面**:可选立即启动
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Inno Setup 未找到
|
||||||
|
|
||||||
|
如果看到警告"未找到 Inno Setup",请:
|
||||||
|
1. 下载安装 [Inno Setup 6](https://jrsoftware.org/isinfo.php)
|
||||||
|
2. 确保安装到默认位置或将路径添加到环境变量
|
||||||
|
3. 重新运行打包脚本
|
||||||
|
|
||||||
|
### 后端打包失败
|
||||||
|
|
||||||
|
检查以下几点:
|
||||||
|
1. 确保 Python 3.11+ 已安装
|
||||||
|
2. 确保 uv 包管理器已安装:`pip install uv`
|
||||||
|
3. 确保后端依赖已安装:`cd backend && uv sync`
|
||||||
|
|
||||||
|
### 前端构建失败
|
||||||
|
|
||||||
|
检查以下几点:
|
||||||
|
1. 确保 Node.js 18+ 已安装
|
||||||
|
2. 确保前端依赖已安装:`cd frontend && npm install`
|
||||||
|
|
||||||
|
### 运行时缺少 DLL
|
||||||
|
|
||||||
|
PyInstaller 已自动包含大部分依赖,如果仍有问题:
|
||||||
|
1. 在干净的 Windows 环境测试
|
||||||
|
2. 安装 [Visual C++ Redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe)
|
||||||
|
|
||||||
|
## 修改配置页面
|
||||||
|
|
||||||
|
如需修改安装程序的配置页面,编辑 `installer/setup.iss` 文件:
|
||||||
|
|
||||||
|
- 添加/删除配置项:修改 `InitializeWizard` 过程
|
||||||
|
- 修改默认值:修改对应的 `Edit.Text` 属性
|
||||||
|
- 修改验证逻辑:修改 `NextButtonClick` 函数
|
||||||
|
- 修改 .env 生成:修改 `CurStepChanged` 过程
|
||||||
|
|
||||||
|
## 版本更新
|
||||||
|
|
||||||
|
更新版本号时,修改以下位置:
|
||||||
|
|
||||||
|
1. `installer/setup.iss` 第 7 行:`#define MyAppVersion "x.x.x"`
|
||||||
|
2. `package.json` 中的 `version` 字段
|
||||||
|
3. `backend/pyproject.toml` 中的 `version` 字段
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
# MiroFish Windows build script
|
||||||
|
# Requirements: Python 3.11+, Node.js 18+, Inno Setup 6
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# .\build.ps1
|
||||||
|
# .\build.ps1 -PyInstaller
|
||||||
|
# .\build.ps1 -SkipFrontend
|
||||||
|
# .\build.ps1 -SkipBackend
|
||||||
|
# .\build.ps1 -SkipInstaller
|
||||||
|
# .\build.ps1 -Clean
|
||||||
|
|
||||||
|
param(
|
||||||
|
[switch]$SkipFrontend,
|
||||||
|
[switch]$SkipBackend,
|
||||||
|
[switch]$SkipInstaller,
|
||||||
|
[switch]$Clean,
|
||||||
|
[switch]$PyInstaller
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$DistDir = Join-Path $ProjectRoot "dist"
|
||||||
|
$InstallerDir = Join-Path $ProjectRoot "installer"
|
||||||
|
$PythonVersion = "3.11.9"
|
||||||
|
$PythonEmbedUrl = "https://www.python.org/ftp/python/$PythonVersion/python-$PythonVersion-embed-amd64.zip"
|
||||||
|
|
||||||
|
Write-Host "============================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " MiroFish Windows Builder" -ForegroundColor Cyan
|
||||||
|
Write-Host "============================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
if ($PyInstaller) {
|
||||||
|
Write-Host "Mode: PyInstaller (large, standalone)" -ForegroundColor Magenta
|
||||||
|
} else {
|
||||||
|
Write-Host "Mode: Embedded Python (recommended)" -ForegroundColor Magenta
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
if ($Clean -or -not (Test-Path $DistDir)) {
|
||||||
|
Write-Host "[1/5] Cleaning output..." -ForegroundColor Yellow
|
||||||
|
if (Test-Path $DistDir) {
|
||||||
|
Remove-Item -Recurse -Force $DistDir
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Force -Path $DistDir | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$MiroFishDist = Join-Path $DistDir "MiroFish"
|
||||||
|
New-Item -ItemType Directory -Force -Path $MiroFishDist | Out-Null
|
||||||
|
New-Item -ItemType Directory -Force -Path (Join-Path $MiroFishDist "backend") | Out-Null
|
||||||
|
New-Item -ItemType Directory -Force -Path (Join-Path $MiroFishDist "frontend") | Out-Null
|
||||||
|
|
||||||
|
if (-not $SkipFrontend) {
|
||||||
|
Write-Host "[2/5] Building frontend..." -ForegroundColor Yellow
|
||||||
|
Push-Location (Join-Path $ProjectRoot "frontend")
|
||||||
|
try {
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
$FrontendDist = Join-Path -Path $ProjectRoot -ChildPath "frontend\\dist"
|
||||||
|
$TargetFrontendDist = Join-Path -Path $MiroFishDist -ChildPath "frontend\\dist"
|
||||||
|
if (Test-Path $FrontendDist) {
|
||||||
|
if (Test-Path $TargetFrontendDist) {
|
||||||
|
Remove-Item -Recurse -Force $TargetFrontendDist
|
||||||
|
}
|
||||||
|
Copy-Item -Recurse -Force $FrontendDist $TargetFrontendDist
|
||||||
|
Write-Host " Frontend build complete" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
throw "Frontend build failed: dist not found"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "[2/5] Skipped frontend build" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $SkipBackend) {
|
||||||
|
Write-Host "[3/5] Preparing backend..." -ForegroundColor Yellow
|
||||||
|
if ($PyInstaller) {
|
||||||
|
Write-Host " Using PyInstaller..." -ForegroundColor Gray
|
||||||
|
Push-Location (Join-Path $ProjectRoot "backend")
|
||||||
|
try {
|
||||||
|
uv sync
|
||||||
|
uv pip install pyinstaller
|
||||||
|
uv run pyinstaller `
|
||||||
|
--name "mirofish_backend" `
|
||||||
|
--distpath "$DistDir\backend_temp" `
|
||||||
|
--workpath "$DistDir\build" `
|
||||||
|
--specpath "$DistDir" `
|
||||||
|
--add-data "app;app" `
|
||||||
|
--hidden-import "flask" `
|
||||||
|
--hidden-import "flask_cors" `
|
||||||
|
--hidden-import "openai" `
|
||||||
|
--hidden-import "zep_cloud" `
|
||||||
|
--hidden-import "pydantic" `
|
||||||
|
--hidden-import "dotenv" `
|
||||||
|
--hidden-import "charset_normalizer" `
|
||||||
|
--hidden-import "chardet" `
|
||||||
|
--hidden-import "fitz" `
|
||||||
|
--noconfirm `
|
||||||
|
--console `
|
||||||
|
run.py
|
||||||
|
$BackendBuild = Join-Path -Path $DistDir -ChildPath "backend_temp\\mirofish_backend"
|
||||||
|
if (Test-Path $BackendBuild) {
|
||||||
|
Copy-Item -Recurse -Force "$BackendBuild\*" (Join-Path $MiroFishDist "backend")
|
||||||
|
Write-Host " Backend build complete" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
throw "Backend build failed"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " Downloading embedded Python $PythonVersion..." -ForegroundColor Gray
|
||||||
|
$PythonZip = Join-Path $DistDir "python-embed.zip"
|
||||||
|
$PythonDir = Join-Path $MiroFishDist "python"
|
||||||
|
if (-not (Test-Path $PythonZip)) {
|
||||||
|
Invoke-WebRequest -Uri $PythonEmbedUrl -OutFile $PythonZip
|
||||||
|
}
|
||||||
|
Write-Host " Extracting embedded Python..." -ForegroundColor Gray
|
||||||
|
Expand-Archive -Path $PythonZip -DestinationPath $PythonDir -Force
|
||||||
|
|
||||||
|
$PthFile = Get-ChildItem -Path $PythonDir -Filter "python*._pth" | Select-Object -First 1
|
||||||
|
if ($PthFile) {
|
||||||
|
$PthContent = Get-Content $PthFile.FullName
|
||||||
|
$PthContent = $PthContent -replace "^#import site", "import site"
|
||||||
|
if ($PthContent -notcontains "Lib\site-packages") {
|
||||||
|
$PthContent += "Lib\site-packages"
|
||||||
|
}
|
||||||
|
Set-Content -Path $PthFile.FullName -Value $PthContent
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host " Installing pip..." -ForegroundColor Gray
|
||||||
|
$GetPipUrl = "https://bootstrap.pypa.io/get-pip.py"
|
||||||
|
$GetPipPath = Join-Path $DistDir "get-pip.py"
|
||||||
|
if (-not (Test-Path $GetPipPath)) {
|
||||||
|
Invoke-WebRequest -Uri $GetPipUrl -OutFile $GetPipPath
|
||||||
|
}
|
||||||
|
$PythonExe = Join-Path $PythonDir "python.exe"
|
||||||
|
& $PythonExe $GetPipPath --no-warn-script-location
|
||||||
|
|
||||||
|
Write-Host " Installing backend deps..." -ForegroundColor Gray
|
||||||
|
$RequirementsFile = Join-Path -Path $ProjectRoot -ChildPath "backend\\requirements.txt"
|
||||||
|
& $PythonExe -m pip install -r $RequirementsFile --no-warn-script-location -q
|
||||||
|
|
||||||
|
Write-Host " Copying backend sources..." -ForegroundColor Gray
|
||||||
|
$BackendSrc = Join-Path -Path $ProjectRoot -ChildPath "backend"
|
||||||
|
$BackendDst = Join-Path -Path $MiroFishDist -ChildPath "backend"
|
||||||
|
Copy-Item -Force (Join-Path -Path $BackendSrc -ChildPath "run.py") $BackendDst
|
||||||
|
Copy-Item -Recurse -Force (Join-Path -Path $BackendSrc -ChildPath "app") (Join-Path -Path $BackendDst -ChildPath "app")
|
||||||
|
Write-Host " Backend prepared" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "[3/5] Skipped backend" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "[4/5] Building launcher..." -ForegroundColor Yellow
|
||||||
|
Push-Location $ProjectRoot
|
||||||
|
try {
|
||||||
|
$pip = pip --version 2>$null
|
||||||
|
if (-not $?) {
|
||||||
|
Write-Host " Installing PyInstaller..." -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
pip install pyinstaller -q
|
||||||
|
pyinstaller `
|
||||||
|
--name "MiroFish" `
|
||||||
|
--distpath "$DistDir\launcher_temp" `
|
||||||
|
--workpath "$DistDir\build" `
|
||||||
|
--specpath "$DistDir" `
|
||||||
|
--icon "$InstallerDir\MiroFish.ico" `
|
||||||
|
--onefile `
|
||||||
|
--windowed `
|
||||||
|
--noconfirm `
|
||||||
|
launcher.py
|
||||||
|
$LauncherExe = Join-Path -Path $DistDir -ChildPath "launcher_temp\\MiroFish.exe"
|
||||||
|
Copy-Item -Force $LauncherExe $MiroFishDist
|
||||||
|
Write-Host " Launcher built" -ForegroundColor Green
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $SkipInstaller) {
|
||||||
|
Write-Host "[5/5] Building installer..." -ForegroundColor Yellow
|
||||||
|
$ISCCPaths = @(
|
||||||
|
"C:\Program Files (x86)\Inno Setup 6\ISCC.exe",
|
||||||
|
"C:\Program Files\Inno Setup 6\ISCC.exe",
|
||||||
|
"$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe"
|
||||||
|
)
|
||||||
|
$ISCC = $null
|
||||||
|
foreach ($path in $ISCCPaths) {
|
||||||
|
if (Test-Path $path) {
|
||||||
|
$ISCC = $path
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($ISCC) {
|
||||||
|
Push-Location $InstallerDir
|
||||||
|
try {
|
||||||
|
& $ISCC "setup.iss"
|
||||||
|
Write-Host " Installer built" -ForegroundColor Green
|
||||||
|
Write-Host (" Output: {0}" -f (Join-Path $InstallerDir "output")) -ForegroundColor Cyan
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " Inno Setup not found; skipping installer" -ForegroundColor Yellow
|
||||||
|
Write-Host " Install Inno Setup 6: https://jrsoftware.org/isinfo.php" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "[5/5] Skipped installer" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Cleaning temp files..." -ForegroundColor Gray
|
||||||
|
Remove-Item -Recurse -Force (Join-Path $DistDir "backend_temp") -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item -Recurse -Force (Join-Path $DistDir "launcher_temp") -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item -Recurse -Force (Join-Path $DistDir "build") -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item -Force (Join-Path $DistDir "*.spec") -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item -Force (Join-Path $DistDir "python-embed.zip") -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item -Force (Join-Path $DistDir "get-pip.py") -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "============================================" -ForegroundColor Green
|
||||||
|
Write-Host " Done" -ForegroundColor Green
|
||||||
|
Write-Host "============================================" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Output: $MiroFishDist" -ForegroundColor Cyan
|
||||||
|
if (Test-Path (Join-Path $InstallerDir "output")) {
|
||||||
|
Write-Host ("Installer: {0}" -f (Join-Path $InstallerDir "output")) -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
; MiroFish 安装程序脚本
|
||||||
|
; 使用 Inno Setup 6.x 编译
|
||||||
|
; 中文界面,包含 API 配置页面
|
||||||
|
|
||||||
|
#define MyAppName "MiroFish"
|
||||||
|
#define MyAppVersion "0.1.0"
|
||||||
|
#define MyAppPublisher "MiroFish Team"
|
||||||
|
#define MyAppURL "https://github.com/666ghj/MiroFish"
|
||||||
|
#define MyAppExeName "MiroFish.exe"
|
||||||
|
|
||||||
|
[Setup]
|
||||||
|
; 基本信息
|
||||||
|
AppId={{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
|
||||||
|
AppName={#MyAppName}
|
||||||
|
AppVersion={#MyAppVersion}
|
||||||
|
AppVerName={#MyAppName} {#MyAppVersion}
|
||||||
|
AppPublisher={#MyAppPublisher}
|
||||||
|
AppPublisherURL={#MyAppURL}
|
||||||
|
AppSupportURL={#MyAppURL}
|
||||||
|
AppUpdatesURL={#MyAppURL}
|
||||||
|
|
||||||
|
; 安装目录
|
||||||
|
DefaultDirName={autopf}\{#MyAppName}
|
||||||
|
DefaultGroupName={#MyAppName}
|
||||||
|
DisableProgramGroupPage=yes
|
||||||
|
|
||||||
|
; 输出设置
|
||||||
|
OutputDir=output
|
||||||
|
OutputBaseFilename=MiroFish_Setup_{#MyAppVersion}
|
||||||
|
SetupIconFile=MiroFish.ico
|
||||||
|
UninstallDisplayIcon={app}\MiroFish.ico
|
||||||
|
|
||||||
|
; 压缩设置
|
||||||
|
Compression=lzma2/ultra64
|
||||||
|
SolidCompression=yes
|
||||||
|
LZMAUseSeparateProcess=yes
|
||||||
|
|
||||||
|
; 权限设置
|
||||||
|
PrivilegesRequired=admin
|
||||||
|
PrivilegesRequiredOverridesAllowed=dialog
|
||||||
|
|
||||||
|
; 向导设置
|
||||||
|
WizardStyle=modern
|
||||||
|
WizardSizePercent=100
|
||||||
|
|
||||||
|
; 语言设置 - 使用中文
|
||||||
|
[Languages]
|
||||||
|
Name: "chinesesimplified"; MessagesFile: "ChineseSimplified.isl"
|
||||||
|
|
||||||
|
[CustomMessages]
|
||||||
|
chinesesimplified.ConfigTitle=API 配置
|
||||||
|
chinesesimplified.ConfigSubtitle=请填写必要的 API 密钥
|
||||||
|
chinesesimplified.ConfigInfo=提示: LLM 推荐使用阿里百炼平台 qwen-plus 模型,ZEP 每月免费额度即可支撑简单使用。
|
||||||
|
chinesesimplified.LLMApiKeyLabel=LLM API Key (必填):
|
||||||
|
chinesesimplified.LLMBaseURLLabel=LLM Base URL (可保持默认):
|
||||||
|
chinesesimplified.LLMModelNameLabel=LLM 模型名称 (可保持默认):
|
||||||
|
chinesesimplified.ZEPApiKeyLabel=ZEP API Key (必填):
|
||||||
|
chinesesimplified.LLMApiKeyRequired=请输入 LLM API Key!
|
||||||
|
chinesesimplified.ZEPApiKeyRequired=请输入 ZEP API Key!
|
||||||
|
|
||||||
|
[Tasks]
|
||||||
|
Name: "desktopicon"; Description: "创建桌面快捷方式"; GroupDescription: "附加选项:"
|
||||||
|
Name: "quicklaunchicon"; Description: "创建快速启动栏快捷方式"; GroupDescription: "附加选项:"; Flags: unchecked
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
; 主程序文件(包含 MiroFish.exe、backend、frontend、python 目录)
|
||||||
|
Source: "..\dist\MiroFish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
; 图标文件
|
||||||
|
Source: "MiroFish.ico"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\MiroFish.ico"
|
||||||
|
Name: "{group}\卸载 {#MyAppName}"; Filename: "{uninstallexe}"
|
||||||
|
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\MiroFish.ico"; Tasks: desktopicon
|
||||||
|
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon
|
||||||
|
|
||||||
|
[Run]
|
||||||
|
Filename: "{app}\{#MyAppExeName}"; Description: "立即启动 {#MyAppName}"; Flags: nowait postinstall skipifsilent
|
||||||
|
|
||||||
|
[Code]
|
||||||
|
var
|
||||||
|
ConfigPage: TInputQueryWizardPage;
|
||||||
|
LLMApiKeyEdit: TNewEdit;
|
||||||
|
LLMBaseURLEdit: TNewEdit;
|
||||||
|
LLMModelNameEdit: TNewEdit;
|
||||||
|
ZEPApiKeyEdit: TNewEdit;
|
||||||
|
|
||||||
|
procedure InitializeWizard;
|
||||||
|
var
|
||||||
|
LabelHeight: Integer;
|
||||||
|
EditHeight: Integer;
|
||||||
|
VerticalSpacing: Integer;
|
||||||
|
TopPos: Integer;
|
||||||
|
LabelLLMApiKey, LabelLLMBaseURL, LabelLLMModelName, LabelZEPApiKey: TLabel;
|
||||||
|
InfoLabel: TNewStaticText;
|
||||||
|
begin
|
||||||
|
LabelHeight := 16;
|
||||||
|
EditHeight := 23;
|
||||||
|
VerticalSpacing := 8;
|
||||||
|
// 增加初始 TopPos,避免与页面顶部说明重叠
|
||||||
|
TopPos := 10;
|
||||||
|
|
||||||
|
// 创建自定义配置页面
|
||||||
|
ConfigPage := CreateInputQueryPage(wpSelectDir,
|
||||||
|
CustomMessage('ConfigTitle'),
|
||||||
|
CustomMessage('ConfigSubtitle'),
|
||||||
|
''); // 移除这一行的长文本,改用 InfoLabel 显示,以便更好地控制布局
|
||||||
|
|
||||||
|
// 添加说明信息 (灰色斜体)
|
||||||
|
InfoLabel := TNewStaticText.Create(ConfigPage);
|
||||||
|
InfoLabel.Parent := ConfigPage.Surface;
|
||||||
|
InfoLabel.Caption := CustomMessage('ConfigInfo');
|
||||||
|
InfoLabel.Left := 0;
|
||||||
|
InfoLabel.Top := TopPos;
|
||||||
|
InfoLabel.AutoSize := False;
|
||||||
|
InfoLabel.Width := ConfigPage.SurfaceWidth;
|
||||||
|
InfoLabel.WordWrap := True; // 允许自动换行
|
||||||
|
InfoLabel.Font.Style := [fsItalic];
|
||||||
|
InfoLabel.Font.Color := clGray;
|
||||||
|
WizardForm.AdjustLabelHeight(InfoLabel);
|
||||||
|
|
||||||
|
// 重新计算 TopPos,根据文本实际高度 + 额外间距
|
||||||
|
TopPos := TopPos + InfoLabel.Height + 20;
|
||||||
|
|
||||||
|
// 1. LLM API Key
|
||||||
|
LabelLLMApiKey := TLabel.Create(ConfigPage);
|
||||||
|
LabelLLMApiKey.Parent := ConfigPage.Surface;
|
||||||
|
LabelLLMApiKey.Caption := CustomMessage('LLMApiKeyLabel');
|
||||||
|
LabelLLMApiKey.Left := 0;
|
||||||
|
LabelLLMApiKey.Top := TopPos;
|
||||||
|
TopPos := TopPos + LabelHeight + VerticalSpacing;
|
||||||
|
|
||||||
|
LLMApiKeyEdit := TNewEdit.Create(ConfigPage);
|
||||||
|
LLMApiKeyEdit.Parent := ConfigPage.Surface;
|
||||||
|
LLMApiKeyEdit.Left := 0;
|
||||||
|
LLMApiKeyEdit.Top := TopPos;
|
||||||
|
LLMApiKeyEdit.Width := ConfigPage.SurfaceWidth;
|
||||||
|
LLMApiKeyEdit.Height := EditHeight;
|
||||||
|
LLMApiKeyEdit.Text := '';
|
||||||
|
|
||||||
|
TopPos := TopPos + EditHeight + 12; // 组间距加大
|
||||||
|
|
||||||
|
// 2. LLM Base URL
|
||||||
|
LabelLLMBaseURL := TLabel.Create(ConfigPage);
|
||||||
|
LabelLLMBaseURL.Parent := ConfigPage.Surface;
|
||||||
|
LabelLLMBaseURL.Caption := CustomMessage('LLMBaseURLLabel');
|
||||||
|
LabelLLMBaseURL.Left := 0;
|
||||||
|
LabelLLMBaseURL.Top := TopPos;
|
||||||
|
TopPos := TopPos + LabelHeight + VerticalSpacing;
|
||||||
|
|
||||||
|
LLMBaseURLEdit := TNewEdit.Create(ConfigPage);
|
||||||
|
LLMBaseURLEdit.Parent := ConfigPage.Surface;
|
||||||
|
LLMBaseURLEdit.Left := 0;
|
||||||
|
LLMBaseURLEdit.Top := TopPos;
|
||||||
|
LLMBaseURLEdit.Width := ConfigPage.SurfaceWidth;
|
||||||
|
LLMBaseURLEdit.Height := EditHeight;
|
||||||
|
LLMBaseURLEdit.Text := 'https://dashscope.aliyuncs.com/compatible-mode/v1';
|
||||||
|
|
||||||
|
TopPos := TopPos + EditHeight + 12;
|
||||||
|
|
||||||
|
// 3. LLM Model Name
|
||||||
|
LabelLLMModelName := TLabel.Create(ConfigPage);
|
||||||
|
LabelLLMModelName.Parent := ConfigPage.Surface;
|
||||||
|
LabelLLMModelName.Caption := CustomMessage('LLMModelNameLabel');
|
||||||
|
LabelLLMModelName.Left := 0;
|
||||||
|
LabelLLMModelName.Top := TopPos;
|
||||||
|
TopPos := TopPos + LabelHeight + VerticalSpacing;
|
||||||
|
|
||||||
|
LLMModelNameEdit := TNewEdit.Create(ConfigPage);
|
||||||
|
LLMModelNameEdit.Parent := ConfigPage.Surface;
|
||||||
|
LLMModelNameEdit.Left := 0;
|
||||||
|
LLMModelNameEdit.Top := TopPos;
|
||||||
|
LLMModelNameEdit.Width := ConfigPage.SurfaceWidth;
|
||||||
|
LLMModelNameEdit.Height := EditHeight;
|
||||||
|
LLMModelNameEdit.Text := 'qwen-plus';
|
||||||
|
|
||||||
|
TopPos := TopPos + EditHeight + 12;
|
||||||
|
|
||||||
|
// 4. ZEP API Key
|
||||||
|
LabelZEPApiKey := TLabel.Create(ConfigPage);
|
||||||
|
LabelZEPApiKey.Parent := ConfigPage.Surface;
|
||||||
|
LabelZEPApiKey.Caption := CustomMessage('ZEPApiKeyLabel');
|
||||||
|
LabelZEPApiKey.Left := 0;
|
||||||
|
LabelZEPApiKey.Top := TopPos;
|
||||||
|
TopPos := TopPos + LabelHeight + VerticalSpacing;
|
||||||
|
|
||||||
|
ZEPApiKeyEdit := TNewEdit.Create(ConfigPage);
|
||||||
|
ZEPApiKeyEdit.Parent := ConfigPage.Surface;
|
||||||
|
ZEPApiKeyEdit.Left := 0;
|
||||||
|
ZEPApiKeyEdit.Top := TopPos;
|
||||||
|
ZEPApiKeyEdit.Width := ConfigPage.SurfaceWidth;
|
||||||
|
ZEPApiKeyEdit.Height := EditHeight;
|
||||||
|
ZEPApiKeyEdit.Text := '';
|
||||||
|
end;
|
||||||
|
|
||||||
|
function NextButtonClick(CurPageID: Integer): Boolean;
|
||||||
|
begin
|
||||||
|
Result := True;
|
||||||
|
|
||||||
|
// 在配置页面验证必填项
|
||||||
|
if CurPageID = ConfigPage.ID then
|
||||||
|
begin
|
||||||
|
if Trim(LLMApiKeyEdit.Text) = '' then
|
||||||
|
begin
|
||||||
|
MsgBox('请输入 LLM API Key!', mbError, MB_OK);
|
||||||
|
Result := False;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if Trim(ZEPApiKeyEdit.Text) = '' then
|
||||||
|
begin
|
||||||
|
MsgBox('请输入 ZEP API Key!', mbError, MB_OK);
|
||||||
|
Result := False;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure CurStepChanged(CurStep: TSetupStep);
|
||||||
|
var
|
||||||
|
EnvContent: String;
|
||||||
|
EnvFilePath: String;
|
||||||
|
begin
|
||||||
|
if CurStep = ssPostInstall then
|
||||||
|
begin
|
||||||
|
// 创建 .env 文件
|
||||||
|
EnvFilePath := ExpandConstant('{app}\backend\.env');
|
||||||
|
|
||||||
|
EnvContent := '# MiroFish configuration file' + #13#10;
|
||||||
|
EnvContent := EnvContent + '# Generated by installer' + #13#10;
|
||||||
|
EnvContent := EnvContent + #13#10;
|
||||||
|
EnvContent := EnvContent + '# LLM API configuration (OpenAI-compatible)' + #13#10;
|
||||||
|
EnvContent := EnvContent + 'LLM_API_KEY=' + LLMApiKeyEdit.Text + #13#10;
|
||||||
|
EnvContent := EnvContent + 'LLM_BASE_URL=' + LLMBaseURLEdit.Text + #13#10;
|
||||||
|
EnvContent := EnvContent + 'LLM_MODEL_NAME=' + LLMModelNameEdit.Text + #13#10;
|
||||||
|
EnvContent := EnvContent + #13#10;
|
||||||
|
EnvContent := EnvContent + '# ZEP configuration' + #13#10;
|
||||||
|
EnvContent := EnvContent + 'ZEP_API_KEY=' + ZEPApiKeyEdit.Text + #13#10;
|
||||||
|
|
||||||
|
EnvContent := #239#187#191 + EnvContent;
|
||||||
|
if not SaveStringToFile(EnvFilePath, EnvContent, False) then
|
||||||
|
begin
|
||||||
|
MsgBox('无法写入配置文件:' + #13#10 + EnvFilePath + #13#10 + #13#10 +
|
||||||
|
'请检查安装目录权限,或以管理员身份重新安装。', mbError, MB_OK);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
@ -0,0 +1,284 @@
|
||||||
|
"""
|
||||||
|
MiroFish 启动器
|
||||||
|
双击运行后自动启动后端服务和前端服务,并打开浏览器
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
import webbrowser
|
||||||
|
import signal
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
def get_base_path():
|
||||||
|
"""获取程序基础路径"""
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
# PyInstaller 打包后的路径
|
||||||
|
return Path(sys.executable).parent
|
||||||
|
else:
|
||||||
|
# 开发环境路径
|
||||||
|
return Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def has_usable_stdin() -> bool:
|
||||||
|
try:
|
||||||
|
return sys.stdin is not None and sys.stdin.isatty()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def show_error(title: str, message: str) -> None:
|
||||||
|
if sys.platform == "win32":
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
ctypes.windll.user32.MessageBoxW(None, message, title, 0x10)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
print(f"{title}\n{message}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
LOG_FILE: Optional[Path] = None
|
||||||
|
|
||||||
|
|
||||||
|
def log_line(message: str) -> None:
|
||||||
|
if not LOG_FILE:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with open(LOG_FILE, "a", encoding="utf-8", errors="replace") as f:
|
||||||
|
f.write(message + "\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_server(url: str, timeout: int = 30) -> bool:
|
||||||
|
"""等待服务器启动"""
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
import socket
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(url, timeout=1)
|
||||||
|
return True
|
||||||
|
except (urllib.error.URLError, ConnectionRefusedError, socket.timeout, TimeoutError):
|
||||||
|
time.sleep(0.5)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def read_env_lines(env_file: Path) -> list[str]:
|
||||||
|
data = env_file.read_bytes()
|
||||||
|
for encoding in ("utf-8", "utf-8-sig", "gbk", "gb2312"):
|
||||||
|
try:
|
||||||
|
return data.decode(encoding).splitlines()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
return data.decode("utf-8", errors="replace").splitlines()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
base_path = get_base_path()
|
||||||
|
|
||||||
|
# 配置路径
|
||||||
|
backend_path = base_path / "backend"
|
||||||
|
frontend_path = base_path / "frontend" / "dist"
|
||||||
|
env_file = backend_path / ".env"
|
||||||
|
|
||||||
|
# 检查 .env 文件是否存在
|
||||||
|
if not env_file.exists():
|
||||||
|
show_error(
|
||||||
|
"MiroFish 启动失败",
|
||||||
|
"未找到配置文件 .env。\n\n"
|
||||||
|
"请先运行安装程序并在“API 配置”页面填写密钥,或手动创建:\n"
|
||||||
|
f"{env_file}",
|
||||||
|
)
|
||||||
|
log_line(f".env missing: {env_file}")
|
||||||
|
if has_usable_stdin():
|
||||||
|
input("按回车键退出...")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
os.environ['FLASK_HOST'] = '127.0.0.1'
|
||||||
|
os.environ['FLASK_PORT'] = '5001'
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
os.environ['FLASK_DEBUG'] = 'False'
|
||||||
|
|
||||||
|
# 日志目录
|
||||||
|
logs_path = base_path / "logs"
|
||||||
|
try:
|
||||||
|
logs_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
except Exception:
|
||||||
|
temp_dir = Path(os.environ.get("TEMP", str(base_path)))
|
||||||
|
logs_path = temp_dir / "MiroFishLogs"
|
||||||
|
logs_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
backend_log = logs_path / "backend.log"
|
||||||
|
global LOG_FILE
|
||||||
|
LOG_FILE = logs_path / "launcher.log"
|
||||||
|
log_line("=== launcher start ===")
|
||||||
|
log_line(f"base_path={base_path}")
|
||||||
|
log_line(f"logs_path={logs_path}")
|
||||||
|
|
||||||
|
# 存储子进程
|
||||||
|
processes = []
|
||||||
|
|
||||||
|
def terminate_processes():
|
||||||
|
"""清理子进程"""
|
||||||
|
if has_usable_stdin():
|
||||||
|
print("\n正在关闭服务...")
|
||||||
|
for proc in processes:
|
||||||
|
try:
|
||||||
|
proc.terminate()
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
except:
|
||||||
|
proc.kill()
|
||||||
|
|
||||||
|
def handle_signal(signum, frame):
|
||||||
|
terminate_processes()
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
# 注册信号处理
|
||||||
|
signal.signal(signal.SIGINT, handle_signal)
|
||||||
|
signal.signal(signal.SIGTERM, handle_signal)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 启动后端服务
|
||||||
|
print("正在启动后端服务...")
|
||||||
|
|
||||||
|
# 加载 .env 文件到环境变量
|
||||||
|
for line in read_env_lines(env_file):
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith('#') and '=' in line:
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
os.environ[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
# 打包环境
|
||||||
|
# 优先检查嵌入式 Python
|
||||||
|
python_exe = base_path / "python" / "python.exe"
|
||||||
|
backend_exe = backend_path / "mirofish_backend.exe"
|
||||||
|
run_py = backend_path / "run.py"
|
||||||
|
|
||||||
|
if python_exe.exists() and run_py.exists():
|
||||||
|
# 嵌入式 Python 模式
|
||||||
|
print(" 使用嵌入式 Python 启动后端...")
|
||||||
|
with open(backend_log, "a", encoding="utf-8", errors="replace") as log_file:
|
||||||
|
log_file.write("\n=== backend start (embedded python) ===\n")
|
||||||
|
backend_proc = subprocess.Popen(
|
||||||
|
[str(python_exe), str(run_py)],
|
||||||
|
cwd=str(backend_path),
|
||||||
|
env=os.environ.copy(),
|
||||||
|
stdout=log_file,
|
||||||
|
stderr=log_file,
|
||||||
|
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||||
|
)
|
||||||
|
elif backend_exe.exists():
|
||||||
|
# PyInstaller 打包模式
|
||||||
|
print(" 使用打包的后端程序...")
|
||||||
|
with open(backend_log, "a", encoding="utf-8", errors="replace") as log_file:
|
||||||
|
log_file.write("\n=== backend start (pyinstaller) ===\n")
|
||||||
|
backend_proc = subprocess.Popen(
|
||||||
|
[str(backend_exe)],
|
||||||
|
cwd=str(backend_path),
|
||||||
|
env=os.environ.copy(),
|
||||||
|
stdout=log_file,
|
||||||
|
stderr=log_file,
|
||||||
|
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
show_error(
|
||||||
|
"MiroFish 启动失败",
|
||||||
|
"未找到后端启动程序。\n\n"
|
||||||
|
f"请检查以下路径是否存在:\n{python_exe}\n{backend_exe}",
|
||||||
|
)
|
||||||
|
if has_usable_stdin():
|
||||||
|
input("按回车键退出...")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
# 开发环境:使用 uv
|
||||||
|
backend_proc = subprocess.Popen(
|
||||||
|
["uv", "run", "python", "run.py"],
|
||||||
|
cwd=str(backend_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
processes.append(backend_proc)
|
||||||
|
|
||||||
|
# 启动前端静态文件服务
|
||||||
|
print("正在启动前端服务...")
|
||||||
|
|
||||||
|
# 使用内置线程作为简单 HTTP 服务器
|
||||||
|
import http.server
|
||||||
|
import socketserver
|
||||||
|
|
||||||
|
class QuietHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, directory=str(frontend_path), **kwargs)
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
pass # 禁用日志输出
|
||||||
|
|
||||||
|
def run_frontend_server():
|
||||||
|
with socketserver.TCPServer(("127.0.0.1", 3000), QuietHandler) as httpd:
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
frontend_thread = threading.Thread(target=run_frontend_server, daemon=True)
|
||||||
|
frontend_thread.start()
|
||||||
|
|
||||||
|
# 等待后端服务启动
|
||||||
|
print("正在等待服务启动...")
|
||||||
|
time.sleep(0.5)
|
||||||
|
if backend_proc.poll() is not None:
|
||||||
|
show_error(
|
||||||
|
"MiroFish 启动失败",
|
||||||
|
"后端服务启动后立刻退出。\n\n"
|
||||||
|
f"请查看日志:\n{backend_log}"
|
||||||
|
)
|
||||||
|
log_line("backend exited immediately; see backend.log")
|
||||||
|
raise SystemExit(1)
|
||||||
|
if wait_for_server("http://127.0.0.1:5001/health", timeout=30):
|
||||||
|
print("后端服务已启动")
|
||||||
|
else:
|
||||||
|
print("警告:后端服务启动超时")
|
||||||
|
|
||||||
|
# 等待前端服务启动
|
||||||
|
if wait_for_server("http://127.0.0.1:3000/", timeout=10):
|
||||||
|
print("前端服务已启动")
|
||||||
|
else:
|
||||||
|
print("警告:前端服务启动超时")
|
||||||
|
|
||||||
|
# 打开浏览器
|
||||||
|
print("正在打开浏览器...")
|
||||||
|
webbrowser.open("http://127.0.0.1:3000")
|
||||||
|
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("MiroFish 已启动!")
|
||||||
|
print("前端地址: http://127.0.0.1:3000")
|
||||||
|
print("后端地址: http://127.0.0.1:5001")
|
||||||
|
print("=" * 50)
|
||||||
|
print("\n按 Ctrl+C 关闭服务...")
|
||||||
|
|
||||||
|
# 等待进程
|
||||||
|
while True:
|
||||||
|
# 检查后端进程是否仍在运行
|
||||||
|
if backend_proc.poll() is not None:
|
||||||
|
print("后端服务已停止")
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
show_error("MiroFish 启动失败", f"{e}")
|
||||||
|
log_line(f"launcher exception: {e}")
|
||||||
|
terminate_processes()
|
||||||
|
raise SystemExit(1)
|
||||||
|
finally:
|
||||||
|
terminate_processes()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue