diff --git a/backend/database.py b/backend/database.py index cb6c290..a92c61f 100644 --- a/backend/database.py +++ b/backend/database.py @@ -23,6 +23,7 @@ def _migrate_db(): """简单的数据库迁移:为旧表添加缺失的列""" migrations = [ ("tasks", "branch", "VARCHAR DEFAULT 'main'"), + ("tasks", "error_category", "VARCHAR"), ] with engine.connect() as conn: for table, column, col_type in migrations: diff --git a/backend/models.py b/backend/models.py index aef4c87..c629d2e 100644 --- a/backend/models.py +++ b/backend/models.py @@ -36,6 +36,7 @@ class Task(Base): # 错误 error_message = Column(Text) + error_category = Column(String) # certificate/provisioning/compilation/dependency/git/config/build/unknown # 配置快照 config_json = Column(Text) diff --git a/backend/routers/tasks.py b/backend/routers/tasks.py index 2bae8ce..4eec234 100644 --- a/backend/routers/tasks.py +++ b/backend/routers/tasks.py @@ -41,7 +41,7 @@ async def create_task(task: TaskCreate, db: Session = Depends(get_db)): app_name=app.get("name", ""), build_type=task.build_type, scheme_id=task.scheme_id, - scheme_name=scheme.get("name", ""), + scheme_name=scheme.get("displayName") or scheme.get("name", ""), obfuscation=task.obfuscation, branch=task.branch, status="pending", diff --git a/backend/schemas.py b/backend/schemas.py index c0c2a7b..3715035 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -34,6 +34,7 @@ class TaskResponse(BaseModel): qr_code_path: Optional[str] build_dir: Optional[str] error_message: Optional[str] + error_category: Optional[str] = None config_json: Optional[str] class Config: diff --git a/backend/services/build_service.py b/backend/services/build_service.py index e3332bd..94c1435 100644 --- a/backend/services/build_service.py +++ b/backend/services/build_service.py @@ -18,6 +18,67 @@ from ..config import ( from .log_streamer import log_streamer +class BuildError(Exception): + """带分类的打包错误""" + + def __init__(self, message: str, category: str = "unknown", detail: str = ""): + super().__init__(message) + self.category = category + self.detail = detail + + +# 错误分类规则:(关键词列表, category, 友好提示) +_ERROR_RULES = [ + (["No signing certificate", "Signing certificate \"", "Code Signing Error", + "Code Sign error", "errSecInternalComponent", "CSSMERR_TP_NOT_TRUSTED", + "iPhone Distribution", "iPhone Developer: no identity found"], + "certificate", "签名证书问题:证书过期、未安装或不匹配,请检查钥匙串中的证书"), + (["Provisioning profile", "No matching provisioning profiles", + "embedded.mobileprovision", "PROVISIONING_PROFILE_SPECIFIER", + "provisioning profile", "No matching", + "requires a provisioning profile"], + "provisioning", "描述文件问题:描述文件过期、不匹配或未安装,请检查 Apple Developer 后台"), + (["Undefined symbols", "linker command failed", "ld: symbol(s) not found", + "ld: framework not found for"], + "compilation", "链接错误:符号未定义或框架缺失,请检查代码引用"), + (["Swift Compiler Error", "Use of undeclared identifier", + "cannot find type", "cannot find symbol in scope", "no such module"], + "compilation", "Swift 编译错误:请检查代码语法和类型引用"), + ([" error:", "fatal error:"], + "compilation", "代码编译错误:请检查代码是否有语法或类型错误"), + (["CocoaPods", "pod install", "ld: library not found for", + "library not found for", "Sandbox is not in sync with Podfile.lock"], + "dependency", "依赖问题:Pod 依赖缺失或版本不兼容,请检查 Podfile"), + (["xcodebuild: error", "Unable to access scheme", "does not contain a scheme"], + "build", "构建配置错误:Scheme 或 Workspace 配置有误"), +] + + +def _classify_build_error(output_lines: list, step: str) -> tuple: + """解析构建输出,返回 (category, friendly_message) + + Args: + output_lines: 构建过程的输出行列表 + step: 失败步骤 (archive/export) + + Returns: + (category, friendly_message) 元组 + """ + output_text = "\n".join(output_lines) + + for keywords, category, hint in _ERROR_RULES: + for kw in keywords: + if kw.lower() in output_text.lower(): + return category, hint + + # 未匹配到特定分类 + if step == "archive": + return "build", "Archive 失败:请查看详细日志排查原因" + elif step == "export": + return "build", "导出 IPA 失败:请查看详细日志排查原因" + return "unknown", "打包失败:请查看详细日志排查原因" + + def _cleanup_build_dir(build_dir: Path): """删除整个打包目录""" try: @@ -234,10 +295,25 @@ async def run_build_task(task_id: str): await asyncio.to_thread(_cleanup_build_dir, build_dir) except Exception as e: + if isinstance(e, BuildError): + category = e.category + error_msg = str(e) + else: + # 根据异常消息推断分类 + msg = str(e).lower() + if "git" in msg: + category = "git" + elif "配置替换" in msg: + category = "config" + elif "混淆" in msg: + category = "config" + else: + category = "unknown" + error_msg = str(e) await asyncio.to_thread(_db_update, db, task, status="failed", completed_at=datetime.utcnow(), - error_message=str(e)) - await log_streamer.emit_error(task_id, f"打包失败: {str(e)}") + error_message=error_msg, error_category=category) + await log_streamer.emit_error(task_id, f"打包失败: {error_msg}") if build_dir and build_dir.exists(): await asyncio.to_thread(_cleanup_build_dir, build_dir) @@ -470,6 +546,7 @@ async def build_project(task_id: str, task, build_dir: Path) -> Path: f"-derivedDataPath {export_path / 'derived_data'} " f"-destination generic/platform=ios -quiet" ) + archive_output = [] process = await asyncio.create_subprocess_shell( archive_cmd, cwd=str(build_dir), @@ -479,11 +556,13 @@ async def build_project(task_id: str, task, build_dir: Path) -> Path: async for line in process.stdout: decoded = line.decode("utf-8", errors="replace").strip() if decoded: + archive_output.append(decoded) await log_streamer.emit(task_id, decoded) await process.wait() if process.returncode != 0: - raise Exception("Archive 失败") + category, hint = _classify_build_error(archive_output, "archive") + raise BuildError(hint, category=category, detail="\n".join(archive_output[-20:])) # 导出 IPA await log_streamer.emit(task_id, "导出 IPA...") @@ -494,6 +573,7 @@ async def build_project(task_id: str, task, build_dir: Path) -> Path: f"-exportPath {export_path} " f"-exportOptionsPlist {export_plist}" ) + export_output = [] process = await asyncio.create_subprocess_shell( export_cmd, cwd=str(build_dir), @@ -503,11 +583,13 @@ async def build_project(task_id: str, task, build_dir: Path) -> Path: async for line in process.stdout: decoded = line.decode("utf-8", errors="replace").strip() if decoded: + export_output.append(decoded) await log_streamer.emit(task_id, decoded) await process.wait() if process.returncode != 0: - raise Exception("导出 IPA 失败") + category, hint = _classify_build_error(export_output, "export") + raise BuildError(hint, category=category, detail="\n".join(export_output[-20:])) # 查找 IPA 文件 ipa_files = list(export_path.glob("*.ipa")) diff --git a/frontend/src/views/BuildView.vue b/frontend/src/views/BuildView.vue index 056708d..bab2afe 100644 --- a/frontend/src/views/BuildView.vue +++ b/frontend/src/views/BuildView.vue @@ -24,7 +24,7 @@ @@ -51,14 +51,27 @@

