From 6a204d8dd3fecd38a4d7cee9914ad127c273574f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=9B=E9=A3=8E?= Date: Thu, 19 Mar 2026 20:49:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(portal):=20=E6=96=B0=E5=A2=9E=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E6=98=BE=E9=9A=90=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=8F=8A=E7=9B=B8=E5=85=B3=E9=85=8D=E7=BD=AE=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在门户配置中添加 menuVisibility 字段控制菜单项显示状态 - 新增后台接口 /portal-config/ui 提供门户 UI 配置数据 - 新增 PortalMenuSettings 组件实现门户菜单显隐设置功能 - 在门户详情页添加菜单显隐管理菜单项入口 - 创建 PortalConfigContext 统一管理和提供菜单显隐状态 - 在前端 Header 组件及路由守卫动态控制菜单及路由访问权限 - HiCoding 功能开关支持终端功能启用状态,通过接口动态获取 - 终端功能禁用时拒绝 WebSocket 连接并隐藏终端面板 - Docker 及 Helm 部署脚本新增 JWT_SECRET 支持,增强安全性 - AcpProperties 配置新增 terminalEnabled 配置属性及注释说明 - RemoteWorkspaceService 过滤隐藏以点开头的文件名 - 统一完善配置项默认值和配置逻辑,保证升级及安装流程顺畅 --- deploy/docker/docker-compose.yml | 1 + deploy/docker/install.sh | 10 ++ .../templates/himarket-server-cm.yaml | 1 + deploy/helm/himarket/templates/mysql.yaml | 12 +++ deploy/helm/himarket/values.yaml | 3 + deploy/helm/install.sh | 16 ++- .../src/main/resources/application.yml | 3 +- .../support/portal/PortalUiConfig.java | 3 + .../himarket/config/AcpProperties.java | 14 +++ .../controller/CliProviderController.java | 6 ++ .../controller/PortalConfigController.java | 58 ++++++++++ .../hicoding/RemoteWorkspaceService.java | 3 + .../terminal/TerminalWebSocketHandler.java | 6 ++ .../components/portal/PortalMenuSettings.tsx | 101 ++++++++++++++++++ .../himarket-admin/src/pages/PortalDetail.tsx | 12 ++- .../himarket-admin/src/types/portal.ts | 1 + himarket-web/himarket-frontend/src/App.tsx | 5 +- .../src/components/Header.tsx | 14 +-- .../src/context/PortalConfigContext.tsx | 81 ++++++++++++++ .../src/lib/apis/cliProvider.ts | 17 +++ .../himarket-frontend/src/lib/apis/index.ts | 5 +- .../himarket-frontend/src/lib/apis/portal.ts | 13 +++ .../himarket-frontend/src/pages/Coding.tsx | 21 +++- himarket-web/himarket-frontend/src/router.tsx | 45 +++++++- 24 files changed, 429 insertions(+), 22 deletions(-) create mode 100644 himarket-server/src/main/java/com/alibaba/himarket/controller/PortalConfigController.java create mode 100644 himarket-web/himarket-admin/src/components/portal/PortalMenuSettings.tsx create mode 100644 himarket-web/himarket-frontend/src/context/PortalConfigContext.tsx create mode 100644 himarket-web/himarket-frontend/src/lib/apis/portal.ts diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index c292fff46..1a064b8d8 100644 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -145,6 +145,7 @@ services: - DB_NAME=${DB_NAME:-portal_db} - DB_USERNAME=${DB_USERNAME:-portal_user} - DB_PASSWORD=${DB_PASSWORD:-himarket_app_2024} + - JWT_SECRET=${JWT_SECRET} - ACP_REMOTE_HOST=${ACP_REMOTE_HOST:-sandbox-shared} - ACP_REMOTE_PORT=${ACP_REMOTE_PORT:-8080} - ACP_DEFAULT_RUNTIME=${ACP_DEFAULT_RUNTIME:-remote} diff --git a/deploy/docker/install.sh b/deploy/docker/install.sh index 8e9edbfd3..6278f73f9 100755 --- a/deploy/docker/install.sh +++ b/deploy/docker/install.sh @@ -366,6 +366,7 @@ load_config() { HIMARKET_SERVER_IMAGE HIMARKET_ADMIN_IMAGE HIMARKET_FRONTEND_IMAGE \ MYSQL_IMAGE NACOS_IMAGE HIGRESS_IMAGE REDIS_IMAGE SANDBOX_IMAGE \ MYSQL_ROOT_PASSWORD MYSQL_PASSWORD MYSQL_DATABASE MYSQL_USER \ + JWT_SECRET \ NACOS_ADMIN_PASSWORD HIGRESS_USERNAME HIGRESS_PASSWORD \ ADMIN_USERNAME ADMIN_PASSWORD FRONT_USERNAME FRONT_PASSWORD \ HIMARKET_LANGUAGE \ @@ -593,6 +594,12 @@ interactive_config() { export DB_USERNAME="${MYSQL_USER:-portal_user}" export DB_PASSWORD="${MYSQL_PASSWORD}" + # ─── JWT Secret(自动生成随机值) ─── + if [[ -z "${JWT_SECRET:-}" ]]; then + JWT_SECRET="$(openssl rand -base64 32)" + fi + export JWT_SECRET + # ─── 服务凭证 ─── log "" log "$(msg section.credential)" @@ -771,6 +778,9 @@ HIGRESS_IMAGE="${HIGRESS_IMAGE}" MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD}" MYSQL_PASSWORD="${MYSQL_PASSWORD}" +# ========== JWT Secret ========== +JWT_SECRET="${JWT_SECRET}" + # ========== 服务凭证 ========== NACOS_ADMIN_PASSWORD="${NACOS_ADMIN_PASSWORD}" HIGRESS_USERNAME="${HIGRESS_USERNAME}" diff --git a/deploy/helm/himarket/templates/himarket-server-cm.yaml b/deploy/helm/himarket/templates/himarket-server-cm.yaml index 8111b68a8..5f77f8169 100644 --- a/deploy/helm/himarket/templates/himarket-server-cm.yaml +++ b/deploy/helm/himarket/templates/himarket-server-cm.yaml @@ -11,4 +11,5 @@ data: ACP_REMOTE_HOST: {{ .Values.sandbox.remoteHost | default "sandbox-shared" | quote }} ACP_REMOTE_PORT: {{ .Values.sandbox.remotePort | default "8080" | quote }} ACP_DEFAULT_RUNTIME: {{ .Values.sandbox.defaultRuntime | default "remote" | quote }} + ACP_TERMINAL_ENABLED: {{ .Values.sandbox.terminalEnabled | default false | quote }} # 其他非敏感配置可以在这里添加 \ No newline at end of file diff --git a/deploy/helm/himarket/templates/mysql.yaml b/deploy/helm/himarket/templates/mysql.yaml index 6af30e680..9dda997e8 100644 --- a/deploy/helm/himarket/templates/mysql.yaml +++ b/deploy/helm/himarket/templates/mysql.yaml @@ -1,6 +1,8 @@ {{- $existingSecret := (lookup "v1" "Secret" .Release.Namespace "mysql-secret") }} +{{- $existingServerSecret := (lookup "v1" "Secret" .Release.Namespace "himarket-server-secret") }} {{- $rootPassword := "" }} {{- $userPassword := "" }} +{{- $jwtSecret := "" }} {{- if $existingSecret }} {{- $rootPassword = (index $existingSecret.data "MYSQL_ROOT_PASSWORD" | b64dec) }} {{- $userPassword = (index $existingSecret.data "MYSQL_PASSWORD" | b64dec) }} @@ -16,6 +18,15 @@ {{- $userPassword = randAlphaNum 16 }} {{- end }} {{- end }} +{{- if $existingServerSecret }} + {{- $jwtSecret = (index $existingServerSecret.data "JWT_SECRET" | b64dec) }} +{{- else }} + {{- if .Values.server.jwtSecret }} + {{- $jwtSecret = .Values.server.jwtSecret }} + {{- else }} + {{- $jwtSecret = randAlphaNum 32 }} + {{- end }} +{{- end }} --- # MySQL Secret: 存储敏感的数据库凭据(自动生成随机密码) apiVersion: v1 @@ -46,6 +57,7 @@ stringData: DB_NAME: {{ .Values.mysql.auth.database | quote }} DB_USERNAME: {{ .Values.mysql.auth.username | quote }} DB_PASSWORD: {{ $userPassword | quote }} + JWT_SECRET: {{ $jwtSecret | quote }} --- # MySQL Headless Service: 为 StatefulSet 提供稳定的网络域 diff --git a/deploy/helm/himarket/values.yaml b/deploy/helm/himarket/values.yaml index 3a0218c70..8bd22bc25 100644 --- a/deploy/helm/himarket/values.yaml +++ b/deploy/helm/himarket/values.yaml @@ -32,6 +32,8 @@ server: port: 80 replicaCount: 1 serverPort: 8080 + # JWT Secret(留空则自动生成随机值) + jwtSecret: "" # MySQL 数据库配置(始终部署内置 MySQL) mysql: @@ -86,6 +88,7 @@ resources: # 共享沙箱配置 sandbox: enabled: true + terminalEnabled: false image: repository: sandbox tag: "latest" diff --git a/deploy/helm/install.sh b/deploy/helm/install.sh index 22c5d905f..50251b3d0 100755 --- a/deploy/helm/install.sh +++ b/deploy/helm/install.sh @@ -522,6 +522,7 @@ load_config() { NACOS_VERSION NACOS_IMAGE_REGISTRY NACOS_IMAGE_REPOSITORY \ HIGRESS_REPO_NAME HIGRESS_REPO_URL HIGRESS_CHART_REF \ MYSQL_ROOT_PASSWORD MYSQL_PASSWORD \ + JWT_SECRET \ NACOS_ADMIN_PASSWORD HIGRESS_USERNAME HIGRESS_PASSWORD \ ADMIN_USERNAME ADMIN_PASSWORD FRONT_USERNAME FRONT_PASSWORD \ MYSQL_STORAGE_CLASS MYSQL_STORAGE_SIZE SANDBOX_STORAGE_CLASS SANDBOX_STORAGE_SIZE \ @@ -753,6 +754,10 @@ interactive_config() { NACOS_IMAGE_REPOSITORY="${NACOS_IMAGE_REPOSITORY:-nacos/nacos-server}" MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-himarket_root_2024}" MYSQL_PASSWORD="${MYSQL_PASSWORD:-himarket_app_2024}" + # JWT Secret: 升级时沿用已有值,全新安装时自动生成 + if [[ -z "${JWT_SECRET:-}" ]]; then + JWT_SECRET="$(openssl rand -base64 32)" + fi NACOS_ADMIN_PASSWORD="${NACOS_ADMIN_PASSWORD:-nacos}" HIGRESS_USERNAME="${HIGRESS_USERNAME:-admin}" HIGRESS_PASSWORD="${HIGRESS_PASSWORD:-admin}" @@ -808,6 +813,11 @@ interactive_config() { prompt MYSQL_ROOT_PASSWORD "MySQL root password" "himarket_root_2024" prompt MYSQL_PASSWORD "MySQL app password" "himarket_app_2024" + # JWT Secret: 自动生成随机值(无需用户交互) + if [[ -z "${JWT_SECRET:-}" ]]; then + JWT_SECRET="$(openssl rand -base64 32)" + fi + log "" log "$(msg section.credential)" prompt NACOS_ADMIN_PASSWORD "Nacos admin password" "nacos" @@ -1008,6 +1018,9 @@ HIGRESS_CHART_REF="${HIGRESS_CHART_REF}" MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD}" MYSQL_PASSWORD="${MYSQL_PASSWORD}" +# ========== JWT Secret ========== +JWT_SECRET="${JWT_SECRET}" + # ========== 服务凭证 ========== NACOS_ADMIN_PASSWORD="${NACOS_ADMIN_PASSWORD}" HIGRESS_USERNAME="${HIGRESS_USERNAME}" @@ -1129,7 +1142,8 @@ deploy_all() { --set "mysql.persistence.storageClass=${MYSQL_STORAGE_CLASS}" \ --set "mysql.persistence.size=${MYSQL_STORAGE_SIZE}" \ --set "sandbox.persistence.storageClass=${SANDBOX_STORAGE_CLASS}" \ - --set "sandbox.persistence.size=${SANDBOX_STORAGE_SIZE}" + --set "sandbox.persistence.size=${SANDBOX_STORAGE_SIZE}" \ + --set "server.jwtSecret=${JWT_SECRET}" # 7. 等待 MySQL Pod 就绪 + 初始化 Nacos 数据库 init_nacos_db_in_cluster "${NS}" "${MYSQL_ROOT_PASSWORD}" "${NACOS_DB_NAME}" diff --git a/himarket-bootstrap/src/main/resources/application.yml b/himarket-bootstrap/src/main/resources/application.yml index bd9d0a4d7..f57e3725d 100644 --- a/himarket-bootstrap/src/main/resources/application.yml +++ b/himarket-bootstrap/src/main/resources/application.yml @@ -37,10 +37,11 @@ springdoc: packages-to-scan: com.alibaba.himarket.controller jwt: - secret: YourJWTSecret + secret: ${JWT_SECRET:YourJWTSecret} expiration: 7d acp: + terminal-enabled: ${ACP_TERMINAL_ENABLED:false} default-provider: ${ACP_DEFAULT_PROVIDER:qwen-code} default-runtime: ${ACP_DEFAULT_RUNTIME:remote} remote: diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/support/portal/PortalUiConfig.java b/himarket-dal/src/main/java/com/alibaba/himarket/support/portal/PortalUiConfig.java index 9088c9e65..f626addad 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/support/portal/PortalUiConfig.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/support/portal/PortalUiConfig.java @@ -19,6 +19,7 @@ package com.alibaba.himarket.support.portal; +import java.util.Map; import lombok.Data; @Data @@ -27,4 +28,6 @@ public class PortalUiConfig { private String logo; private String icon; + + private Map menuVisibility; } diff --git a/himarket-server/src/main/java/com/alibaba/himarket/config/AcpProperties.java b/himarket-server/src/main/java/com/alibaba/himarket/config/AcpProperties.java index c972ec281..f5b54b4bb 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/config/AcpProperties.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/config/AcpProperties.java @@ -9,6 +9,12 @@ @ConfigurationProperties(prefix = "acp") public class AcpProperties { + /** + * 是否启用终端功能。 + * 设为 false 时后端拒绝 Terminal WebSocket 连接,前端隐藏终端面板。 + */ + private boolean terminalEnabled = true; + /** * 默认使用的 CLI provider key(对应 providers map 中的 key) */ @@ -32,6 +38,14 @@ public class AcpProperties { */ private RemoteConfig remote = new RemoteConfig(); + public boolean isTerminalEnabled() { + return terminalEnabled; + } + + public void setTerminalEnabled(boolean terminalEnabled) { + this.terminalEnabled = terminalEnabled; + } + public String getDefaultProvider() { return defaultProvider; } diff --git a/himarket-server/src/main/java/com/alibaba/himarket/controller/CliProviderController.java b/himarket-server/src/main/java/com/alibaba/himarket/controller/CliProviderController.java index 850afa14e..e1d2883b8 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/controller/CliProviderController.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/controller/CliProviderController.java @@ -338,6 +338,12 @@ private MarketModelInfo buildMarketModelInfo(ProductResult product) { .build(); } + @Operation(summary = "获取 HiCoding 功能开关状态") + @GetMapping("/features") + public Map getFeatures() { + return Map.of("terminalEnabled", acpProperties.isTerminalEnabled()); + } + @Operation(summary = "获取可用的 CLI Provider 列表(含运行时兼容性信息)") @GetMapping public List listProviders() { diff --git a/himarket-server/src/main/java/com/alibaba/himarket/controller/PortalConfigController.java b/himarket-server/src/main/java/com/alibaba/himarket/controller/PortalConfigController.java new file mode 100644 index 000000000..261cd1d8b --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/controller/PortalConfigController.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.alibaba.himarket.controller; + +import com.alibaba.himarket.core.annotation.PublicAccess; +import com.alibaba.himarket.service.PortalService; +import com.alibaba.himarket.support.portal.PortalUiConfig; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/portal-config") +@Tag(name = "门户配置(公开)") +@PublicAccess +@RequiredArgsConstructor +public class PortalConfigController { + + private final PortalService portalService; + + @Operation(summary = "获取门户 UI 配置") + @GetMapping("/ui") + public PortalUiConfig getUiConfig() { + String portalId = portalService.getDefaultPortal(); + if (portalId == null) { + return new PortalUiConfig(); + } + try { + var portalResult = portalService.getPortal(portalId); + if (portalResult.getPortalUiConfig() == null) { + return new PortalUiConfig(); + } + return portalResult.getPortalUiConfig(); + } catch (Exception e) { + return new PortalUiConfig(); + } + } +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hicoding/RemoteWorkspaceService.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hicoding/RemoteWorkspaceService.java index 2a878757d..34b3a9a8c 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/hicoding/RemoteWorkspaceService.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hicoding/RemoteWorkspaceService.java @@ -390,6 +390,9 @@ private List> convertChildren( for (Map item : sidecarItems) { String name = (String) item.get("name"); + if (name != null && name.startsWith(".")) { + continue; + } String type = (String) item.get("type"); String childPath = parentPath.endsWith("/") ? parentPath + name : parentPath + "/" + name; diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hicoding/terminal/TerminalWebSocketHandler.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hicoding/terminal/TerminalWebSocketHandler.java index 9b10d75e5..62ccf8a7f 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/hicoding/terminal/TerminalWebSocketHandler.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hicoding/terminal/TerminalWebSocketHandler.java @@ -43,6 +43,12 @@ public TerminalWebSocketHandler( @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { + if (!acpProperties.isTerminalEnabled()) { + logger.info("Terminal feature is disabled, closing connection: id={}", session.getId()); + session.close(CloseStatus.NORMAL); + return; + } + String userId = (String) session.getAttributes().get("userId"); if (userId == null) { logger.error("No userId in session attributes, closing terminal connection"); diff --git a/himarket-web/himarket-admin/src/components/portal/PortalMenuSettings.tsx b/himarket-web/himarket-admin/src/components/portal/PortalMenuSettings.tsx new file mode 100644 index 000000000..d0560217a --- /dev/null +++ b/himarket-web/himarket-admin/src/components/portal/PortalMenuSettings.tsx @@ -0,0 +1,101 @@ +import {Card, Form, Switch, message} from 'antd' +import {Portal} from '@/types' +import {portalApi} from '@/lib/api' + +interface PortalMenuSettingsProps { + portal: Portal + onRefresh?: () => void +} + +const MENU_ITEMS = [ + {key: "chat", label: "HiChat"}, + {key: "coding", label: "HiCoding"}, + {key: "agents", label: "智能体"}, + {key: "mcp", label: "MCP"}, + {key: "models", label: "模型"}, + {key: "apis", label: "API"}, + {key: "skills", label: "Skills"}, +] + +export function PortalMenuSettings({portal, onRefresh}: PortalMenuSettingsProps) { + const [form] = Form.useForm() + + const getMenuVisibility = (key: string): boolean => { + return portal.portalUiConfig?.menuVisibility?.[key] ?? true + } + + const handleToggle = async (key: string, checked: boolean) => { + const currentVisibility = {...(portal.portalUiConfig?.menuVisibility || {})} + const newVisibility = {...currentVisibility, [key]: checked} + + // 至少保留一个菜单项可见 + const visibleCount = MENU_ITEMS.filter( + item => newVisibility[item.key] ?? true + ).length + if (visibleCount === 0) { + message.warning('至少保留一个菜单项为可见状态') + // 恢复 form 中的值 + form.setFieldValue(key, true) + return + } + + try { + await portalApi.updatePortal(portal.portalId, { + name: portal.name, + description: portal.description, + portalSettingConfig: portal.portalSettingConfig, + portalDomainConfig: portal.portalDomainConfig, + portalUiConfig: { + ...portal.portalUiConfig, + menuVisibility: newVisibility, + }, + }) + message.success('菜单配置保存成功') + onRefresh?.() + } catch { + message.error('保存菜单配置失败') + // 恢复 form 中的值 + form.setFieldValue(key, !checked) + } + } + + const initialValues = MENU_ITEMS.reduce((acc, item) => { + acc[item.key] = getMenuVisibility(item.key) + return acc + }, {} as Record) + + return ( +
+
+

菜单管理

+

控制开发者门户导航栏的菜单项显隐

+
+ +
+ +
+

导航菜单项

+
+ {MENU_ITEMS.map(item => ( + + handleToggle(item.key, checked)} + /> + + ))} +
+
+
+
+
+ ) +} diff --git a/himarket-web/himarket-admin/src/pages/PortalDetail.tsx b/himarket-web/himarket-admin/src/pages/PortalDetail.tsx index 0a096add7..54c40815e 100644 --- a/himarket-web/himarket-admin/src/pages/PortalDetail.tsx +++ b/himarket-web/himarket-admin/src/pages/PortalDetail.tsx @@ -8,7 +8,8 @@ import { ApiOutlined, TeamOutlined, SafetyOutlined, - CloudOutlined + CloudOutlined, + AppstoreOutlined } from '@ant-design/icons' import { PortalOverview } from '@/components/portal/PortalOverview' import { PortalPublishedApis } from '@/components/portal/PortalPublishedApis' @@ -17,6 +18,7 @@ import { PortalConsumers } from '@/components/portal/PortalConsumers' import { PortalDashboard } from '@/components/portal/PortalDashboard' import { PortalSecurity } from '@/components/portal/PortalSecurity' import { PortalDomain } from '@/components/portal/PortalDomain' +import { PortalMenuSettings } from '@/components/portal/PortalMenuSettings' import PortalFormModal from '@/components/portal/PortalFormModal' import { portalApi } from '@/lib/api' import { Portal } from '@/types' @@ -52,6 +54,12 @@ const menuItems = [ icon: CloudOutlined, description: "域名管理" }, + { + key: "menu", + label: "Menu", + icon: AppstoreOutlined, + description: "菜单显隐管理" + }, // { // key: "consumers", // label: "Consumers", @@ -137,6 +145,8 @@ export default function PortalDetail() { return case "domain": return + case "menu": + return case "consumers": return case "dashboard": diff --git a/himarket-web/himarket-admin/src/types/portal.ts b/himarket-web/himarket-admin/src/types/portal.ts index 2a9fcadc3..b1152d7b0 100644 --- a/himarket-web/himarket-admin/src/types/portal.ts +++ b/himarket-web/himarket-admin/src/types/portal.ts @@ -86,6 +86,7 @@ export interface PortalSettingConfig { export interface PortalUiConfig { logo: string | null; icon: string | null; + menuVisibility?: Record | null; } export interface PortalDomainConfig { diff --git a/himarket-web/himarket-frontend/src/App.tsx b/himarket-web/himarket-frontend/src/App.tsx index 0e99be57c..aeb1635bf 100644 --- a/himarket-web/himarket-frontend/src/App.tsx +++ b/himarket-web/himarket-frontend/src/App.tsx @@ -5,6 +5,7 @@ import zhCN from 'antd/locale/zh_CN'; import './App.css' import "./styles/table.css"; import aliyunThemeToken from './aliyunThemeToken.ts'; +import { PortalConfigProvider } from './context/PortalConfigContext'; function App() { return ( @@ -15,7 +16,9 @@ function App() { }} > - + + + ); diff --git a/himarket-web/himarket-frontend/src/components/Header.tsx b/himarket-web/himarket-frontend/src/components/Header.tsx index 1a1c43af8..1a90d20a7 100644 --- a/himarket-web/himarket-frontend/src/components/Header.tsx +++ b/himarket-web/himarket-frontend/src/components/Header.tsx @@ -2,10 +2,12 @@ import { Link, useLocation } from "react-router-dom"; import { useState, useEffect } from "react"; import { UserInfo } from "./UserInfo"; import { HiMarket, Logo } from "./icon"; +import { usePortalConfig } from "../context/PortalConfigContext"; export function Header() { const location = useLocation(); const [isScrolled, setIsScrolled] = useState(false); + const { visibleTabs } = usePortalConfig(); useEffect(() => { const handleScroll = () => { @@ -16,16 +18,6 @@ export function Header() { return () => window.removeEventListener("scroll", handleScroll); }, []); - const tabs = [ - { path: "/chat", label: "HiChat" }, - { path: "/coding", label: "HiCoding" }, - { path: "/agents", label: "智能体" }, - { path: "/mcp", label: "MCP" }, - { path: "/models", label: "模型" }, - { path: "/apis", label: "API" }, - { path: "/skills", label: "Skills" }, - ]; - const isActiveTab = (path: string) => { return ( location.pathname === path || location.pathname.startsWith(path + "/") @@ -59,7 +51,7 @@ export function Header() {
{/* Tab 区域 */}
- {tabs.map(tab => ( + {visibleTabs.map(tab => (
boolean; + visibleTabs: TabItem[]; + firstVisiblePath: string; + loading: boolean; +} + +const PortalConfigContext = createContext({ + isMenuVisible: () => true, + visibleTabs: ALL_TABS, + firstVisiblePath: "/models", + loading: true, +}); + +export function usePortalConfig() { + return useContext(PortalConfigContext); +} + +export function PortalConfigProvider({ children }: { children: ReactNode }) { + const [menuVisibility, setMenuVisibility] = useState | null>(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + getPortalUiConfig() + .then((res) => { + console.log("[PortalConfig] API response:", JSON.stringify(res)); + const mv = res.data?.menuVisibility ?? null; + console.log("[PortalConfig] menuVisibility:", JSON.stringify(mv)); + setMenuVisibility(mv); + }) + .catch((err) => { + // 接口失败时静默降级,全部菜单显示 + console.warn("[PortalConfig] API failed:", err); + setMenuVisibility(null); + }) + .finally(() => { + setLoading(false); + }); + }, []); + + const isMenuVisible = (key: string): boolean => { + if (menuVisibility == null) return true; + return menuVisibility[key] ?? true; + }; + + const visibleTabs = useMemo(() => { + const result = ALL_TABS.filter((tab) => isMenuVisible(tab.key)); + console.log("[PortalConfig] visibleTabs:", result.map((t) => t.key)); + return result; + }, [menuVisibility]); + + const firstVisiblePath = useMemo( + () => (visibleTabs.length > 0 ? visibleTabs[0].path : "/models"), + [visibleTabs] + ); + + return ( + + {children} + + ); +} diff --git a/himarket-web/himarket-frontend/src/lib/apis/cliProvider.ts b/himarket-web/himarket-frontend/src/lib/apis/cliProvider.ts index 8cda3711c..388eec2eb 100644 --- a/himarket-web/himarket-frontend/src/lib/apis/cliProvider.ts +++ b/himarket-web/himarket-frontend/src/lib/apis/cliProvider.ts @@ -32,6 +32,23 @@ export function getCliProviders() { ); } +// ============ 功能开关类型定义 ============ + +export interface CodingFeatures { + terminalEnabled: boolean; +} + +// ============ 功能开关 API 函数 ============ + +/** + * 获取 HiCoding 功能开关状态 + */ +export function getCodingFeatures() { + return request.get, RespI>( + "/cli-providers/features" + ); +} + // ============ 模型市场类型定义 ============ export interface MarketModelInfo { diff --git a/himarket-web/himarket-frontend/src/lib/apis/index.ts b/himarket-web/himarket-frontend/src/lib/apis/index.ts index e2d9525bb..c7dde0312 100644 --- a/himarket-web/himarket-frontend/src/lib/apis/index.ts +++ b/himarket-web/himarket-frontend/src/lib/apis/index.ts @@ -5,6 +5,7 @@ import * as category from "./category"; import * as chat from "./chat"; import * as cliProvider from "./cliProvider"; import * as codingSession from "./codingSession"; +import * as portal from "./portal"; const APIs = { @@ -15,6 +16,7 @@ const APIs = { ...chat, ...cliProvider, ...codingSession, + ...portal, } export default APIs; @@ -25,4 +27,5 @@ export * from "./developer"; export * from "./category"; export * from "./chat"; export * from "./cliProvider"; -export * from "./codingSession"; \ No newline at end of file +export * from "./codingSession"; +export * from "./portal"; \ No newline at end of file diff --git a/himarket-web/himarket-frontend/src/lib/apis/portal.ts b/himarket-web/himarket-frontend/src/lib/apis/portal.ts new file mode 100644 index 000000000..f5680f223 --- /dev/null +++ b/himarket-web/himarket-frontend/src/lib/apis/portal.ts @@ -0,0 +1,13 @@ +import request, { type RespI } from "../request"; + +export interface PortalUiConfig { + logo: string | null; + icon: string | null; + menuVisibility: Record | null; +} + +export function getPortalUiConfig() { + return request.get, RespI>( + '/portal-config/ui' + ); +} diff --git a/himarket-web/himarket-frontend/src/pages/Coding.tsx b/himarket-web/himarket-frontend/src/pages/Coding.tsx index ac47a3da6..73cc55dee 100644 --- a/himarket-web/himarket-frontend/src/pages/Coding.tsx +++ b/himarket-web/himarket-frontend/src/pages/Coding.tsx @@ -38,7 +38,7 @@ import type { FileNode } from "../types/coding"; import type { ChatItemPlan } from "../types/coding-protocol"; import { buildCodingWsUrl } from "../lib/utils/wsUrl"; -import { getMarketModels, getCliProviders } from "../lib/apis/cliProvider"; +import { getMarketModels, getCliProviders, getCodingFeatures } from "../lib/apis/cliProvider"; import { sortCliProviders } from "../lib/utils/cliProviderSort"; import { createCodingSession, @@ -106,6 +106,19 @@ type RightTab = "preview" | "code"; const READ_ONLY_KINDS = new Set(["read", "search", "think", "fetch", "switch_mode"]); function CodingContent() { + // ===== 功能开关 ===== + const [terminalEnabled, setTerminalEnabled] = useState(true); + useEffect(() => { + getCodingFeatures() + .then((res) => { + const data = res.data ?? (res as any); + if (typeof data.terminalEnabled === "boolean") { + setTerminalEnabled(data.terminalEnabled); + } + }) + .catch(() => { /* 获取失败时保持默认值 true */ }); + }, []); + // ===== 配置(纯内存,不持久化) ===== const { config, setConfig, isComplete } = useCodingConfig(); const [sessionRefreshTrigger, setSessionRefreshTrigger] = useState(0); @@ -429,9 +442,9 @@ function CodingContent() { }); }, []); - // Derived visibility: hide file tree and terminal in preview mode + // Derived visibility: hide file tree and terminal in preview mode; hide terminal when disabled const showFileTree = fileTreeVisible && !isPreviewMode; - const showTerminal = !isPreviewMode; + const showTerminal = !isPreviewMode && terminalEnabled; // ===== Resizable panels ===== const conversationPanel = useResizable({ @@ -824,6 +837,7 @@ function CodingContent() { {!terminalCollapsed && showTerminal && ( )} + {terminalEnabled && (
+ )}
diff --git a/himarket-web/himarket-frontend/src/router.tsx b/himarket-web/himarket-frontend/src/router.tsx index 4837f9d99..c62e3646c 100644 --- a/himarket-web/himarket-frontend/src/router.tsx +++ b/himarket-web/himarket-frontend/src/router.tsx @@ -1,4 +1,5 @@ -import { Routes, Route, Navigate } from "react-router-dom"; +import { Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom"; +import { useEffect } from "react"; import ApiDetail from "./pages/ApiDetail"; import Consumers from "./pages/Consumers"; import ConsumerDetail from "./pages/ConsumerDetail"; @@ -17,11 +18,48 @@ import Chat from "./pages/Chat"; import Coding from "./pages/Coding"; import SkillDetail from "./pages/SkillDetail"; import { RequireAuth } from "./components/RequireAuth"; +import { usePortalConfig } from "./context/PortalConfigContext"; + +function DynamicHome() { + const { firstVisiblePath } = usePortalConfig(); + return ; +} + +function MenuRedirectGuard() { + const location = useLocation(); + const navigate = useNavigate(); + const { isMenuVisible, firstVisiblePath, loading } = usePortalConfig(); + + useEffect(() => { + if (loading) return; + + const pathToKeyMap: Record = { + "/chat": "chat", + "/coding": "coding", + "/agents": "agents", + "/mcp": "mcp", + "/models": "models", + "/apis": "apis", + "/skills": "skills", + }; + + const currentPath = location.pathname; + // 仅拦截顶级菜单路径,不拦截子路径(如 /models/xxx) + const menuKey = pathToKeyMap[currentPath]; + if (menuKey && !isMenuVisible(menuKey)) { + navigate(firstVisiblePath, { replace: true }); + } + }, [location.pathname, isMenuVisible, firstVisiblePath, loading, navigate]); + + return null; +} export function Router() { return ( - - } /> + <> + + + } /> } /> } /> } /> @@ -47,5 +85,6 @@ export function Router() { {/* 其他页面可继续添加 */} + ); }