From 8575beede3e925585bff0fc4e881b3b57b058fcd Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Thu, 21 Aug 2025 16:58:33 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(deploy-agent):=20=E6=B7=BB=E5=8A=A0=20?= =?UTF-8?q?SSE=20Endpoint=20=E9=AA=8C=E8=AF=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/deployment/agent.py | 51 +++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/app/deployment/agent.py b/src/app/deployment/agent.py index 3a0e8e3..9415838 100644 --- a/src/app/deployment/agent.py +++ b/src/app/deployment/agent.py @@ -529,6 +529,16 @@ class AgentManager: callback: Callable[[DeploymentState], None] | None, ) -> str | None: """处理单个 MCP 服务""" + # 如果是 SSE 类型,先验证 URL可用且为SSE + if config.mcp_type == "sse": + valid = await self._validate_sse_endpoint(config, state, callback) + if not valid: + self._report_progress( + state, + f" ❌ MCP 服务 {config.name} SSE Endpoint 验证失败", + callback, + ) + return None try: # 注册服务 service_id = await self._register_mcp_service(config, state, callback) @@ -547,3 +557,44 @@ class AgentManager: else: return service_id + + async def _validate_sse_endpoint( + self, + config: McpConfig, + state: DeploymentState, + callback: Callable[[DeploymentState], None] | None, + ) -> bool: + """验证 SSE Endpoint 是否可用""" + url = config.config.get("url") or "" + self._report_progress( + state, + f"🔍 验证 SSE Endpoint: {config.name} -> {url}", + callback, + ) + try: + async with httpx.AsyncClient(timeout=self.api_client.timeout) as client: + response = await client.get( + url, + headers={"Accept": "text/event-stream"}, + ) + if response.status_code != HTTP_OK: + self._report_progress( + state, + f" ❌ {config.name} URL 响应码非 200: {response.status_code}", + callback, + ) + return False + content_type = response.headers.get("content-type", "") + if "text/event-stream" not in content_type: + self._report_progress( + state, + f" ❌ {config.name} Content-Type 非 SSE: {content_type}", + callback, + ) + return False + self._report_progress(state, f" ✅ {config.name} SSE Endpoint 验证通过", callback) + return True + except Exception as e: + self._report_progress(state, f" ❌ {config.name} SSE 验证失败: {e}", callback) + logger.exception("验证 SSE Endpoint 失败: %s", url) + return False -- Gitee From 65b6e84aa978050b913e57dbbd987f66509b08c5 Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Fri, 22 Aug 2025 14:28:48 +0800 Subject: [PATCH 2/3] =?UTF-8?q?chore(deploy-ui):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E6=A8=A1=E5=BC=8F=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/css/styles.tcss | 29 +++++--------------------- src/app/deployment/components/modes.py | 28 ++++++++++++++++++++++--- src/app/deployment/models.py | 21 +++++++++---------- src/app/deployment/ui.py | 14 +++++++++++-- 4 files changed, 52 insertions(+), 40 deletions(-) diff --git a/src/app/css/styles.tcss b/src/app/css/styles.tcss index a9029c2..10ab2ea 100644 --- a/src/app/css/styles.tcss +++ b/src/app/css/styles.tcss @@ -367,7 +367,7 @@ Static { background: rgba(76, 175, 80, 0.2); } -/* 初始化模式选择界面样式 */ +/* 初始化 - 模式选择界面 */ .mode-container { background: $surface; border: solid $primary; @@ -421,7 +421,7 @@ Static { outline: none; } -/* 连接现有服务界面样式 */ +/* 初始化 - 连接现有服务界面 */ .connect-container { background: $surface; border: solid $primary; @@ -448,33 +448,14 @@ Static { padding: 1; } -.form-row { - height: 3; - margin: 1 0; - align: left middle; -} - -.form-label { - color: #4963b1; - text-style: bold; - width: 20; - content-align: left middle; - padding-right: 1; - padding-top: 1; -} - -.form-input { - width: 1fr; - margin-left: 1; -} - -.button-row { +/* 初始化 - 选择部署模式 - 底部按钮 */ +.mode-button-row { height: 3; align: center middle; dock: bottom; } -.button-row > Button { +.mode-button-row > Button { margin: 0 1; width: auto; min-height: 3; diff --git a/src/app/deployment/components/modes.py b/src/app/deployment/components/modes.py index 860336f..f3f6135 100644 --- a/src/app/deployment/components/modes.py +++ b/src/app/deployment/components/modes.py @@ -84,7 +84,7 @@ class InitializationModeScreen(ModalScreen[bool]): variant="default", ) - with Horizontal(classes="button-row"): + with Horizontal(classes="mode-button-row"): yield Button("退出", id="exit", variant="error", classes="exit-button") def on_mount(self) -> None: @@ -121,6 +121,28 @@ class ConnectExistingServiceScreen(ModalScreen[bool]): 允许用户输入现有 openEuler Intelligence 服务的连接信息。 """ + CSS = """ + .form-row { + height: 3; + margin: 1 0; + align: left middle; + } + + .form-label { + color: #4963b1; + text-style: bold; + width: 20; + content-align: left middle; + padding-right: 1; + padding-top: 1; + } + + .form-input { + width: 1fr; + margin-left: 1; + } + """ + BINDINGS: ClassVar = [ Binding("escape", "back", "返回"), Binding("ctrl+q", "app.quit", "退出"), @@ -144,7 +166,7 @@ class ConnectExistingServiceScreen(ModalScreen[bool]): with Horizontal(classes="form-row"): yield Label("服务 URL:", classes="form-label") yield Input( - placeholder="例如:http://your-server.com:8002", + placeholder="例如:http://your-server:8002", id="service_url", classes="form-input", ) @@ -169,7 +191,7 @@ class ConnectExistingServiceScreen(ModalScreen[bool]): classes="help-text", ) - with Horizontal(classes="button-row"): + with Horizontal(classes="mode-button-row"): yield Button("连接并保存", id="connect", variant="success", disabled=True) yield Button("返回", id="back", variant="primary") yield Button("退出", id="exit", variant="error") diff --git a/src/app/deployment/models.py b/src/app/deployment/models.py index 82c6271..33f34a7 100644 --- a/src/app/deployment/models.py +++ b/src/app/deployment/models.py @@ -168,11 +168,13 @@ class DeploymentConfig: errors = [] # 检查是否有任何 Embedding 字段已填写 - has_embedding_config = any([ - self.embedding.endpoint.strip(), - self.embedding.api_key.strip(), - self.embedding.model.strip(), - ]) + has_embedding_config = any( + [ + self.embedding.endpoint.strip(), + self.embedding.api_key.strip(), + self.embedding.model.strip(), + ], + ) # 轻量部署模式下,Embedding 配置是可选的 if self.deployment_mode == "light": @@ -180,18 +182,15 @@ class DeploymentConfig: if has_embedding_config: if not self.embedding.endpoint.strip(): errors.append( - "Embedding API 端点不能为空" - "(轻量部署模式下,如果填写 Embedding 配置,所有字段都必须完整)", + "Embedding API 端点不能为空(轻量部署模式下,如果填写 Embedding 配置,所有字段都必须完整)", ) if not self.embedding.api_key.strip(): errors.append( - "Embedding API 密钥不能为空" - "(轻量部署模式下,如果填写 Embedding 配置,所有字段都必须完整)", + "Embedding API 密钥不能为空(轻量部署模式下,如果填写 Embedding 配置,所有字段都必须完整)", ) if not self.embedding.model.strip(): errors.append( - "Embedding 模型名称不能为空" - "(轻量部署模式下,如果填写 Embedding 配置,所有字段都必须完整)", + "Embedding 模型名称不能为空(轻量部署模式下,如果填写 Embedding 配置,所有字段都必须完整)", ) # 如果没有填写,则跳过验证 else: diff --git a/src/app/deployment/ui.py b/src/app/deployment/ui.py index da302ab..6622197 100644 --- a/src/app/deployment/ui.py +++ b/src/app/deployment/ui.py @@ -614,7 +614,17 @@ class DeploymentProgressScreen(ModalScreen[bool]): .progress-section { margin: 1 0; - height: 6; + height: auto; + min-height: 6; + } + + #progress_bar { + width: 100%; + } + + #step_label { + min-height: 1; + height: auto; } .log-section { @@ -653,7 +663,7 @@ class DeploymentProgressScreen(ModalScreen[bool]): with Vertical(classes="progress-section"): yield Static("部署进度:", id="progress_label") yield ProgressBar(total=100, show_eta=False, id="progress_bar") - yield Static("", id="step_label") + yield Static("准备开始部署...", id="step_label") with Container(classes="log-section"): yield RichLog(id="deployment_log", highlight=True, markup=True) -- Gitee From 62c20a9f9f607e5080629e8e5b31eaa8401e8f95 Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Fri, 22 Aug 2025 16:13:14 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(deploy-ui):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E6=9D=A1=E7=8A=B6=E6=80=81=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=E5=80=92=E9=80=80=E5=B9=B6=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E6=AD=A3=E7=A1=AE=E6=98=BE=E7=A4=BA=E8=BF=9B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/deployment/ui.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/app/deployment/ui.py b/src/app/deployment/ui.py index 6622197..05ee8dc 100644 --- a/src/app/deployment/ui.py +++ b/src/app/deployment/ui.py @@ -31,6 +31,8 @@ if TYPE_CHECKING: from .models import DeploymentConfig, DeploymentState, EmbeddingConfig, LLMConfig from .service import DeploymentService +FULL_PROGRESS = 100 + class DeploymentConfigScreen(ModalScreen[bool]): """ @@ -604,9 +606,9 @@ class DeploymentProgressScreen(ModalScreen[bool]): } .progress-container { - width: 90%; - max-width: 120; - height: 90%; + width: 95%; + max-width: 130; + height: 95%; background: $surface; border: solid $primary; padding: 1; @@ -619,7 +621,7 @@ class DeploymentProgressScreen(ModalScreen[bool]): } #progress_bar { - width: 100%; + margin: 0 1; } #step_label { @@ -654,6 +656,7 @@ class DeploymentProgressScreen(ModalScreen[bool]): self.deployment_task: asyncio.Task[None] | None = None self.deployment_success = False self.deployment_errors: list[str] = [] + self.deployment_progress_value = 0 def compose(self) -> ComposeResult: """组合界面组件""" @@ -662,7 +665,7 @@ class DeploymentProgressScreen(ModalScreen[bool]): with Vertical(classes="progress-section"): yield Static("部署进度:", id="progress_label") - yield ProgressBar(total=100, show_eta=False, id="progress_bar") + yield ProgressBar(total=FULL_PROGRESS, show_eta=False, id="progress_bar") yield Static("准备开始部署...", id="step_label") with Container(classes="log-section"): @@ -694,9 +697,7 @@ class DeploymentProgressScreen(ModalScreen[bool]): @on(Button.Pressed, "#reconfigure") async def on_reconfigure_button_pressed(self) -> None: """处理重新配置按钮点击""" - # 返回配置屏幕 - await self.app.push_screen(DeploymentConfigScreen()) - # 关闭当前屏幕 + # 关闭当前屏幕,返回配置屏幕 self.dismiss(result=False) @on(Button.Pressed, "#cancel") @@ -732,13 +733,14 @@ class DeploymentProgressScreen(ModalScreen[bool]): log_widget = self.query_one("#deployment_log", RichLog) log_widget.clear() - # 重置进度 - self.query_one("#progress_bar", ProgressBar).update(progress=0) - self.query_one("#step_label", Static).update("") - # 重置状态 self.deployment_success = False self.deployment_errors.clear() + self.deployment_progress_value = 0 # 重置进度记录 + + # 重置进度 + self.query_one("#progress_bar", ProgressBar).update(progress=self.deployment_progress_value) + self.query_one("#step_label", Static).update("") # 重置按钮状态 self.query_one("#finish", Button).disabled = True @@ -815,6 +817,8 @@ class DeploymentProgressScreen(ModalScreen[bool]): # 更新界面状态 if success: self.deployment_success = True + self.query_one("#progress_bar", ProgressBar).update(progress=FULL_PROGRESS) + self.query_one("#step_label", Static).update("部署完成!") self.query_one("#deployment_log", RichLog).write( "[bold green]部署成功完成![/bold green]", @@ -841,8 +845,14 @@ class DeploymentProgressScreen(ModalScreen[bool]): def _on_progress_update(self, state: DeploymentState) -> None: """处理进度更新""" # 更新进度条 - progress = (state.current_step / state.total_steps * 100) if state.total_steps > 0 else 0 - self.query_one("#progress_bar", ProgressBar).update(progress=progress) + completed_steps = max(0, state.current_step - 1) # 前面的步骤已完成 + progress = (completed_steps / state.total_steps * FULL_PROGRESS) if state.total_steps > 0 else 0 + + # 记录最后的真实进度值,但避免倒退(除非是重置操作) + # 只有在进度实际前进或者是初始状态时才更新 + if progress >= self.deployment_progress_value or self.deployment_progress_value == 0: + self.deployment_progress_value = progress + self.query_one("#progress_bar", ProgressBar).update(progress=self.deployment_progress_value) # 更新步骤标签 step_text = f"步骤 {state.current_step}/{state.total_steps}: {state.current_step_name}" -- Gitee