diff --git a/backend/apps/asset/services/search_service.py b/backend/apps/asset/services/search_service.py
index 34dabffd..cb012678 100644
--- a/backend/apps/asset/services/search_service.py
+++ b/backend/apps/asset/services/search_service.py
@@ -323,7 +323,8 @@ class AssetSearchService:
def search(
self,
query: str,
- asset_type: AssetType = 'website'
+ asset_type: AssetType = 'website',
+ limit: Optional[int] = None
) -> List[Dict[str, Any]]:
"""
搜索资产
@@ -331,6 +332,7 @@ class AssetSearchService:
Args:
query: 搜索查询字符串
asset_type: 资产类型 ('website' 或 'endpoint')
+ limit: 最大返回数量(可选)
Returns:
List[Dict]: 搜索结果列表
@@ -348,6 +350,10 @@ class AssetSearchService:
ORDER BY created_at DESC
"""
+ # 添加 LIMIT
+ if limit is not None and limit > 0:
+ sql += f" LIMIT {int(limit)}"
+
try:
with connection.cursor() as cursor:
cursor.execute(sql, params)
diff --git a/backend/apps/asset/urls.py b/backend/apps/asset/urls.py
index 45790194..d97b39fe 100644
--- a/backend/apps/asset/urls.py
+++ b/backend/apps/asset/urls.py
@@ -11,6 +11,7 @@ from .views import (
VulnerabilityViewSet,
AssetStatisticsViewSet,
AssetSearchView,
+ AssetSearchExportView,
)
# 创建 DRF 路由器
@@ -27,4 +28,5 @@ router.register(r'statistics', AssetStatisticsViewSet, basename='asset-statistic
urlpatterns = [
path('assets/', include(router.urls)),
path('assets/search/', AssetSearchView.as_view(), name='asset-search'),
+ path('assets/search/export/', AssetSearchExportView.as_view(), name='asset-search-export'),
]
diff --git a/backend/apps/asset/views/__init__.py b/backend/apps/asset/views/__init__.py
index a7f1c07a..648a80a8 100644
--- a/backend/apps/asset/views/__init__.py
+++ b/backend/apps/asset/views/__init__.py
@@ -19,7 +19,7 @@ from .asset_views import (
HostPortMappingSnapshotViewSet,
VulnerabilitySnapshotViewSet,
)
-from .search_views import AssetSearchView
+from .search_views import AssetSearchView, AssetSearchExportView
__all__ = [
'AssetStatisticsViewSet',
@@ -36,4 +36,5 @@ __all__ = [
'HostPortMappingSnapshotViewSet',
'VulnerabilitySnapshotViewSet',
'AssetSearchView',
+ 'AssetSearchExportView',
]
diff --git a/backend/apps/asset/views/search_views.py b/backend/apps/asset/views/search_views.py
index 2e3f964a..21a95b45 100644
--- a/backend/apps/asset/views/search_views.py
+++ b/backend/apps/asset/views/search_views.py
@@ -3,6 +3,7 @@
提供资产搜索的 REST API 接口:
- GET /api/assets/search/ - 搜索资产
+- GET /api/assets/search/export/ - 导出搜索结果为 CSV
搜索语法:
- field="value" 模糊匹配(ILIKE %value%)
@@ -27,10 +28,14 @@
import logging
import json
+import csv
+from io import StringIO
+from datetime import datetime
from urllib.parse import urlparse, urlunparse
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.request import Request
+from django.http import StreamingHttpResponse
from django.db import connection
from apps.common.response_helpers import success_response, error_response
@@ -269,3 +274,127 @@ class AssetSearchView(APIView):
'totalPages': total_pages,
'assetType': asset_type,
})
+
+
+class AssetSearchExportView(APIView):
+ """
+ 资产搜索导出 API
+
+ GET /api/assets/search/export/
+
+ Query Parameters:
+ q: 搜索查询表达式
+ asset_type: 资产类型 ('website' 或 'endpoint',默认 'website')
+
+ Response:
+ CSV 文件流
+ """
+
+ # 导出数量限制
+ MAX_EXPORT_ROWS = 10000
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.service = AssetSearchService()
+
+ def _parse_headers(self, headers_data) -> str:
+ """解析响应头为字符串"""
+ if not headers_data:
+ return ''
+ try:
+ headers = json.loads(headers_data)
+ return '; '.join(f'{k}: {v}' for k, v in headers.items())
+ except (json.JSONDecodeError, TypeError):
+ return str(headers_data)
+
+ def _generate_csv(self, results: list, asset_type: str):
+ """生成 CSV 内容的生成器"""
+ # 定义列
+ if asset_type == 'website':
+ columns = ['url', 'host', 'title', 'status_code', 'content_type', 'content_length',
+ 'webserver', 'location', 'tech', 'vhost', 'created_at']
+ headers = ['URL', 'Host', 'Title', 'Status', 'Content-Type', 'Content-Length',
+ 'Webserver', 'Location', 'Technologies', 'VHost', 'Created At']
+ else:
+ columns = ['url', 'host', 'title', 'status_code', 'content_type', 'content_length',
+ 'webserver', 'location', 'tech', 'matched_gf_patterns', 'vhost', 'created_at']
+ headers = ['URL', 'Host', 'Title', 'Status', 'Content-Type', 'Content-Length',
+ 'Webserver', 'Location', 'Technologies', 'GF Patterns', 'VHost', 'Created At']
+
+ # 写入 BOM 和表头
+ output = StringIO()
+ writer = csv.writer(output)
+
+ # UTF-8 BOM
+ yield '\ufeff'
+
+ # 表头
+ writer.writerow(headers)
+ yield output.getvalue()
+ output.seek(0)
+ output.truncate(0)
+
+ # 数据行
+ for result in results:
+ row = []
+ for col in columns:
+ value = result.get(col)
+ if col == 'tech' or col == 'matched_gf_patterns':
+ # 数组转字符串
+ row.append('; '.join(value) if value else '')
+ elif col == 'created_at':
+ # 日期格式化
+ row.append(value.strftime('%Y-%m-%d %H:%M:%S') if value else '')
+ elif col == 'vhost':
+ row.append('true' if value else 'false' if value is False else '')
+ else:
+ row.append(str(value) if value is not None else '')
+
+ writer.writerow(row)
+ yield output.getvalue()
+ output.seek(0)
+ output.truncate(0)
+
+ def get(self, request: Request):
+ """导出搜索结果为 CSV"""
+ # 获取搜索查询
+ query = request.query_params.get('q', '').strip()
+
+ if not query:
+ return error_response(
+ code=ErrorCodes.VALIDATION_ERROR,
+ message='Search query (q) is required',
+ status_code=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 获取并验证资产类型
+ asset_type = request.query_params.get('asset_type', 'website').strip().lower()
+ if asset_type not in VALID_ASSET_TYPES:
+ return error_response(
+ code=ErrorCodes.VALIDATION_ERROR,
+ message=f'Invalid asset_type. Must be one of: {", ".join(VALID_ASSET_TYPES)}',
+ status_code=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 获取搜索结果(限制数量)
+ results = self.service.search(query, asset_type, limit=self.MAX_EXPORT_ROWS)
+
+ if not results:
+ return error_response(
+ code=ErrorCodes.NOT_FOUND,
+ message='No results to export',
+ status_code=status.HTTP_404_NOT_FOUND
+ )
+
+ # 生成文件名
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
+ filename = f'search_{asset_type}_{timestamp}.csv'
+
+ # 返回流式响应
+ response = StreamingHttpResponse(
+ self._generate_csv(results, asset_type),
+ content_type='text/csv; charset=utf-8'
+ )
+ response['Content-Disposition'] = f'attachment; filename="{filename}"'
+
+ return response
diff --git a/docker/start.sh b/docker/start.sh
index f1c44e2d..b378588a 100755
--- a/docker/start.sh
+++ b/docker/start.sh
@@ -15,10 +15,12 @@ NC='\033[0m'
# 解析参数
WITH_FRONTEND=true
DEV_MODE=false
+QUIET_MODE=false
for arg in "$@"; do
case $arg in
--no-frontend) WITH_FRONTEND=false ;;
--dev) DEV_MODE=true ;;
+ --quiet) QUIET_MODE=true ;;
esac
done
@@ -155,6 +157,11 @@ echo -e "${GREEN}[OK]${NC} 服务已启动"
# 数据初始化
./scripts/init-data.sh
+# 静默模式下不显示结果(由调用方显示)
+if [ "$QUIET_MODE" = true ]; then
+ exit 0
+fi
+
# 获取访问地址
PUBLIC_HOST=$(grep "^PUBLIC_HOST=" .env 2>/dev/null | cut -d= -f2)
if [ -n "$PUBLIC_HOST" ] && [ "$PUBLIC_HOST" != "server" ]; then
diff --git a/frontend/components/common/smart-filter-input.tsx b/frontend/components/common/smart-filter-input.tsx
index a593b0a9..f45b993c 100644
--- a/frontend/components/common/smart-filter-input.tsx
+++ b/frontend/components/common/smart-filter-input.tsx
@@ -381,9 +381,9 @@ export function SmartFilterInput({
return (
)
}
diff --git a/frontend/components/search/search-page.tsx b/frontend/components/search/search-page.tsx
index 38fcf5ec..f6ea22f4 100644
--- a/frontend/components/search/search-page.tsx
+++ b/frontend/components/search/search-page.tsx
@@ -1,24 +1,25 @@
"use client"
import { useState, useCallback, useMemo, useEffect } from "react"
+import { useSearchParams } from "next/navigation"
import { motion, AnimatePresence } from "framer-motion"
-import { Search, AlertCircle, Globe, Link2, ShieldAlert, History, X } from "lucide-react"
+import { Search, AlertCircle, History, X, Download } from "lucide-react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
-import { useQuery } from "@tanstack/react-query"
import { SmartFilterInput, type FilterField } from "@/components/common/smart-filter-input"
import { SearchPagination } from "./search-pagination"
import { useAssetSearch } from "@/hooks/use-search"
import { VulnerabilityDetailDialog } from "@/components/vulnerabilities/vulnerability-detail-dialog"
import { VulnerabilityService } from "@/services/vulnerability.service"
+import { SearchService } from "@/services/search.service"
import type { SearchParams, SearchState, Vulnerability as SearchVuln, AssetType } from "@/types/search.types"
import type { Vulnerability } from "@/types/vulnerability.types"
import { Alert, AlertDescription } from "@/components/ui/alert"
+import { Button } from "@/components/ui/button"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { SearchResultsTable } from "./search-results-table"
import { SearchResultCard } from "./search-result-card"
import { Badge } from "@/components/ui/badge"
-import { getAssetStatistics } from "@/services/dashboard.service"
import { cn } from "@/lib/utils"
// Website 搜索示例
@@ -98,6 +99,7 @@ function removeRecentSearch(query: string) {
export function SearchPage() {
const t = useTranslations('search')
+ const urlSearchParams = useSearchParams()
const [searchState, setSearchState] = useState("initial")
const [query, setQuery] = useState("")
const [assetType, setAssetType] = useState("website")
@@ -108,19 +110,28 @@ export function SearchPage() {
const [vulnDialogOpen, setVulnDialogOpen] = useState(false)
const [, setLoadingVuln] = useState(false)
const [recentSearches, setRecentSearches] = useState([])
-
- // 获取资产统计数据
- const { data: stats } = useQuery({
- queryKey: ['assetStatistics'],
- queryFn: getAssetStatistics,
- staleTime: 5 * 60 * 1000, // 5 minutes
- })
+ const [initialQueryProcessed, setInitialQueryProcessed] = useState(false)
// 加载最近搜索记录
useEffect(() => {
setRecentSearches(getRecentSearches())
}, [])
+ // 处理 URL 参数中的搜索查询
+ useEffect(() => {
+ if (initialQueryProcessed) return
+
+ const q = urlSearchParams.get('q')
+ if (q) {
+ setQuery(q)
+ setSearchParams({ q, asset_type: assetType })
+ setSearchState("searching")
+ saveRecentSearch(q)
+ setRecentSearches(getRecentSearches())
+ }
+ setInitialQueryProcessed(true)
+ }, [urlSearchParams, assetType, initialQueryProcessed])
+
// 根据资产类型选择搜索示例
const searchExamples = useMemo(() => {
return assetType === 'endpoint' ? ENDPOINT_SEARCH_EXAMPLES : WEBSITE_SEARCH_EXAMPLES
@@ -178,6 +189,25 @@ export function SearchPage() {
setRecentSearches(getRecentSearches())
}, [])
+ // 导出状态
+ const [isExporting, setIsExporting] = useState(false)
+
+ // 导出 CSV(调用后端 API 导出全部结果)
+ const handleExportCSV = useCallback(async () => {
+ if (!searchParams.q) return
+
+ setIsExporting(true)
+ try {
+ await SearchService.exportCSV(searchParams.q, assetType)
+ toast.success(t('exportSuccess'))
+ } catch (error) {
+ console.error('Export failed:', error)
+ toast.error(t('exportFailed'))
+ } finally {
+ setIsExporting(false)
+ }
+ }, [searchParams.q, assetType, t])
+
// 当数据加载完成时更新状态
if (searchState === "searching" && data && !isLoading) {
setSearchState("results")
@@ -289,38 +319,6 @@ export function SearchPage() {
))}
- {/* 资产统计 */}
- {stats && (
-
-
-
-
- {stats.totalWebsites.toLocaleString()}
-
- {t('assetTypes.website')}
-
-
-
-
- {stats.totalEndpoints.toLocaleString()}
-
- {t('assetTypes.endpoint')}
-
-
-
-
- {stats.totalVulns.toLocaleString()}
-
- {t('stats.vulnerabilities')}
-
-
- )}
-
{/* 最近搜索 */}
{recentSearches.length > 0 && (
{isFetching ? t('loading') : t('resultsCount', { count: data?.total ?? 0 })}
+
diff --git a/frontend/components/search/search-results-table.tsx b/frontend/components/search/search-results-table.tsx
index ea87c700..d94f1533 100644
--- a/frontend/components/search/search-results-table.tsx
+++ b/frontend/components/search/search-results-table.tsx
@@ -1,7 +1,7 @@
"use client"
import { useMemo } from "react"
-import { useTranslations, useFormatter } from "next-intl"
+import { useFormatter } from "next-intl"
import type { ColumnDef } from "@tanstack/react-table"
import { Badge } from "@/components/ui/badge"
import { DataTableColumnHeader, UnifiedDataTable } from "@/components/ui/data-table"
@@ -15,7 +15,6 @@ interface SearchResultsTableProps {
}
export function SearchResultsTable({ results, assetType }: SearchResultsTableProps) {
- const t = useTranslations('search.table')
const format = useFormatter()
const formatDate = (dateString: string) => {
@@ -33,9 +32,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
{
id: "url",
accessorKey: "url",
- meta: { title: t('url') },
+ meta: { title: "URL" },
header: ({ column }) => (
-
+
),
size: 350,
minSize: 200,
@@ -47,9 +46,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
{
id: "host",
accessorKey: "host",
- meta: { title: t('host') },
+ meta: { title: "Host" },
header: ({ column }) => (
-
+
),
size: 180,
minSize: 100,
@@ -61,9 +60,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
{
id: "title",
accessorKey: "title",
- meta: { title: t('title') },
+ meta: { title: "Title" },
header: ({ column }) => (
-
+
),
size: 150,
minSize: 100,
@@ -75,9 +74,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
{
id: "statusCode",
accessorKey: "statusCode",
- meta: { title: t('status') },
+ meta: { title: "Status" },
header: ({ column }) => (
-
+
),
size: 80,
minSize: 60,
@@ -103,9 +102,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
{
id: "technologies",
accessorKey: "technologies",
- meta: { title: t('technologies') },
+ meta: { title: "Tech" },
header: ({ column }) => (
-
+
),
size: 180,
minSize: 120,
@@ -118,9 +117,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
{
id: "contentLength",
accessorKey: "contentLength",
- meta: { title: t('contentLength') },
+ meta: { title: "Length" },
header: ({ column }) => (
-
+
),
size: 100,
minSize: 80,
@@ -134,9 +133,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
{
id: "location",
accessorKey: "location",
- meta: { title: t('location') },
+ meta: { title: "Location" },
header: ({ column }) => (
-
+
),
size: 150,
minSize: 100,
@@ -148,9 +147,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
{
id: "webserver",
accessorKey: "webserver",
- meta: { title: t('webserver') },
+ meta: { title: "Server" },
header: ({ column }) => (
-
+
),
size: 120,
minSize: 80,
@@ -162,9 +161,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
{
id: "contentType",
accessorKey: "contentType",
- meta: { title: t('contentType') },
+ meta: { title: "Type" },
header: ({ column }) => (
-
+
),
size: 120,
minSize: 80,
@@ -176,9 +175,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
{
id: "responseBody",
accessorKey: "responseBody",
- meta: { title: t('responseBody') },
+ meta: { title: "Body" },
header: ({ column }) => (
-
+
),
size: 300,
minSize: 200,
@@ -189,9 +188,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
{
id: "responseHeaders",
accessorKey: "responseHeaders",
- meta: { title: t('responseHeaders') },
+ meta: { title: "Headers" },
header: ({ column }) => (
-
+
),
size: 250,
minSize: 150,
@@ -210,9 +209,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
{
id: "vhost",
accessorKey: "vhost",
- meta: { title: t('vhost') },
+ meta: { title: "VHost" },
header: ({ column }) => (
-
+
),
size: 80,
minSize: 60,
@@ -226,9 +225,9 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
{
id: "createdAt",
accessorKey: "createdAt",
- meta: { title: t('createdAt') },
+ meta: { title: "Created" },
header: ({ column }) => (
-
+
),
size: 150,
minSize: 120,
@@ -239,16 +238,16 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
return {formatDate(createdAt)}
},
},
- ], [t, formatDate])
+ ], [formatDate])
// Endpoint 特有列
const endpointColumns: ColumnDef[] = useMemo(() => [
{
id: "matchedGfPatterns",
accessorKey: "matchedGfPatterns",
- meta: { title: t('gfPatterns') },
+ meta: { title: "GF Patterns" },
header: ({ column }) => (
-
+
),
size: 150,
minSize: 100,
@@ -259,7 +258,7 @@ export function SearchResultsTable({ results, assetType }: SearchResultsTablePro
return
},
},
- ], [t])
+ ], [])
// 根据资产类型组合列
const columns = useMemo(() => {
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 574a7316..b03491ac 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -326,6 +326,10 @@
"noResultsHint": "Try adjusting your search criteria",
"vulnLoadError": "Failed to load vulnerability details",
"recentSearches": "Recent Searches",
+ "export": "Export",
+ "exporting": "Exporting...",
+ "exportSuccess": "Export successful",
+ "exportFailed": "Export failed",
"stats": {
"vulnerabilities": "Vulnerabilities"
},
@@ -1971,6 +1975,16 @@
"formatInvalid": "Invalid format"
}
},
+ "globalSearch": {
+ "search": "Search",
+ "placeholder": "Search assets... (host=\"api\" && tech=\"nginx\")",
+ "noResults": "No results found",
+ "searchFor": "Search for",
+ "recent": "Recent Searches",
+ "quickSearch": "Quick Search",
+ "hint": "Supports FOFA-style syntax",
+ "toSearch": "to search"
+ },
"errors": {
"unknown": "Operation failed, please try again later",
"validation": "Invalid input data",
diff --git a/frontend/messages/zh.json b/frontend/messages/zh.json
index 052f3b7d..b041f140 100644
--- a/frontend/messages/zh.json
+++ b/frontend/messages/zh.json
@@ -326,6 +326,10 @@
"noResultsHint": "请尝试调整搜索条件",
"vulnLoadError": "加载漏洞详情失败",
"recentSearches": "最近搜索",
+ "export": "导出",
+ "exporting": "导出中...",
+ "exportSuccess": "导出成功",
+ "exportFailed": "导出失败",
"stats": {
"vulnerabilities": "漏洞"
},
@@ -1971,6 +1975,16 @@
"formatInvalid": "格式无效"
}
},
+ "globalSearch": {
+ "search": "搜索",
+ "placeholder": "搜索资产... (host=\"api\" && tech=\"nginx\")",
+ "noResults": "未找到结果",
+ "searchFor": "搜索",
+ "recent": "最近搜索",
+ "quickSearch": "快捷搜索",
+ "hint": "支持 FOFA 风格语法",
+ "toSearch": "搜索"
+ },
"errors": {
"unknown": "操作失败,请稍后重试",
"validation": "输入数据无效",
diff --git a/frontend/services/search.service.ts b/frontend/services/search.service.ts
index a64b0eac..5f6bb31c 100644
--- a/frontend/services/search.service.ts
+++ b/frontend/services/search.service.ts
@@ -1,5 +1,5 @@
import { api } from "@/lib/api-client"
-import type { SearchParams, SearchResponse } from "@/types/search.types"
+import type { SearchParams, SearchResponse, AssetType } from "@/types/search.types"
/**
* 资产搜索 API 服务
@@ -38,4 +38,38 @@ export class SearchService {
)
return response.data
}
+
+ /**
+ * 导出搜索结果为 CSV
+ * GET /api/assets/search/export/
+ */
+ static async exportCSV(query: string, assetType: AssetType): Promise {
+ const queryParams = new URLSearchParams()
+ queryParams.append('q', query)
+ queryParams.append('asset_type', assetType)
+
+ const response = await api.get(
+ `/assets/search/export/?${queryParams.toString()}`,
+ { responseType: 'blob' }
+ )
+
+ // 从响应头获取文件名
+ const contentDisposition = response.headers?.['content-disposition']
+ let filename = `search_${assetType}_${new Date().toISOString().slice(0, 10)}.csv`
+ if (contentDisposition) {
+ const match = contentDisposition.match(/filename="?([^"]+)"?/)
+ if (match) filename = match[1]
+ }
+
+ // 创建下载链接
+ const blob = new Blob([response.data as BlobPart], { type: 'text/csv;charset=utf-8' })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = filename
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ URL.revokeObjectURL(url)
+ }
}
diff --git a/install.sh b/install.sh
index 093c793c..16069cde 100755
--- a/install.sh
+++ b/install.sh
@@ -94,7 +94,7 @@ show_banner() {
echo -e "${MAGENTA} ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝${RESET}"
echo -e ""
echo -e "${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
- echo -e "${BOLD} 🔒 分布式安全扫描平台 │ 一键部署安装程序${RESET}"
+ echo -e "${BOLD} 🔒 分布式安全扫描平台 │ 一键部署 (Ubuntu)${RESET}"
echo -e "${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e ""
}
@@ -132,7 +132,6 @@ fi
# 显示标题
show_banner
-header "XingRin 一键安装脚本 (Ubuntu)"
info "当前用户: ${BOLD}$REAL_USER${RESET}"
info "项目路径: ${BOLD}$ROOT_DIR${RESET}"
info "安装版本: ${BOLD}$APP_VERSION${RESET}"
@@ -324,7 +323,7 @@ check_pg_ivm() {
# 显示安装总结信息
show_summary() {
echo
- if [ "$1" == "success" ]; then
+ if [ "$1" = "success" ]; then
# 成功 Banner
echo -e ""
echo -e "${GREEN}${BOLD} ╔═══════════════════════════════════════════════════╗${RESET}"
@@ -378,7 +377,9 @@ show_summary() {
echo -e " ${YELLOW} ⚠ 请首次登录后修改密码!${RESET}"
echo
- if [ "$1" != "success" ]; then
+ if [ "$1" = "success" ]; then
+ : # 成功模式,不显示后续命令
+ else
echo -e "${DIM} ──────────────────────────────────────────────────────${RESET}"
echo -e " ${BLUE}🚀 后续命令${RESET}"
echo -e " ${DIM}├─${RESET} ./start.sh ${DIM}# 启动所有服务${RESET}"
@@ -743,7 +744,7 @@ fi
# 启动服务
# ==============================================================================
step "正在启动服务..."
-"$ROOT_DIR/start.sh" $START_ARGS
+"$ROOT_DIR/start.sh" ${START_ARGS} --quiet
# ==============================================================================
# 完成总结