Merge branch 'SecScore-tauri' of https://github.com/SECTL/SecScore into SecScore-tauri

This commit is contained in:
Fox_block
2026-03-20 21:35:10 +08:00
12 changed files with 871 additions and 2 deletions
+88
View File
@@ -63,6 +63,7 @@ function MainContent(): React.JSX.Element {
const syncCheckingRef = useRef(false)
const syncApplyLoadingRef = useRef(false)
const lastLocalMutationAtRef = useRef(0)
const lastLandscapeSizeRef = useRef<{ width: number; height: number } | null>(null)
const activeMenu = useMemo(() => {
const p = location.pathname
@@ -284,6 +285,93 @@ function MainContent(): React.JSX.Element {
if (key === "settings") navigate("/settings")
}
<<<<<<< HEAD
=======
const getMaxWindowSize = () => {
const availableWidth = window.screen?.availWidth || window.innerWidth || 1200
const availableHeight = window.screen?.availHeight || window.innerHeight || 800
return {
maxWidth: Math.max(360, Math.round(availableWidth - 64)),
maxHeight: Math.max(640, Math.round(availableHeight - 64)),
}
}
const getFixedPortraitSize = () => {
const { maxWidth, maxHeight } = getMaxWindowSize()
const phonePortraitBase = { width: 430, height: 932 }
let width = Math.max(360, Math.min(phonePortraitBase.width, maxWidth))
let height = Math.max(640, Math.min(phonePortraitBase.height, maxHeight))
// 保证竖屏高于宽,维持手机风格比例。
if (height <= width) {
height = Math.min(maxHeight, Math.max(640, Math.round(width * 2.0)))
}
if (height <= width) {
width = Math.max(360, Math.min(maxWidth, Math.round(height * 0.5)))
}
return { width, height }
}
const getLandscapeRestoreSize = () => {
const { maxWidth, maxHeight } = getMaxWindowSize()
const fallback = { width: 1180, height: 680 }
const source = lastLandscapeSizeRef.current || fallback
let width = Math.max(800, Math.min(maxWidth, Math.round(source.width)))
let height = Math.max(600, Math.min(maxHeight, Math.round(source.height)))
if (width < height) {
const swappedWidth = Math.max(800, Math.min(maxWidth, height))
const swappedHeight = Math.max(600, Math.min(maxHeight, width))
width = swappedWidth
height = swappedHeight
}
return { width, height }
}
const toggleOrientationMode = async () => {
const api = (window as any).api
if (!api) return
const nextPortraitMode = !isPortraitMode
try {
const maximized = await api.windowIsMaximized()
if (maximized) {
await api.windowMaximize()
}
if (nextPortraitMode) {
lastLandscapeSizeRef.current = {
width: Math.max(1, Math.round(window.innerWidth || 0)),
height: Math.max(1, Math.round(window.innerHeight || 0)),
}
const targetSize = getFixedPortraitSize()
await api.windowSetResizable(false)
await api.windowResize(targetSize.width, targetSize.height)
setSidebarCollapsed(true)
setFloatingSidebarExpanded(false)
} else {
const targetSize = getLandscapeRestoreSize()
await api.windowSetResizable(true)
await api.windowResize(targetSize.width, targetSize.height)
setSidebarCollapsed(false)
setFloatingSidebarExpanded(false)
}
setIsPortraitMode(nextPortraitMode)
} catch (error) {
messageApi.error(t("common.error"))
console.error("Failed to toggle orientation mode:", error)
}
}
useEffect(() => {
if (!isPortraitMode || !sidebarCollapsed) {
setFloatingSidebarExpanded(false)
}
}, [isPortraitMode, sidebarCollapsed])
>>>>>>> 1fe9dbe66a8a3df649fd86fb5ba64f3178057ec1
const toggleSidebar = () => {
if (isPortraitMode && sidebarCollapsed) {
setFloatingSidebarExpanded((prev) => !prev)
+155
View File
@@ -88,6 +88,20 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
const [urlRegisterLoading, setUrlRegisterLoading] = useState(false)
const [appQuitLoading, setAppQuitLoading] = useState(false)
const [appRestartLoading, setAppRestartLoading] = useState(false)
const [mcpLoading, setMcpLoading] = useState(false)
const [mcpConfig, setMcpConfig] = useState<{ host: string; port: number }>({
host: "127.0.0.1",
port: 3901,
})
const [mcpStatus, setMcpStatus] = useState<{
is_running: boolean
config: { host: string; port: number }
url?: string | null
}>({
is_running: false,
config: { host: "127.0.0.1", port: 3901 },
url: null,
})
const canAdmin = permission === "admin"
const [messageApi, contextHolder] = message.useMessage()
@@ -119,6 +133,21 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
window.dispatchEvent(new CustomEvent("ss:data-updated", { detail: { category } }))
}
const loadMcpStatus = async () => {
if (!(window as any).api?.mcpServerStatus) return
try {
const res = await (window as any).api.mcpServerStatus()
if (res.success && res.data) {
setMcpStatus(res.data)
if (res.data.config?.host && res.data.config?.port) {
setMcpConfig({ host: res.data.config.host, port: res.data.config.port })
}
}
} catch {
// ignore status polling errors in settings page
}
}
const loadAll = async () => {
if (!(window as any).api) return
const res = await (window as any).api.getAllSettings()
@@ -129,6 +158,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
}
const authRes = await (window as any).api.authGetStatus()
if (authRes.success && authRes.data) setSecurityStatus(authRes.data)
await loadMcpStatus()
}
useEffect(() => {
@@ -464,6 +494,61 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
}
}
const startMcpServer = async () => {
if (!(window as any).api?.mcpServerStart) return
const host = mcpConfig.host.trim()
const port = Number(mcpConfig.port)
if (!host) {
messageApi.warning(t("settings.mcp.hostRequired"))
return
}
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
messageApi.warning(t("settings.mcp.portInvalid"))
return
}
setMcpLoading(true)
try {
const res = await withTimeout(
(window as any).api.mcpServerStart({ host, port }),
10_000,
t("settings.mcp.startTimeout")
)
if (res.success) {
messageApi.success(t("settings.mcp.startSuccess"))
} else {
messageApi.error(res.message || t("settings.mcp.startFailed"))
}
} catch (e: any) {
messageApi.error(e?.message || t("settings.mcp.startFailed"))
} finally {
await loadMcpStatus()
setMcpLoading(false)
}
}
const stopMcpServer = async () => {
if (!(window as any).api?.mcpServerStop) return
setMcpLoading(true)
try {
const res = await withTimeout(
(window as any).api.mcpServerStop(),
10_000,
t("settings.mcp.stopTimeout")
)
if (res.success) {
messageApi.success(t("settings.mcp.stopSuccess"))
} else {
messageApi.error(res.message || t("settings.mcp.stopFailed"))
}
} catch (e: any) {
messageApi.error(e?.message || t("settings.mcp.stopFailed"))
} finally {
await loadMcpStatus()
setMcpLoading(false)
}
}
const currentYear = new Date().getFullYear()
const confirmQuitApp = () => {
@@ -1065,6 +1150,76 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
</Space>
</div>
<Divider />
<div style={{ fontSize: "16px", fontWeight: 600, marginBottom: "8px" }}>
{t("settings.mcp.title")}
</div>
<div style={{ color: "var(--ss-text-secondary)", marginBottom: "12px", fontSize: "12px" }}>
{t("settings.mcp.description")}
</div>
<Space style={{ marginBottom: "12px" }}>
<Tag color={mcpStatus.is_running ? "success" : "default"}>
{mcpStatus.is_running ? t("settings.mcp.running") : t("settings.mcp.stopped")}
</Tag>
{mcpStatus.url ? (
<Tag color="blue">{mcpStatus.url}</Tag>
) : (
<Tag>{t("settings.mcp.noUrl")}</Tag>
)}
</Space>
<Form layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
<Form.Item label={t("settings.mcp.host")}>
<Input
value={mcpConfig.host}
onChange={(e) => setMcpConfig((prev) => ({ ...prev, host: e.target.value }))}
disabled={!canAdmin || mcpStatus.is_running}
style={{ width: "280px" }}
placeholder="127.0.0.1"
/>
</Form.Item>
<Form.Item label={t("settings.mcp.port")}>
<Input
value={String(mcpConfig.port)}
onChange={(e) => {
const next = Number(e.target.value.replace(/[^\d]/g, ""))
if (Number.isFinite(next) && next > 0) {
setMcpConfig((prev) => ({ ...prev, port: next }))
} else if (!e.target.value) {
setMcpConfig((prev) => ({ ...prev, port: 0 }))
}
}}
disabled={!canAdmin || mcpStatus.is_running}
style={{ width: "180px" }}
placeholder="3901"
/>
</Form.Item>
<Form.Item label={t("common.operation")}>
<Space>
<Button
type="primary"
onClick={startMcpServer}
loading={mcpLoading}
disabled={!canAdmin || mcpStatus.is_running}
>
{t("settings.mcp.start")}
</Button>
<Button
danger
onClick={stopMcpServer}
loading={mcpLoading}
disabled={!canAdmin || !mcpStatus.is_running}
>
{t("settings.mcp.stop")}
</Button>
<Button onClick={loadMcpStatus} disabled={mcpLoading}>
{t("settings.mcp.refresh")}
</Button>
</Space>
</Form.Item>
</Form>
<div style={{ color: "var(--ss-text-secondary)", marginTop: "-8px", fontSize: "12px" }}>
{t("settings.mcp.hint")}
</div>
<Divider />
<div style={{ fontSize: "16px", fontWeight: 600, marginBottom: "8px" }}>
{t("settings.app.title")}
</div>
+21
View File
@@ -276,6 +276,27 @@
"registerFailed": "Registration failed",
"installerRequired": "Requires installed SecScore, may not work in development mode."
},
"mcp": {
"title": "MCP Service",
"description": "Manage the built-in MCP HTTP service for external MCP clients.",
"running": "Running",
"stopped": "Stopped",
"noUrl": "No URL",
"host": "Host",
"port": "Port",
"start": "Start",
"stop": "Stop",
"refresh": "Refresh Status",
"hint": "Recommended to bind only to 127.0.0.1; after start, endpoint is http://host:port/mcp",
"hostRequired": "Please enter MCP host",
"portInvalid": "Please enter a valid port (1-65535)",
"startSuccess": "MCP server started",
"startFailed": "Failed to start MCP server",
"stopSuccess": "MCP server stopped",
"stopFailed": "Failed to stop MCP server",
"startTimeout": "MCP server start timed out, please try again",
"stopTimeout": "MCP server stop timed out, please try again"
},
"app": {
"title": "App Controls",
"description": "Quickly restart or quit the app.",
+21
View File
@@ -276,6 +276,27 @@
"registerFailed": "注册失败",
"installerRequired": "需要安装版 SecScore,开发模式下可能无效。"
},
"mcp": {
"title": "MCP 服务",
"description": "管理内置 MCP HTTP 服务,供外部 MCP 客户端调用。",
"running": "运行中",
"stopped": "已停止",
"noUrl": "暂无地址",
"host": "主机",
"port": "端口",
"start": "启动",
"stop": "停止",
"refresh": "刷新状态",
"hint": "建议仅绑定到 127.0.0.1;启动后 MCP 入口为 http://host:port/mcp",
"hostRequired": "请输入 MCP 主机地址",
"portInvalid": "请输入有效端口(1-65535",
"startSuccess": "MCP 服务启动成功",
"startFailed": "MCP 服务启动失败",
"stopSuccess": "MCP 服务已停止",
"stopFailed": "MCP 服务停止失败",
"startTimeout": "启动 MCP 服务超时,请稍后重试",
"stopTimeout": "停止 MCP 服务超时,请稍后重试"
},
"app": {
"title": "应用控制",
"description": "快速重启或退出应用。",
+16
View File
@@ -295,6 +295,22 @@ const api = {
data: { isRunning: boolean; config?: any; url?: string }
}> => invoke("http_server_status"),
// MCP Server
mcpServerStart: (config?: {
port?: number
host?: string
}): Promise<{ success: boolean; data?: { url: string; config: { port: number; host: string } } }> =>
invoke("mcp_server_start", { config }),
mcpServerStop: (): Promise<{ success: boolean }> => invoke("mcp_server_stop"),
mcpServerStatus: (): Promise<{
success: boolean
data?: {
is_running: boolean
config: { port: number; host: string }
url?: string | null
}
}> => invoke("mcp_server_status"),
// File System
fsGetConfigStructure: (): Promise<{
success: boolean