实时日志 {{ currentTaskId ? `- ${currentTaskId}` : '' }}

- +
+ + +
-
- [{{ formatTime(log.timestamp) }}] {{ log.message }} -
+
选择任务或创建新任务查看日志
@@ -84,16 +97,23 @@ 状态 {{ statusText(completedTask.status) }}
-
- 错误信息 - {{ completedTask.error_message }} +
+ 失败原因 + {{ errorCategoryHint(completedTask.error_category) }}
-
+
+ 错误详情 +
+ 查看详细错误信息 +
{{ completedTask.error_message }}
+
+
+
下载链接 点击下载 IPA
-
+
下载二维码

扫码下载安装

@@ -119,6 +139,7 @@ diff --git a/frontend/src/views/ConfigView.vue b/frontend/src/views/ConfigView.vue index 52e4066..2e1d29d 100644 --- a/frontend/src/views/ConfigView.vue +++ b/frontend/src/views/ConfigView.vue @@ -88,7 +88,8 @@ ID - 名称 + Scheme 名称 + 显示名称 OSS 目录 操作 @@ -97,6 +98,7 @@ {{ id }} {{ scheme.name }} + {{ scheme.displayName || '-' }} {{ scheme.ossFloder }} @@ -409,6 +411,10 @@
+
+ + +
@@ -636,7 +642,7 @@ const deleteApp = async (id) => { // Scheme 相关 const openSchemeModal = (id = null, scheme = null) => { editingSchemeId.value = id - schemeForm.value = scheme ? { ...scheme } : { name: '', ossFloder: '' } + schemeForm.value = scheme ? { ...scheme } : { name: '', displayName: '', ossFloder: '' } showSchemeModal.value = true } diff --git a/frontend/src/views/HistoryView.vue b/frontend/src/views/HistoryView.vue index 7b86117..3f0e2fc 100644 --- a/frontend/src/views/HistoryView.vue +++ b/frontend/src/views/HistoryView.vue @@ -42,6 +42,9 @@ {{ task.scheme_name }} {{ statusText(task.status) }} + + {{ errorCategoryLabel(task.error_category) }} + @@ -58,6 +61,64 @@
+ + + + +