Compare commits
2 Commits
master
...
codex/clie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a574b2c661 | ||
|
|
5d87c9eee7 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@qiming-ai/qimingclaw",
|
"name": "@qiming-ai/qimingclaw",
|
||||||
"version": "0.9.7",
|
"version": "0.9.7",
|
||||||
"description": "启明数字员工",
|
"description": "陇电数字员工",
|
||||||
"main": "dist/main/main.js",
|
"main": "dist/main/main.js",
|
||||||
"author": "Qiming Team",
|
"author": "Qiming Team",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.qiming.qimingclaw",
|
"appId": "com.qiming.qimingclaw",
|
||||||
"productName": "启明数字员工",
|
"productName": "陇电数字员工",
|
||||||
"afterSign": "scripts/build/after-sign.js",
|
"afterSign": "scripts/build/after-sign.js",
|
||||||
"electronLanguages": [
|
"electronLanguages": [
|
||||||
"en",
|
"en",
|
||||||
@@ -297,8 +297,8 @@
|
|||||||
"hardenedRuntime": true,
|
"hardenedRuntime": true,
|
||||||
"extendInfo": {
|
"extendInfo": {
|
||||||
"CFBundleIdentifier": "com.qiming.agent",
|
"CFBundleIdentifier": "com.qiming.agent",
|
||||||
"CFBundleName": "启明数字员工",
|
"CFBundleName": "陇电数字员工",
|
||||||
"CFBundleDisplayName": "启明数字员工",
|
"CFBundleDisplayName": "陇电数字员工",
|
||||||
"NSDesktopFolderUsageDescription": "需要访问桌面文件夹以支持文件操作",
|
"NSDesktopFolderUsageDescription": "需要访问桌面文件夹以支持文件操作",
|
||||||
"NSDocumentsFolderUsageDescription": "需要访问文档文件夹以支持文件操作",
|
"NSDocumentsFolderUsageDescription": "需要访问文档文件夹以支持文件操作",
|
||||||
"NSDownloadsFolderUsageDescription": "需要访问下载文件夹以支持文件操作"
|
"NSDownloadsFolderUsageDescription": "需要访问下载文件夹以支持文件操作"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 672 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 67 KiB |
@@ -9,26 +9,13 @@ import React, {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
ConfigProvider,
|
ConfigProvider,
|
||||||
Menu,
|
|
||||||
Badge,
|
|
||||||
Spin,
|
Spin,
|
||||||
Button,
|
Button,
|
||||||
|
Modal,
|
||||||
notification,
|
notification,
|
||||||
message,
|
message,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import type { PresetStatusColorType } from "antd/es/_util/colors";
|
import { ArrowLeftOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||||
import {
|
|
||||||
SettingOutlined,
|
|
||||||
DashboardOutlined,
|
|
||||||
FolderOutlined,
|
|
||||||
InfoCircleOutlined,
|
|
||||||
SafetyOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
TeamOutlined,
|
|
||||||
ArrowLeftOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
ApiOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import {
|
import {
|
||||||
setupService,
|
setupService,
|
||||||
authService,
|
authService,
|
||||||
@@ -39,16 +26,14 @@ import {
|
|||||||
syncConfigToServer,
|
syncConfigToServer,
|
||||||
normalizeServerHost,
|
normalizeServerHost,
|
||||||
loginAndRegister,
|
loginAndRegister,
|
||||||
|
logout,
|
||||||
|
isLoggedIn as checkIsLoggedIn,
|
||||||
} from "./services/core/auth";
|
} from "./services/core/auth";
|
||||||
import {
|
import { AUTH_KEYS, normalizeAgentEngine } from "@shared/constants";
|
||||||
APP_DISPLAY_NAME,
|
|
||||||
AUTH_KEYS,
|
|
||||||
normalizeAgentEngine,
|
|
||||||
} from "@shared/constants";
|
|
||||||
import type { QuickInitConfig } from "@shared/types/quickInit";
|
import type { QuickInitConfig } from "@shared/types/quickInit";
|
||||||
import type { UpdateState } from "@shared/types/updateTypes";
|
|
||||||
import { t, getCurrentLang } from "./services/core/i18n";
|
import { t, getCurrentLang } from "./services/core/i18n";
|
||||||
import SetupWizard from "./components/setup/SetupWizard";
|
import SetupWizard from "./components/setup/SetupWizard";
|
||||||
|
import LoginPage from "./components/setup/LoginPage";
|
||||||
import SetupDependencies from "./components/setup/SetupDependencies";
|
import SetupDependencies from "./components/setup/SetupDependencies";
|
||||||
import ClientPage from "./components/pages/ClientPage";
|
import ClientPage from "./components/pages/ClientPage";
|
||||||
import SettingsPage from "./components/pages/SettingsPage";
|
import SettingsPage from "./components/pages/SettingsPage";
|
||||||
@@ -58,6 +43,10 @@ import LogViewer from "./components/pages/LogViewer";
|
|||||||
import PermissionsPage from "./components/pages/PermissionsPage";
|
import PermissionsPage from "./components/pages/PermissionsPage";
|
||||||
import SessionsPage from "./components/pages/SessionsPage";
|
import SessionsPage from "./components/pages/SessionsPage";
|
||||||
import MCPSettings from "./components/settings/MCPSettings";
|
import MCPSettings from "./components/settings/MCPSettings";
|
||||||
|
import AppSidebar, {
|
||||||
|
NavKey,
|
||||||
|
LegacyTabKey,
|
||||||
|
} from "./components/layout/AppSidebar";
|
||||||
import type { WebviewHeaderActions } from "./components/pages/SessionsPage";
|
import type { WebviewHeaderActions } from "./components/pages/SessionsPage";
|
||||||
import { createLogger } from "./services/utils/rendererLog";
|
import { createLogger } from "./services/utils/rendererLog";
|
||||||
import styles from "./styles/components/App.module.css";
|
import styles from "./styles/components/App.module.css";
|
||||||
@@ -110,27 +99,7 @@ type TabKey =
|
|||||||
| "dependencies"
|
| "dependencies"
|
||||||
| "permissions"
|
| "permissions"
|
||||||
| "logs"
|
| "logs"
|
||||||
| "about"
|
| "about";
|
||||||
| "model";
|
|
||||||
|
|
||||||
// 状态配置(对齐 Tauri 客户端)
|
|
||||||
// 就绪、繁忙使用橙色(warning)、小点展示
|
|
||||||
const STATUS_CONFIG: Record<
|
|
||||||
string,
|
|
||||||
{ status: PresetStatusColorType; textKey: string }
|
|
||||||
> = {
|
|
||||||
idle: { status: "warning", textKey: "Claw.Agent.Status.idle" },
|
|
||||||
starting: { status: "processing", textKey: "Claw.Agent.Status.starting" },
|
|
||||||
running: { status: "success", textKey: "Claw.Agent.Status.running" },
|
|
||||||
busy: { status: "warning", textKey: "Claw.Agent.Status.busy" },
|
|
||||||
stopped: { status: "default", textKey: "Claw.Agent.Status.stopped" },
|
|
||||||
error: { status: "error", textKey: "Claw.Agent.Status.error" },
|
|
||||||
};
|
|
||||||
|
|
||||||
/** macOS/Linux 无 download-progress 时,header tag 用本地模拟进度避免长期显示 0%。 */
|
|
||||||
const HEADER_SIMULATED_PROGRESS_CAP = 90;
|
|
||||||
const HEADER_SIMULATED_PROGRESS_INTERVAL_MS = 500;
|
|
||||||
const HEADER_SIMULATED_DURATION_MS = 45_000;
|
|
||||||
|
|
||||||
// 服务状态接口(与 ClientPage 共享)
|
// 服务状态接口(与 ClientPage 共享)
|
||||||
export interface ServiceItem {
|
export interface ServiceItem {
|
||||||
@@ -187,6 +156,9 @@ function App() {
|
|||||||
// 初始化向导状态
|
// 初始化向导状态
|
||||||
// ============================================
|
// ============================================
|
||||||
const [isSetupComplete, setIsSetupComplete] = useState<boolean | null>(null);
|
const [isSetupComplete, setIsSetupComplete] = useState<boolean | null>(null);
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState<boolean | null>(null);
|
||||||
|
const [loginStep1Config, setLoginStep1Config] =
|
||||||
|
useState<Step1Config>(DEFAULT_STEP1_CONFIG);
|
||||||
const setupJustCompleted = useRef(false);
|
const setupJustCompleted = useRef(false);
|
||||||
// 内存变量:标记服务是否由登录流程启动(不持久化)
|
// 内存变量:标记服务是否由登录流程启动(不持久化)
|
||||||
const loginStartedRef = useRef(false);
|
const loginStartedRef = useRef(false);
|
||||||
@@ -340,36 +312,21 @@ function App() {
|
|||||||
// 核心状态
|
// 核心状态
|
||||||
// ============================================
|
// ============================================
|
||||||
const [activeTab, setActiveTab] = useState<TabKey>("client");
|
const [activeTab, setActiveTab] = useState<TabKey>("client");
|
||||||
|
const [activeNavKey, setActiveNavKey] = useState<NavKey>("overview");
|
||||||
const [sessionsAutoOpen, setSessionsAutoOpen] = useState(false);
|
const [sessionsAutoOpen, setSessionsAutoOpen] = useState(false);
|
||||||
const [webviewActions, setWebviewActions] =
|
const [webviewActions, setWebviewActions] =
|
||||||
useState<WebviewHeaderActions | null>(null);
|
useState<WebviewHeaderActions | null>(null);
|
||||||
const [username, setUsername] = useState<string>("");
|
const [username, setUsername] = useState<string>("");
|
||||||
|
const [userEmail, setUserEmail] = useState<string>("");
|
||||||
const [onlineStatus, setOnlineStatus] = useState<boolean | null>(null);
|
const [onlineStatus, setOnlineStatus] = useState<boolean | null>(null);
|
||||||
const [agentStatus, setAgentStatus] = useState<string>("idle");
|
|
||||||
const [services, setServices] = useState<ServiceItem[]>([]);
|
const [services, setServices] = useState<ServiceItem[]>([]);
|
||||||
const [servicesLoading, setServicesLoading] = useState(true);
|
const [servicesLoading, setServicesLoading] = useState(true);
|
||||||
const [guiMcpEnabled, setGuiMcpEnabled] = useState(false);
|
|
||||||
const [pollFailCount, setPollFailCount] = useState(0);
|
|
||||||
const [startingServices, setStartingServices] = useState<Set<string>>(
|
const [startingServices, setStartingServices] = useState<Set<string>>(
|
||||||
new Set(),
|
new Set(),
|
||||||
);
|
);
|
||||||
const servicesPollTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
const servicesPollTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
/** 递增后通知 ClientPage 刷新账号状态(用户名等),与 reg 返回保持一致 */
|
/** 递增后通知 ClientPage 刷新账号状态(用户名等),与 reg 返回保持一致 */
|
||||||
const [authRefreshTrigger, setAuthRefreshTrigger] = useState(0);
|
const [authRefreshTrigger, setAuthRefreshTrigger] = useState(0);
|
||||||
const [updateState, setUpdateState] = useState<UpdateState>({
|
|
||||||
status: "idle",
|
|
||||||
});
|
|
||||||
const [headerSimulatedPercent, setHeaderSimulatedPercent] = useState(0);
|
|
||||||
const headerSimulatedIntervalRef = useRef<ReturnType<
|
|
||||||
typeof setInterval
|
|
||||||
> | null>(null);
|
|
||||||
const statusExpectedKeys = useMemo(() => {
|
|
||||||
const keys = ["mcpProxy", "agent", "fileServer", "lanproxy"];
|
|
||||||
if (FEATURES.ENABLE_GUI_AGENT_SERVER && guiMcpEnabled) {
|
|
||||||
keys.splice(3, 0, "guiServer");
|
|
||||||
}
|
|
||||||
return keys;
|
|
||||||
}, [guiMcpEnabled]);
|
|
||||||
const getStartupServiceKeys = useCallback(async (): Promise<string[]> => {
|
const getStartupServiceKeys = useCallback(async (): Promise<string[]> => {
|
||||||
const keys = ["mcpProxy", "agent", "fileServer", "lanproxy"];
|
const keys = ["mcpProxy", "agent", "fileServer", "lanproxy"];
|
||||||
if (!FEATURES.ENABLE_GUI_AGENT_SERVER) return keys;
|
if (!FEATURES.ENABLE_GUI_AGENT_SERVER) return keys;
|
||||||
@@ -392,6 +349,10 @@ function App() {
|
|||||||
const checkSetup = async () => {
|
const checkSetup = async () => {
|
||||||
try {
|
try {
|
||||||
const completed = await setupService.isSetupCompleted();
|
const completed = await setupService.isSetupCompleted();
|
||||||
|
const step1 = await setupService.getStep1Config();
|
||||||
|
setLoginStep1Config(step1);
|
||||||
|
const loggedIn = await checkIsLoggedIn();
|
||||||
|
setIsLoggedIn(loggedIn);
|
||||||
|
|
||||||
// 每次启动优先读取 quick init 配置
|
// 每次启动优先读取 quick init 配置
|
||||||
if (completed) {
|
if (completed) {
|
||||||
@@ -411,6 +372,7 @@ function App() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Failed to check setup status:", error);
|
log.error("Failed to check setup status:", error);
|
||||||
setIsSetupComplete(true);
|
setIsSetupComplete(true);
|
||||||
|
setIsLoggedIn(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
checkSetup();
|
checkSetup();
|
||||||
@@ -429,6 +391,7 @@ function App() {
|
|||||||
setUsername(
|
setUsername(
|
||||||
user.displayName || user.username || t("Claw.App.defaultUsername"),
|
user.displayName || user.username || t("Claw.App.defaultUsername"),
|
||||||
);
|
);
|
||||||
|
setUserEmail(user.email || user.phone || user.username || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载在线状态
|
// 加载在线状态
|
||||||
@@ -445,12 +408,16 @@ function App() {
|
|||||||
// ============================================
|
// ============================================
|
||||||
const handleAuthChange = useCallback(async () => {
|
const handleAuthChange = useCallback(async () => {
|
||||||
const user = await authService.getAuthUser();
|
const user = await authService.getAuthUser();
|
||||||
|
const loggedIn = await checkIsLoggedIn();
|
||||||
|
setIsLoggedIn(loggedIn);
|
||||||
if (user) {
|
if (user) {
|
||||||
setUsername(
|
setUsername(
|
||||||
user.displayName || user.username || t("Claw.App.defaultUsername"),
|
user.displayName || user.username || t("Claw.App.defaultUsername"),
|
||||||
);
|
);
|
||||||
|
setUserEmail(user.email || user.phone || user.username || "");
|
||||||
} else {
|
} else {
|
||||||
setUsername("");
|
setUsername("");
|
||||||
|
setUserEmail("");
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -459,6 +426,29 @@ function App() {
|
|||||||
loginStartedRef.current = true;
|
loginStartedRef.current = true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const ensureDefaultLoginConfig = useCallback(async () => {
|
||||||
|
const currentConfig = await setupService.getStep1Config();
|
||||||
|
const nextConfig: Step1Config = {
|
||||||
|
...DEFAULT_STEP1_CONFIG,
|
||||||
|
...currentConfig,
|
||||||
|
serverHost: currentConfig.serverHost || DEFAULT_STEP1_CONFIG.serverHost,
|
||||||
|
workspaceDir:
|
||||||
|
currentConfig.workspaceDir || DEFAULT_STEP1_CONFIG.workspaceDir,
|
||||||
|
};
|
||||||
|
await setupService.saveStep1Config(nextConfig);
|
||||||
|
setLoginStep1Config(nextConfig);
|
||||||
|
return nextConfig;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFullscreenLoginSuccess = useCallback(async () => {
|
||||||
|
await setupService.completeStep2();
|
||||||
|
await setupService.completeSetup();
|
||||||
|
setupJustCompleted.current = true;
|
||||||
|
setIsSetupComplete(true);
|
||||||
|
await handleAuthChange();
|
||||||
|
setAuthRefreshTrigger((v) => v + 1);
|
||||||
|
}, [handleAuthChange]);
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 主界面下必需依赖检查:仅当存在「未安装」或「错误」时进入依赖安装
|
// 主界面下必需依赖检查:仅当存在「未安装」或「错误」时进入依赖安装
|
||||||
// 版本以当前真实安装为准,outdated 不触发(用户可在依赖 Tab 手动升级)
|
// 版本以当前真实安装为准,outdated 不触发(用户可在依赖 Tab 手动升级)
|
||||||
@@ -561,7 +551,6 @@ function App() {
|
|||||||
]);
|
]);
|
||||||
const isGuiEnabled =
|
const isGuiEnabled =
|
||||||
FEATURES.ENABLE_GUI_AGENT_SERVER && (guiEnabledRes?.enabled ?? false);
|
FEATURES.ENABLE_GUI_AGENT_SERVER && (guiEnabledRes?.enabled ?? false);
|
||||||
setGuiMcpEnabled(isGuiEnabled);
|
|
||||||
items.push({
|
items.push({
|
||||||
key: "mcpProxy",
|
key: "mcpProxy",
|
||||||
label: t("Claw.Service.mcp"),
|
label: t("Claw.Service.mcp"),
|
||||||
@@ -614,10 +603,8 @@ function App() {
|
|||||||
error: lpStatus?.error,
|
error: lpStatus?.error,
|
||||||
});
|
});
|
||||||
setServices(items);
|
setServices(items);
|
||||||
setPollFailCount(0);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[App] pollServicesStatus failed:", error);
|
console.error("[App] pollServicesStatus failed:", error);
|
||||||
setPollFailCount((count) => count + 1);
|
|
||||||
} finally {
|
} finally {
|
||||||
setServicesLoading(false);
|
setServicesLoading(false);
|
||||||
}
|
}
|
||||||
@@ -803,57 +790,6 @@ function App() {
|
|||||||
getStartupServiceKeys,
|
getStartupServiceKeys,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// 根据服务状态计算 Agent 状态
|
|
||||||
// ============================================
|
|
||||||
// 根据服务状态计算 Agent 状态(对齐 Tauri 客户端逻辑)
|
|
||||||
useEffect(() => {
|
|
||||||
// 如果正在加载,保持当前状态不变(避免初始加载时的闪烁)
|
|
||||||
if (servicesLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusExpectedKeys.length === 0) {
|
|
||||||
setAgentStatus("idle");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serviceMap = new Map(services.map((s) => [s.key, s]));
|
|
||||||
const trackedServices = statusExpectedKeys.map((key) =>
|
|
||||||
serviceMap.get(key),
|
|
||||||
);
|
|
||||||
const runningCount = trackedServices.filter((s) => s?.running).length;
|
|
||||||
const totalCount = statusExpectedKeys.length;
|
|
||||||
const hasErrors = trackedServices.some((s) => !!s?.error);
|
|
||||||
const hasStartingServices = Array.from(startingServices).some((key) =>
|
|
||||||
statusExpectedKeys.includes(key),
|
|
||||||
);
|
|
||||||
const hasStaleServiceStatus = pollFailCount >= 2;
|
|
||||||
|
|
||||||
if (hasStaleServiceStatus) {
|
|
||||||
// 连续轮询失败时,避免继续展示可能过期的 running 状态。
|
|
||||||
setAgentStatus("busy");
|
|
||||||
} else if (hasErrors) {
|
|
||||||
setAgentStatus("error");
|
|
||||||
} else if (hasStartingServices) {
|
|
||||||
setAgentStatus("starting");
|
|
||||||
} else if (runningCount === totalCount && runningCount > 0) {
|
|
||||||
setAgentStatus("running");
|
|
||||||
} else if (runningCount > 0 && runningCount < totalCount) {
|
|
||||||
setAgentStatus("busy");
|
|
||||||
} else if (runningCount === 0) {
|
|
||||||
setAgentStatus("stopped");
|
|
||||||
} else {
|
|
||||||
setAgentStatus("idle");
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
services,
|
|
||||||
servicesLoading,
|
|
||||||
startingServices,
|
|
||||||
statusExpectedKeys,
|
|
||||||
pollFailCount,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 启动服务状态轮询
|
// 启动服务状态轮询
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSetupComplete !== true) return;
|
if (isSetupComplete !== true) return;
|
||||||
@@ -871,55 +807,6 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, [isSetupComplete]);
|
}, [isSetupComplete]);
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// 监听更新状态(header tag 展示)
|
|
||||||
// ============================================
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = (state: UpdateState) => {
|
|
||||||
if (state) setUpdateState(state);
|
|
||||||
};
|
|
||||||
window.electronAPI?.on("update:status", handler as any);
|
|
||||||
window.electronAPI?.app?.getUpdateState?.()?.then((state) => {
|
|
||||||
if (state) setUpdateState(state);
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
window.electronAPI?.off("update:status", handler as any);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isDownloading = updateState.status === "downloading";
|
|
||||||
const hasRealProgress = updateState.progress != null;
|
|
||||||
|
|
||||||
if (isDownloading && !hasRealProgress) {
|
|
||||||
setHeaderSimulatedPercent(0);
|
|
||||||
const increment =
|
|
||||||
(HEADER_SIMULATED_PROGRESS_CAP / HEADER_SIMULATED_DURATION_MS) *
|
|
||||||
HEADER_SIMULATED_PROGRESS_INTERVAL_MS;
|
|
||||||
const id = setInterval(() => {
|
|
||||||
setHeaderSimulatedPercent((prev) => {
|
|
||||||
const next = prev + increment;
|
|
||||||
return next >= HEADER_SIMULATED_PROGRESS_CAP
|
|
||||||
? HEADER_SIMULATED_PROGRESS_CAP
|
|
||||||
: next;
|
|
||||||
});
|
|
||||||
}, HEADER_SIMULATED_PROGRESS_INTERVAL_MS);
|
|
||||||
headerSimulatedIntervalRef.current = id;
|
|
||||||
return () => {
|
|
||||||
clearInterval(id);
|
|
||||||
headerSimulatedIntervalRef.current = null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDownloading || hasRealProgress) {
|
|
||||||
if (headerSimulatedIntervalRef.current) {
|
|
||||||
clearInterval(headerSimulatedIntervalRef.current);
|
|
||||||
headerSimulatedIntervalRef.current = null;
|
|
||||||
}
|
|
||||||
setHeaderSimulatedPercent(0);
|
|
||||||
}
|
|
||||||
}, [updateState.status, updateState.progress]);
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 监听托盘/菜单事件
|
// 监听托盘/菜单事件
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -1034,65 +921,103 @@ function App() {
|
|||||||
setIsSetupComplete(true);
|
setIsSetupComplete(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================
|
const stopServicesForLogout = useCallback(async () => {
|
||||||
// 状态 Badge
|
const toStop = services.filter((svc) => svc.running || !!svc.error);
|
||||||
// ============================================
|
for (const svc of toStop) {
|
||||||
const badge = STATUS_CONFIG[agentStatus] || STATUS_CONFIG.idle;
|
try {
|
||||||
|
if (svc.key === "agent") await window.electronAPI?.agent.destroy();
|
||||||
|
else if (svc.key === "fileServer")
|
||||||
|
await window.electronAPI?.fileServer.stop();
|
||||||
|
else if (svc.key === "lanproxy")
|
||||||
|
await window.electronAPI?.lanproxy.stop();
|
||||||
|
else if (svc.key === "mcpProxy") await window.electronAPI?.mcp.stop();
|
||||||
|
else if (svc.key === "guiServer")
|
||||||
|
await window.electronAPI?.guiServer?.stop();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[App] Failed to stop ${svc.label}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.electronAPI?.computerServer.stop().catch((e: unknown) => {
|
||||||
|
console.error("[App] Failed to stop computerServer:", e);
|
||||||
|
});
|
||||||
|
}, [services]);
|
||||||
|
|
||||||
|
const handleSidebarLogout = useCallback(() => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t("Claw.Client.logoutConfirm"),
|
||||||
|
content: t("Claw.Client.logoutConfirmDetail"),
|
||||||
|
okText: t("Claw.Client.logout"),
|
||||||
|
cancelText: t("Claw.Client.cancel"),
|
||||||
|
okButtonProps: { danger: true },
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await stopServicesForLogout();
|
||||||
|
await logout();
|
||||||
|
await handleAuthChange();
|
||||||
|
setIsLoggedIn(false);
|
||||||
|
setActiveTab("client");
|
||||||
|
setActiveNavKey("overview");
|
||||||
|
setServices([]);
|
||||||
|
setServicesLoading(false);
|
||||||
|
setStartingServices(new Set());
|
||||||
|
setOnlineStatus(false);
|
||||||
|
message.success(t("Claw.Auth.loggedOut"));
|
||||||
|
} catch {
|
||||||
|
message.error(t("Claw.Client.logoutFailed"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [handleAuthChange, stopServicesForLogout]);
|
||||||
|
|
||||||
|
const handleNavSelect = useCallback((key: NavKey) => {
|
||||||
|
setActiveNavKey(key);
|
||||||
|
|
||||||
|
if (key === "overview") {
|
||||||
|
setActiveTab("client");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "newTask") {
|
||||||
|
setSessionsAutoOpen(true);
|
||||||
|
setActiveTab("sessions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "aiChat") {
|
||||||
|
setActiveTab("sessions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "mcpLibrary") {
|
||||||
|
setActiveTab("mcp");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "toolIntegration" || key === "systemSettings") {
|
||||||
|
setActiveTab("settings");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveTab("client");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLegacySelect = useCallback((key: LegacyTabKey) => {
|
||||||
|
setActiveTab(key);
|
||||||
|
const navByLegacyTab: Partial<Record<LegacyTabKey, NavKey>> = {
|
||||||
|
client: "overview",
|
||||||
|
sessions: "aiChat",
|
||||||
|
mcp: "mcpLibrary",
|
||||||
|
settings: "systemSettings",
|
||||||
|
};
|
||||||
|
setActiveNavKey(navByLegacyTab[key] || "systemSettings");
|
||||||
|
}, []);
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 平台检测
|
// 平台检测
|
||||||
// ============================================
|
// ============================================
|
||||||
const isMacOS = navigator.platform.toUpperCase().includes("MAC");
|
const isMacOS = navigator.platform.toUpperCase().includes("MAC");
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// 菜单配置(对齐 Tauri 客户端)
|
|
||||||
// ============================================
|
|
||||||
const menuItems = useMemo(() => {
|
|
||||||
const items = [
|
|
||||||
{
|
|
||||||
key: "client",
|
|
||||||
icon: <DashboardOutlined />,
|
|
||||||
label: t("Claw.Menu.client"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "sessions",
|
|
||||||
icon: <TeamOutlined />,
|
|
||||||
label: t("Claw.Menu.session"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "mcp",
|
|
||||||
icon: <ApiOutlined />,
|
|
||||||
label: t("Claw.Menu.mcp"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "settings",
|
|
||||||
icon: <SettingOutlined />,
|
|
||||||
label: t("Claw.Menu.settings"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "dependencies",
|
|
||||||
icon: <FolderOutlined />,
|
|
||||||
label: t("Claw.Menu.dependencies"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
if (isMacOS) {
|
|
||||||
items.push({
|
|
||||||
key: "permissions",
|
|
||||||
icon: <SafetyOutlined />,
|
|
||||||
label: t("Claw.Menu.authorization"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
items.push(
|
|
||||||
{ key: "logs", icon: <FileTextOutlined />, label: t("Claw.Menu.logs") },
|
|
||||||
{
|
|
||||||
key: "about",
|
|
||||||
icon: <InfoCircleOutlined />,
|
|
||||||
label: t("Claw.Menu.about"),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return items;
|
|
||||||
}, [isMacOS, i18nLang]);
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// i18n Context value
|
// i18n Context value
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -1106,7 +1031,8 @@ function App() {
|
|||||||
// ============================================
|
// ============================================
|
||||||
if (
|
if (
|
||||||
isSetupComplete === null ||
|
isSetupComplete === null ||
|
||||||
(isSetupComplete && needsRequiredDepsReinstall === null)
|
isLoggedIn === null ||
|
||||||
|
(isLoggedIn && isSetupComplete && needsRequiredDepsReinstall === null)
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<I18nContext.Provider value={i18nContextValue}>
|
<I18nContext.Provider value={i18nContextValue}>
|
||||||
@@ -1120,6 +1046,20 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
return (
|
||||||
|
<I18nContext.Provider value={i18nContextValue}>
|
||||||
|
<ConfigProvider theme={currentTheme}>
|
||||||
|
<LoginPage
|
||||||
|
step1Config={loginStep1Config}
|
||||||
|
onLoginStart={ensureDefaultLoginConfig}
|
||||||
|
onLoginSuccess={handleFullscreenLoginSuccess}
|
||||||
|
/>
|
||||||
|
</ConfigProvider>
|
||||||
|
</I18nContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 渲染:初始化向导
|
// 渲染:初始化向导
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -1162,9 +1102,8 @@ function App() {
|
|||||||
value={{ themeMode, isDarkMode, setThemeMode: handleSetThemeMode }}
|
value={{ themeMode, isDarkMode, setThemeMode: handleSetThemeMode }}
|
||||||
>
|
>
|
||||||
<div className="app-container">
|
<div className="app-container">
|
||||||
{/* 顶部栏 */}
|
{webviewActions && (
|
||||||
<div className="app-header">
|
<div className="app-header">
|
||||||
{webviewActions ? (
|
|
||||||
<div className={styles.headerWebviewActions}>
|
<div className={styles.headerWebviewActions}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
@@ -1181,143 +1120,23 @@ function App() {
|
|||||||
{t("Claw.App.refresh")}
|
{t("Claw.App.refresh")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="app-header-logo">
|
|
||||||
<img
|
|
||||||
src="./32x32.png"
|
|
||||||
alt=""
|
|
||||||
style={{ width: 16, height: 16 }}
|
|
||||||
/>
|
|
||||||
<span className="app-header-title">{APP_DISPLAY_NAME}</span>
|
|
||||||
{updateState.status === "available" && (
|
|
||||||
<span
|
|
||||||
className="app-header-update-tag app-header-update-tag--available"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={async () => {
|
|
||||||
if (updateState.canAutoUpdate === false) {
|
|
||||||
await window.electronAPI?.app?.openReleasesPage?.();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
setUpdateState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
status: "downloading",
|
|
||||||
progress: undefined,
|
|
||||||
}));
|
|
||||||
const res =
|
|
||||||
await window.electronAPI?.app?.downloadUpdate?.();
|
|
||||||
if (!res || !res.success) {
|
|
||||||
message.error(
|
|
||||||
res?.error || t("Claw.About.downloadFailed"),
|
|
||||||
);
|
|
||||||
setUpdateState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
status: "available",
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
message.error(t("Claw.About.downloadFailed"));
|
|
||||||
setUpdateState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
status: "available",
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) =>
|
|
||||||
(e.key === "Enter" || e.key === " ") &&
|
|
||||||
(e.target as HTMLElement).click()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{updateState.canAutoUpdate === false
|
|
||||||
? t("Claw.App.UpdateTag.newVersion", {
|
|
||||||
version: updateState.version,
|
|
||||||
})
|
|
||||||
: t("Claw.App.UpdateTag.download", {
|
|
||||||
version: updateState.version,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{updateState.status === "downloading" && (
|
|
||||||
<span className="app-header-update-tag app-header-update-tag--downloading">
|
|
||||||
{t("Claw.App.UpdateTag.downloading", {
|
|
||||||
percent: Math.round(
|
|
||||||
updateState.progress?.percent ??
|
|
||||||
headerSimulatedPercent,
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{updateState.status === "downloaded" && (
|
|
||||||
<span
|
|
||||||
className="app-header-update-tag app-header-update-tag--install"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
const res =
|
|
||||||
await window.electronAPI?.app?.installUpdate?.();
|
|
||||||
if (res && !res.success) {
|
|
||||||
message.error(
|
|
||||||
res.error || t("Claw.About.installFailed"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
message.error(t("Claw.About.installFailed"));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) =>
|
|
||||||
(e.key === "Enter" || e.key === " ") &&
|
|
||||||
(e.target as HTMLElement).click()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("Claw.About.installUpdate")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.headerRight}>
|
|
||||||
{username && (
|
|
||||||
<span className={styles.username}>{username}</span>
|
|
||||||
)}
|
|
||||||
<Badge
|
|
||||||
status={badge.status}
|
|
||||||
className={
|
|
||||||
agentStatus === "idle" || agentStatus === "busy"
|
|
||||||
? styles.badgeIdle
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
text={
|
|
||||||
<span className={styles.badgeText}>{t(badge.textKey)}</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 主体部分 */}
|
{/* 主体部分 */}
|
||||||
<div className="app-body">
|
<div className="app-body">
|
||||||
{/* 左侧边栏 (hidden when webview is active) */}
|
{/* 左侧边栏 (hidden when webview is active) */}
|
||||||
{!webviewActions && (
|
{!webviewActions && (
|
||||||
<div
|
<AppSidebar
|
||||||
className={
|
activeNavKey={activeNavKey}
|
||||||
// 英文菜单文案通常更长,侧边栏适当加宽以减少截断;其他语言保持默认宽度
|
username={username}
|
||||||
i18nLang.toLowerCase().startsWith("en")
|
email={userEmail}
|
||||||
? "app-sider app-sider-en"
|
onlineStatus={onlineStatus}
|
||||||
: "app-sider"
|
isMacOS={isMacOS}
|
||||||
}
|
onNavSelect={handleNavSelect}
|
||||||
>
|
onLegacySelect={handleLegacySelect}
|
||||||
<Menu
|
onLogout={handleSidebarLogout}
|
||||||
mode="inline"
|
|
||||||
inlineIndent={0}
|
|
||||||
selectedKeys={[activeTab]}
|
|
||||||
items={menuItems.map((item) => ({
|
|
||||||
key: item.key,
|
|
||||||
icon: item.icon,
|
|
||||||
label: item.label,
|
|
||||||
onClick: () => setActiveTab(item.key as TabKey),
|
|
||||||
}))}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 主内容区:flex 子撑满,便于日志等页占满高度 */}
|
{/* 主内容区:flex 子撑满,便于日志等页占满高度 */}
|
||||||
@@ -1342,6 +1161,9 @@ function App() {
|
|||||||
onNavigate={(tab) => {
|
onNavigate={(tab) => {
|
||||||
if (tab === "sessions") setSessionsAutoOpen(true);
|
if (tab === "sessions") setSessionsAutoOpen(true);
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
|
setActiveNavKey(
|
||||||
|
tab === "sessions" ? "aiChat" : "overview",
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
services={services}
|
services={services}
|
||||||
servicesLoading={servicesLoading}
|
servicesLoading={servicesLoading}
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Popover } from "antd";
|
||||||
|
import {
|
||||||
|
AppstoreOutlined,
|
||||||
|
ApiOutlined,
|
||||||
|
BookOutlined,
|
||||||
|
BuildOutlined,
|
||||||
|
CommentOutlined,
|
||||||
|
DatabaseOutlined,
|
||||||
|
DeploymentUnitOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
FolderOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
LogoutOutlined,
|
||||||
|
MenuFoldOutlined,
|
||||||
|
PlusCircleOutlined,
|
||||||
|
ProjectOutlined,
|
||||||
|
SafetyOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
ThunderboltOutlined,
|
||||||
|
ToolOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { APP_DISPLAY_NAME } from "@shared/constants";
|
||||||
|
import styles from "../../styles/components/AppSidebar.module.css";
|
||||||
|
|
||||||
|
export type NavKey =
|
||||||
|
| "overview"
|
||||||
|
| "newTask"
|
||||||
|
| "automation"
|
||||||
|
| "aiChat"
|
||||||
|
| "knowledge"
|
||||||
|
| "document"
|
||||||
|
| "dataFetch"
|
||||||
|
| "skills"
|
||||||
|
| "mcpLibrary"
|
||||||
|
| "workflow"
|
||||||
|
| "plugins"
|
||||||
|
| "toolIntegration"
|
||||||
|
| "systemSettings";
|
||||||
|
|
||||||
|
export type LegacyTabKey =
|
||||||
|
| "client"
|
||||||
|
| "sessions"
|
||||||
|
| "mcp"
|
||||||
|
| "settings"
|
||||||
|
| "dependencies"
|
||||||
|
| "permissions"
|
||||||
|
| "logs"
|
||||||
|
| "about";
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
key: NavKey;
|
||||||
|
label: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
badge?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LegacyItem {
|
||||||
|
key: LegacyTabKey;
|
||||||
|
label: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppSidebarProps {
|
||||||
|
activeNavKey: NavKey;
|
||||||
|
username: string;
|
||||||
|
email?: string;
|
||||||
|
onlineStatus: boolean | null;
|
||||||
|
isMacOS: boolean;
|
||||||
|
onNavSelect: (key: NavKey) => void;
|
||||||
|
onLegacySelect: (key: LegacyTabKey) => void;
|
||||||
|
onLogout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceItems: NavItem[] = [
|
||||||
|
{ key: "overview", label: "概览", icon: <AppstoreOutlined /> },
|
||||||
|
{
|
||||||
|
key: "newTask",
|
||||||
|
label: "新建任务",
|
||||||
|
icon: <PlusCircleOutlined />,
|
||||||
|
badge: "3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "automation",
|
||||||
|
label: "自动化",
|
||||||
|
icon: <ThunderboltOutlined />,
|
||||||
|
badge: "运行中",
|
||||||
|
},
|
||||||
|
{ key: "aiChat", label: "AI 对话", icon: <CommentOutlined /> },
|
||||||
|
{ key: "knowledge", label: "知识库", icon: <BookOutlined /> },
|
||||||
|
{ key: "document", label: "文档编辑", icon: <FileTextOutlined /> },
|
||||||
|
{ key: "dataFetch", label: "智能取数", icon: <DatabaseOutlined /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
const marketItems: NavItem[] = [
|
||||||
|
{ key: "skills", label: "技能库", icon: <BuildOutlined />, badge: "获取" },
|
||||||
|
{ key: "mcpLibrary", label: "MCP 库", icon: <ApiOutlined />, badge: "获取" },
|
||||||
|
{
|
||||||
|
key: "workflow",
|
||||||
|
label: "工作流",
|
||||||
|
icon: <ProjectOutlined />,
|
||||||
|
badge: "安装",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "plugins",
|
||||||
|
label: "插件",
|
||||||
|
icon: <DeploymentUnitOutlined />,
|
||||||
|
badge: "安装",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const settingItems: NavItem[] = [
|
||||||
|
{ key: "toolIntegration", label: "工具集成", icon: <ToolOutlined /> },
|
||||||
|
{ key: "systemSettings", label: "系统设置", icon: <SettingOutlined /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
function getLegacyItems(isMacOS: boolean): LegacyItem[] {
|
||||||
|
const items: LegacyItem[] = [
|
||||||
|
{ key: "client", label: "客户端", icon: <AppstoreOutlined /> },
|
||||||
|
{ key: "sessions", label: "会话", icon: <TeamOutlined /> },
|
||||||
|
{ key: "mcp", label: "MCP", icon: <ApiOutlined /> },
|
||||||
|
{ key: "settings", label: "设置", icon: <SettingOutlined /> },
|
||||||
|
{ key: "dependencies", label: "依赖", icon: <FolderOutlined /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isMacOS) {
|
||||||
|
items.push({ key: "permissions", label: "授权", icon: <SafetyOutlined /> });
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
{ key: "logs", label: "日志", icon: <FileTextOutlined /> },
|
||||||
|
{ key: "about", label: "关于", icon: <InfoCircleOutlined /> },
|
||||||
|
);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitial(name: string): string {
|
||||||
|
const trimmed = name.trim();
|
||||||
|
if (!trimmed) return "陇";
|
||||||
|
return trimmed.slice(0, 1).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNavItem(
|
||||||
|
item: NavItem,
|
||||||
|
activeNavKey: NavKey,
|
||||||
|
onNavSelect: (key: NavKey) => void,
|
||||||
|
) {
|
||||||
|
const active = activeNavKey === item.key;
|
||||||
|
const isStatusBadge = item.badge === "运行中";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.key}
|
||||||
|
type="button"
|
||||||
|
className={`${styles.navItem} ${active ? styles.navItemActive : ""}`}
|
||||||
|
onClick={() => onNavSelect(item.key)}
|
||||||
|
>
|
||||||
|
<span className={styles.navIcon}>{item.icon}</span>
|
||||||
|
<span className={styles.navLabel}>{item.label}</span>
|
||||||
|
{item.badge && (
|
||||||
|
<span
|
||||||
|
className={`${styles.navBadge} ${isStatusBadge ? styles.navBadgeSuccess : ""}`}
|
||||||
|
>
|
||||||
|
{item.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AppSidebar({
|
||||||
|
activeNavKey,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
onlineStatus,
|
||||||
|
isMacOS,
|
||||||
|
onNavSelect,
|
||||||
|
onLegacySelect,
|
||||||
|
onLogout,
|
||||||
|
}: AppSidebarProps) {
|
||||||
|
const displayName = username || "陈星宇";
|
||||||
|
const displayEmail = email || "xingyu@feitian.com";
|
||||||
|
const legacyItems = getLegacyItems(isMacOS);
|
||||||
|
const online = onlineStatus !== false;
|
||||||
|
|
||||||
|
const historyMenu = (
|
||||||
|
<div className={styles.historyMenu}>
|
||||||
|
<div className={styles.historyTitle}>历史菜单</div>
|
||||||
|
{legacyItems.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.key}
|
||||||
|
type="button"
|
||||||
|
className={styles.historyItem}
|
||||||
|
onClick={() => onLegacySelect(item.key)}
|
||||||
|
>
|
||||||
|
<span className={styles.historyIcon}>{item.icon}</span>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className={styles.sidebar}>
|
||||||
|
<div className={styles.brand}>
|
||||||
|
<img className={styles.brandLogo} src="./icon.png" alt="" />
|
||||||
|
<div className={styles.brandText}>
|
||||||
|
<div className={styles.brandName}>{APP_DISPLAY_NAME}</div>
|
||||||
|
<div className={styles.brandSubTitle}>DIGITAL EMPLOYEE</div>
|
||||||
|
</div>
|
||||||
|
<Popover
|
||||||
|
content={historyMenu}
|
||||||
|
trigger="click"
|
||||||
|
placement="rightTop"
|
||||||
|
overlayClassName={styles.historyPopover}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.historyTrigger}
|
||||||
|
title="导航目录"
|
||||||
|
>
|
||||||
|
<MenuFoldOutlined />
|
||||||
|
</button>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className={styles.nav}>
|
||||||
|
<section className={styles.navSection}>
|
||||||
|
<div className={styles.sectionTitle}>工作台</div>
|
||||||
|
{workspaceItems.map((item) =>
|
||||||
|
renderNavItem(item, activeNavKey, onNavSelect),
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.navSection}>
|
||||||
|
<div className={styles.sectionTitle}>扩展市场</div>
|
||||||
|
{marketItems.map((item) =>
|
||||||
|
renderNavItem(item, activeNavKey, onNavSelect),
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.navSection}>
|
||||||
|
<div className={styles.sectionTitle}>设置</div>
|
||||||
|
{settingItems.map((item) =>
|
||||||
|
renderNavItem(item, activeNavKey, onNavSelect),
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<div className={styles.userCard}>
|
||||||
|
<div className={styles.avatar}>{getInitial(displayName)}</div>
|
||||||
|
<div className={styles.userMeta}>
|
||||||
|
<div className={styles.userName}>{displayName}</div>
|
||||||
|
<div className={styles.userEmail}>{displayEmail}</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`${styles.onlineDot} ${online ? styles.onlineDotActive : ""}`}
|
||||||
|
aria-label={online ? "在线" : "离线"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.logoutButton}
|
||||||
|
onClick={onLogout}
|
||||||
|
>
|
||||||
|
<LogoutOutlined />
|
||||||
|
<span>退出登录</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,7 +27,6 @@ import {
|
|||||||
UserOutlined,
|
UserOutlined,
|
||||||
LockOutlined,
|
LockOutlined,
|
||||||
GlobalOutlined,
|
GlobalOutlined,
|
||||||
LogoutOutlined,
|
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
PoweroffOutlined,
|
PoweroffOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
@@ -43,7 +42,6 @@ import {
|
|||||||
import { QRCodeSVG } from "qrcode.react";
|
import { QRCodeSVG } from "qrcode.react";
|
||||||
import {
|
import {
|
||||||
loginAndRegister,
|
loginAndRegister,
|
||||||
logout,
|
|
||||||
getCurrentAuth,
|
getCurrentAuth,
|
||||||
syncConfigToServer,
|
syncConfigToServer,
|
||||||
} from "../../services/core/auth";
|
} from "../../services/core/auth";
|
||||||
@@ -235,65 +233,6 @@ function ClientPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: t("Claw.Client.logoutConfirm"),
|
|
||||||
content: t("Claw.Client.logoutConfirmDetail"),
|
|
||||||
okText: t("Claw.Client.logout"),
|
|
||||||
cancelText: t("Claw.Client.cancel"),
|
|
||||||
okButtonProps: { danger: true },
|
|
||||||
onOk: async () => {
|
|
||||||
try {
|
|
||||||
// 停止所有运行中或处于 error 状态的服务(error 状态进程可能仍驻留)
|
|
||||||
const toStop = services.filter((s) => s.running || !!s.error);
|
|
||||||
for (const svc of toStop) {
|
|
||||||
try {
|
|
||||||
if (svc.key === "agent")
|
|
||||||
await window.electronAPI?.agent.destroy();
|
|
||||||
else if (svc.key === "fileServer")
|
|
||||||
await window.electronAPI?.fileServer.stop();
|
|
||||||
else if (svc.key === "lanproxy")
|
|
||||||
await window.electronAPI?.lanproxy.stop();
|
|
||||||
else if (svc.key === "mcpProxy")
|
|
||||||
await window.electronAPI?.mcp.stop();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`[ClientPage] Failed to stop ${svc.label}:`, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// computerServer 不在 services 列表中,需单独停止,避免进程残留导致端口冲突
|
|
||||||
await window.electronAPI?.computerServer
|
|
||||||
.stop()
|
|
||||||
.catch((e: unknown) => {
|
|
||||||
console.error("[ClientPage] Failed to stop computerServer:", e);
|
|
||||||
});
|
|
||||||
|
|
||||||
await logout();
|
|
||||||
// 退出登录后,默认回填上一次“服务域名”到登录输入框,减少用户重复输入。
|
|
||||||
// 回填优先级说明(从高到低):
|
|
||||||
// 1) displayDomainFallback:本页面内最近一次明确业务域名(用户输入/认证状态同步得到);
|
|
||||||
// 2) authState.domain:当前登录态里记录的业务域名;
|
|
||||||
// 3) step1_config.serverHost:持久化配置兜底(首次进入或刷新后也可恢复)。
|
|
||||||
// 说明:这里回填的是“业务域名”,仅用于登录输入体验,不改变 reg/lanproxy 的代理地址逻辑。
|
|
||||||
const step1 = (await window.electronAPI?.settings.get(
|
|
||||||
"step1_config",
|
|
||||||
)) as { serverHost?: string } | null;
|
|
||||||
const lastDomain =
|
|
||||||
displayDomainFallback ||
|
|
||||||
authState.domain ||
|
|
||||||
step1?.serverHost ||
|
|
||||||
"";
|
|
||||||
setLoginDomain(lastDomain);
|
|
||||||
setDisplayDomainFallback(lastDomain);
|
|
||||||
|
|
||||||
setAuthState({ isLoggedIn: false, username: null, domain: null });
|
|
||||||
onAuthChange?.();
|
|
||||||
} catch {
|
|
||||||
message.error(t("Claw.Client.logoutFailed"));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRedirectUrl = useCallback(() => {
|
const getRedirectUrl = useCallback(() => {
|
||||||
if (!authState.domain || !authState.userId) return "";
|
if (!authState.domain || !authState.userId) return "";
|
||||||
return buildRedirectUrl(authState.domain, authState.userId);
|
return buildRedirectUrl(authState.domain, authState.userId);
|
||||||
@@ -676,15 +615,6 @@ function ClientPage({
|
|||||||
>
|
>
|
||||||
{t("Claw.Client.qrCode")}
|
{t("Claw.Client.qrCode")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<LogoutOutlined />}
|
|
||||||
onClick={handleLogout}
|
|
||||||
size="small"
|
|
||||||
danger
|
|
||||||
>
|
|
||||||
{t("Claw.Client.logout")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,306 @@
|
|||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(360px, 36.1vw) 1fr;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f4f4f1;
|
||||||
|
color: #171412;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandPanel {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #1a1714;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandContent {
|
||||||
|
width: 280px;
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandIcon {
|
||||||
|
position: relative;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
margin: 0 auto 24px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: linear-gradient(135deg, #00a960 0%, #007d73 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandIconHead {
|
||||||
|
position: absolute;
|
||||||
|
top: 25px;
|
||||||
|
left: 32px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandIconBody {
|
||||||
|
position: absolute;
|
||||||
|
top: 38px;
|
||||||
|
left: 24px;
|
||||||
|
width: 24px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-radius: 14px 14px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandTitle {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 29px;
|
||||||
|
line-height: 40px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandSubtitle {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: 8px;
|
||||||
|
text-indent: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandSlogan {
|
||||||
|
margin-top: 24px;
|
||||||
|
color: rgba(255, 255, 255, 0.74);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 22px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandCopyright {
|
||||||
|
margin-top: 24px;
|
||||||
|
color: rgba(255, 255, 255, 0.38);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginPanel {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
position: relative;
|
||||||
|
width: 420px;
|
||||||
|
height: 636px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #ededeb;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 18px 40px rgba(26, 23, 20, 0.08);
|
||||||
|
padding: 46px 48px 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
color: #171412;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 40px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin: 20px 0 0;
|
||||||
|
color: #8c8883;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #171412;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input,
|
||||||
|
.input:global(.ant-input-affix-wrapper) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:global(.ant-input-affix-wrapper) {
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 13px;
|
||||||
|
border: 1px solid #d7d3ce;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fdfdfb;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:global(.ant-input-affix-wrapper) :global(.ant-input) {
|
||||||
|
height: 46px;
|
||||||
|
color: #171412;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:global(.ant-input-affix-wrapper) :global(.ant-input-prefix) {
|
||||||
|
margin-inline-end: 13px;
|
||||||
|
color: #8c8883;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:global(.ant-input-affix-wrapper) :global(.ant-input-suffix) {
|
||||||
|
color: #8c8883;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:global(.ant-input-affix-wrapper) :global(.ant-input-clear-icon) {
|
||||||
|
color: #c8c5bf;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:global(.ant-input-affix-wrapper) :global(.ant-input::placeholder),
|
||||||
|
.input:global(.ant-input-affix-wrapper)
|
||||||
|
:global(.ant-input-password input::placeholder) {
|
||||||
|
color: #b7b3ad;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:global(.ant-input-affix-wrapper-focused),
|
||||||
|
.input:global(.ant-input-affix-wrapper:hover) {
|
||||||
|
border-color: #00846d;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
height: 0;
|
||||||
|
margin-top: -16px;
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginButton {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
margin-top: 2px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(90deg, #00a455 0%, #007d73 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0;
|
||||||
|
box-shadow: 0 9px 16px rgba(0, 160, 88, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginButton:hover,
|
||||||
|
.loginButton:focus {
|
||||||
|
background: linear-gradient(90deg, #00ae5d 0%, #00887c 100%) !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginButton:active {
|
||||||
|
background: linear-gradient(90deg, #00964e 0%, #006e66 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
width: 100%;
|
||||||
|
margin: 24px 0 0;
|
||||||
|
color: #006f68;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hintIcon {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
border: 1.5px solid currentColor;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hintArrow {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
margin-top: 25px;
|
||||||
|
background: #e7e1dc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 25px;
|
||||||
|
color: #aaa5a0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.page {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandPanel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginPanel {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 720px), (max-width: 520px) {
|
||||||
|
.loginPanel {
|
||||||
|
align-items: flex-start;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: min(420px, calc(100vw - 48px));
|
||||||
|
height: auto;
|
||||||
|
min-height: 636px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import { Button, Input, message } from "antd";
|
||||||
|
import {
|
||||||
|
GlobalOutlined,
|
||||||
|
LockOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import type { Step1Config } from "../../services/core/setup";
|
||||||
|
import {
|
||||||
|
getAuthErrorMessage,
|
||||||
|
loginAndRegister,
|
||||||
|
} from "../../services/core/auth";
|
||||||
|
import { t } from "../../services/core/i18n";
|
||||||
|
import styles from "./LoginPage.module.css";
|
||||||
|
|
||||||
|
interface LoginPageProps {
|
||||||
|
step1Config: Step1Config;
|
||||||
|
initialUsername?: string;
|
||||||
|
onLoginStart?: () => Promise<void>;
|
||||||
|
onLoginSuccess: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoginPage({
|
||||||
|
step1Config,
|
||||||
|
initialUsername = "",
|
||||||
|
onLoginStart,
|
||||||
|
onLoginSuccess,
|
||||||
|
}: LoginPageProps) {
|
||||||
|
const [domain, setDomain] = useState(step1Config.serverHost || "");
|
||||||
|
const [username, setUsername] = useState(initialUsername);
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [retryCooldown, setRetryCooldown] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDomain(step1Config.serverHost || "");
|
||||||
|
}, [step1Config.serverHost]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setUsername(initialUsername);
|
||||||
|
}, [initialUsername]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (retryCooldown <= 0) return;
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
setRetryCooldown((prev) => Math.max(0, prev - 1));
|
||||||
|
}, 1000);
|
||||||
|
return () => window.clearInterval(timer);
|
||||||
|
}, [retryCooldown]);
|
||||||
|
|
||||||
|
const loginButtonText = useMemo(() => {
|
||||||
|
if (retryCooldown > 0) {
|
||||||
|
return t("Claw.Setup.login.pleaseWait", { seconds: retryCooldown });
|
||||||
|
}
|
||||||
|
return t("Claw.Setup.login.loginButton");
|
||||||
|
}, [retryCooldown]);
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
const normalizedDomain = domain.trim();
|
||||||
|
const normalizedUsername = username.trim();
|
||||||
|
if (!normalizedUsername || !password) {
|
||||||
|
message.warning(t("Claw.Setup.login.accountAndCodeRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!normalizedDomain) {
|
||||||
|
message.warning(t("Claw.Setup.login.domainRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
await onLoginStart?.();
|
||||||
|
await loginAndRegister(normalizedUsername, password, {
|
||||||
|
suppressToast: true,
|
||||||
|
domain: normalizedDomain,
|
||||||
|
});
|
||||||
|
await onLoginSuccess();
|
||||||
|
} catch (loginError: any) {
|
||||||
|
const errorMessage = getAuthErrorMessage(loginError);
|
||||||
|
message.error(errorMessage);
|
||||||
|
setError(errorMessage);
|
||||||
|
setPassword("");
|
||||||
|
setRetryCooldown(3);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.page}>
|
||||||
|
<aside className={styles.brandPanel}>
|
||||||
|
<div className={styles.brandContent}>
|
||||||
|
<div className={styles.brandIcon} aria-hidden="true">
|
||||||
|
<span className={styles.brandIconHead} />
|
||||||
|
<span className={styles.brandIconBody} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.brandTitle}>陇电数字员工</div>
|
||||||
|
<div className={styles.brandSubtitle}>DIGITAL EMPLOYEE</div>
|
||||||
|
<div className={styles.brandSlogan}>智能协同,高效工作</div>
|
||||||
|
<div className={styles.brandCopyright}>© 2026 陇电数字员工</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main className={styles.loginPanel}>
|
||||||
|
<section className={styles.card}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<h1 className={styles.title}>欢迎登录</h1>
|
||||||
|
<p className={styles.subtitle}>请输入您的认证信息以继续</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.form}>
|
||||||
|
<label className={styles.field}>
|
||||||
|
<span className={styles.label}>认证地址</span>
|
||||||
|
<Input
|
||||||
|
className={styles.input}
|
||||||
|
prefix={<GlobalOutlined />}
|
||||||
|
value={domain}
|
||||||
|
onChange={(event) => setDomain(event.target.value)}
|
||||||
|
placeholder="请输入认证地址"
|
||||||
|
autoComplete="off"
|
||||||
|
allowClear
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className={styles.field}>
|
||||||
|
<span className={styles.label}>用户名</span>
|
||||||
|
<Input
|
||||||
|
className={styles.input}
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
value={username}
|
||||||
|
onChange={(event) => setUsername(event.target.value)}
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className={styles.field}>
|
||||||
|
<span className={styles.label}>密码</span>
|
||||||
|
<Input.Password
|
||||||
|
className={styles.input}
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
value={password}
|
||||||
|
onChange={(event) => setPassword(event.target.value)}
|
||||||
|
placeholder="请输入密码"
|
||||||
|
autoComplete="current-password"
|
||||||
|
onPressEnter={handleLogin}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className={styles.error}>{error}</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={styles.loginButton}
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
loading={loading}
|
||||||
|
disabled={retryCooldown > 0}
|
||||||
|
onClick={handleLogin}
|
||||||
|
>
|
||||||
|
{loginButtonText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.hint}>
|
||||||
|
<span className={styles.hintIcon} />
|
||||||
|
<span>统一权限登录</span>
|
||||||
|
<span className={styles.hintArrow}>›</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.divider} />
|
||||||
|
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<span>需要帮助?</span>
|
||||||
|
<span>v2.0.1</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
@@ -33,10 +33,7 @@ import {
|
|||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
RobotOutlined,
|
|
||||||
FolderOpenOutlined,
|
FolderOpenOutlined,
|
||||||
LockOutlined,
|
|
||||||
GlobalOutlined,
|
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import {
|
import {
|
||||||
setupService,
|
setupService,
|
||||||
@@ -46,9 +43,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
loginAndRegister,
|
loginAndRegister,
|
||||||
normalizeServerHost,
|
normalizeServerHost,
|
||||||
isLoggedIn as checkIsLoggedIn,
|
|
||||||
getCurrentAuth,
|
getCurrentAuth,
|
||||||
getAuthErrorMessage,
|
|
||||||
logout,
|
logout,
|
||||||
} from "../../services/core/auth";
|
} from "../../services/core/auth";
|
||||||
import { AUTH_KEYS } from "@shared/constants";
|
import { AUTH_KEYS } from "@shared/constants";
|
||||||
@@ -56,6 +51,7 @@ import type { QuickInitConfig } from "@shared/types/quickInit";
|
|||||||
import SetupDependencies, {
|
import SetupDependencies, {
|
||||||
type MockDependenciesApi,
|
type MockDependenciesApi,
|
||||||
} from "./SetupDependencies";
|
} from "./SetupDependencies";
|
||||||
|
import LoginPage from "./LoginPage";
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -94,19 +90,15 @@ function SetupWizard({
|
|||||||
const [dependenciesReady, setDependenciesReady] = useState<boolean | null>(
|
const [dependenciesReady, setDependenciesReady] = useState<boolean | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
const [currentStep, setCurrentStep] = useState(2);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [step1Config, setStep1Config] =
|
const [step1Config, setStep1Config] =
|
||||||
useState<Step1Config>(DEFAULT_STEP1_CONFIG);
|
useState<Step1Config>(DEFAULT_STEP1_CONFIG);
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [completed, setCompleted] = useState(false);
|
const [completed, setCompleted] = useState(false);
|
||||||
const [domain, setDomain] = useState("");
|
const [domain, setDomain] = useState("");
|
||||||
const [loginLoading, setLoginLoading] = useState(false);
|
|
||||||
const [loginError, setLoginError] = useState("");
|
|
||||||
const [retryCooldown, setRetryCooldown] = useState(0);
|
|
||||||
const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false);
|
const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false);
|
||||||
const [checkingAuth, setCheckingAuth] = useState(false);
|
const [checkingAuth, setCheckingAuth] = useState(true);
|
||||||
const [quickIniting, setQuickIniting] = useState(false);
|
const [quickIniting, setQuickIniting] = useState(false);
|
||||||
const quickInitAttempted = useRef(false);
|
const quickInitAttempted = useRef(false);
|
||||||
|
|
||||||
@@ -117,14 +109,14 @@ function SetupWizard({
|
|||||||
if (skipDependencyCheck) {
|
if (skipDependencyCheck) {
|
||||||
setDependenciesReady(true);
|
setDependenciesReady(true);
|
||||||
const state = await setupService.getSetupState();
|
const state = await setupService.getSetupState();
|
||||||
if (state.completed) {
|
|
||||||
setCompleted(true);
|
|
||||||
onCompleteRef.current();
|
|
||||||
} else {
|
|
||||||
setCurrentStep(state.step1Completed ? 2 : 1);
|
|
||||||
}
|
|
||||||
const config = await setupService.getStep1Config();
|
const config = await setupService.getStep1Config();
|
||||||
setStep1Config(config);
|
setStep1Config(config);
|
||||||
|
if (state.completed) {
|
||||||
|
setCompleted(true);
|
||||||
|
onComplete();
|
||||||
|
} else {
|
||||||
|
setCurrentStep(2);
|
||||||
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -173,7 +165,7 @@ function SetupWizard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setCurrentStep(state.step1Completed ? 2 : 1);
|
setCurrentStep(2);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 有依赖缺失,进入安装流程
|
// 有依赖缺失,进入安装流程
|
||||||
@@ -203,6 +195,7 @@ function SetupWizard({
|
|||||||
try {
|
try {
|
||||||
// 1. 保存 step1 配置
|
// 1. 保存 step1 配置
|
||||||
const step1: Step1Config = {
|
const step1: Step1Config = {
|
||||||
|
...DEFAULT_STEP1_CONFIG,
|
||||||
serverHost: normalizeServerHost(config.serverHost),
|
serverHost: normalizeServerHost(config.serverHost),
|
||||||
agentPort: config.agentPort,
|
agentPort: config.agentPort,
|
||||||
fileServerPort: config.fileServerPort,
|
fileServerPort: config.fileServerPort,
|
||||||
@@ -247,9 +240,7 @@ function SetupWizard({
|
|||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
setQuickIniting(false);
|
setQuickIniting(false);
|
||||||
// step1 可能已保存,从 step1 或 step2 继续
|
setCurrentStep(2);
|
||||||
const state = await setupService.getSetupState();
|
|
||||||
setCurrentStep(state.step1Completed ? 2 : 1);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onComplete],
|
[onComplete],
|
||||||
@@ -272,9 +263,23 @@ function SetupWizard({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentStep(1);
|
setCurrentStep(2);
|
||||||
}, [performQuickInit]);
|
}, [performQuickInit]);
|
||||||
|
|
||||||
|
const ensureDefaultStep1Config = useCallback(async () => {
|
||||||
|
const currentConfig = await setupService.getStep1Config();
|
||||||
|
const nextConfig: Step1Config = {
|
||||||
|
...DEFAULT_STEP1_CONFIG,
|
||||||
|
...currentConfig,
|
||||||
|
serverHost: currentConfig.serverHost || DEFAULT_STEP1_CONFIG.serverHost,
|
||||||
|
workspaceDir:
|
||||||
|
currentConfig.workspaceDir || DEFAULT_STEP1_CONFIG.workspaceDir,
|
||||||
|
};
|
||||||
|
await setupService.saveStep1Config(nextConfig);
|
||||||
|
setStep1Config(nextConfig);
|
||||||
|
return nextConfig;
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Check login status when entering step 2
|
// Check login status when entering step 2
|
||||||
const checkLoginStatus = useCallback(async () => {
|
const checkLoginStatus = useCallback(async () => {
|
||||||
setCheckingAuth(true);
|
setCheckingAuth(true);
|
||||||
@@ -300,15 +305,6 @@ function SetupWizard({
|
|||||||
}
|
}
|
||||||
}, [currentStep, checkLoginStatus]);
|
}, [currentStep, checkLoginStatus]);
|
||||||
|
|
||||||
// Retry cooldown timer
|
|
||||||
useEffect(() => {
|
|
||||||
if (retryCooldown <= 0) return;
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
setRetryCooldown((prev) => Math.max(0, prev - 1));
|
|
||||||
}, 1000);
|
|
||||||
return () => clearInterval(timer);
|
|
||||||
}, [retryCooldown]);
|
|
||||||
|
|
||||||
const handleStep1Submit = async () => {
|
const handleStep1Submit = async () => {
|
||||||
if (!step1Config.fileServerPort) {
|
if (!step1Config.fileServerPort) {
|
||||||
message.warning(t("Claw.Setup.basicConfig.fileServerPortRequired"));
|
message.warning(t("Claw.Setup.basicConfig.fileServerPortRequired"));
|
||||||
@@ -327,49 +323,13 @@ function SetupWizard({
|
|||||||
await setupService.saveStep1Config(step1Config);
|
await setupService.saveStep1Config(step1Config);
|
||||||
setCurrentStep(2);
|
setCurrentStep(2);
|
||||||
message.success(t("Claw.Setup.basicConfig.saved"));
|
message.success(t("Claw.Setup.basicConfig.saved"));
|
||||||
} catch (error) {
|
} catch {
|
||||||
message.error(t("Claw.Setup.basicConfig.saveFailed"));
|
message.error(t("Claw.Setup.basicConfig.saveFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogin = async () => {
|
|
||||||
if (!username || !password) {
|
|
||||||
message.warning(t("Claw.Setup.login.accountAndCodeRequired"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginDomain = domain || step1Config.serverHost;
|
|
||||||
if (!loginDomain) {
|
|
||||||
message.warning(t("Claw.Setup.login.domainRequired"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoginLoading(true);
|
|
||||||
setLoginError("");
|
|
||||||
try {
|
|
||||||
await loginAndRegister(username, password, {
|
|
||||||
suppressToast: true,
|
|
||||||
domain: loginDomain,
|
|
||||||
});
|
|
||||||
setLoginError("");
|
|
||||||
await setupService.completeStep2();
|
|
||||||
await setupService.completeSetup();
|
|
||||||
setCompleted(true);
|
|
||||||
message.success(t("Claw.Setup.login.success"));
|
|
||||||
setTimeout(() => onComplete(), 2000);
|
|
||||||
} catch (error: any) {
|
|
||||||
const errorMessage = getAuthErrorMessage(error);
|
|
||||||
message.error(errorMessage);
|
|
||||||
setLoginError(errorMessage);
|
|
||||||
setPassword("");
|
|
||||||
setRetryCooldown(3);
|
|
||||||
} finally {
|
|
||||||
setLoginLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContinueLoggedIn = async () => {
|
const handleContinueLoggedIn = async () => {
|
||||||
try {
|
try {
|
||||||
await setupService.completeStep2();
|
await setupService.completeStep2();
|
||||||
@@ -381,6 +341,15 @@ function SetupWizard({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLoginSuccess = async () => {
|
||||||
|
await ensureDefaultStep1Config();
|
||||||
|
await setupService.completeStep2();
|
||||||
|
await setupService.completeSetup();
|
||||||
|
setCompleted(true);
|
||||||
|
message.success(t("Claw.Setup.login.success"));
|
||||||
|
setTimeout(() => onComplete(), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await logout();
|
await logout();
|
||||||
@@ -551,85 +520,11 @@ function SetupWizard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form layout="vertical">
|
<LoginPage
|
||||||
{loginError && (
|
step1Config={step1Config}
|
||||||
<Alert
|
initialUsername={username}
|
||||||
message={t("Claw.Setup.login.failed")}
|
onLoginSuccess={handleLoginSuccess}
|
||||||
description={
|
|
||||||
<Text
|
|
||||||
copyable={{ text: loginError }}
|
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
color: "var(--color-text-secondary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loginError}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
type="error"
|
|
||||||
showIcon
|
|
||||||
style={{ marginBottom: 12 }}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
<Form.Item label={t("Claw.Setup.login.domain")} required>
|
|
||||||
<Input
|
|
||||||
prefix={<GlobalOutlined />}
|
|
||||||
value={domain || step1Config.serverHost}
|
|
||||||
onChange={(e) => setDomain(e.target.value)}
|
|
||||||
placeholder={t("Claw.Setup.login.domainPlaceholder")}
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item label={t("Claw.Setup.login.account")} required>
|
|
||||||
<Input
|
|
||||||
prefix={<UserOutlined />}
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
placeholder={t("Claw.Setup.login.accountPlaceholder")}
|
|
||||||
autoComplete="username"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item label={t("Claw.Setup.login.code")} required>
|
|
||||||
<Input.Password
|
|
||||||
prefix={<LockOutlined />}
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
placeholder={t("Claw.Setup.login.codePlaceholder")}
|
|
||||||
autoComplete="current-password"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Space style={{ width: "100%" }} direction="vertical">
|
|
||||||
<Space>
|
|
||||||
<Button onClick={() => setCurrentStep(1)}>
|
|
||||||
{t("Claw.Setup.login.prevStep")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={handleLogin}
|
|
||||||
loading={loginLoading}
|
|
||||||
disabled={retryCooldown > 0}
|
|
||||||
>
|
|
||||||
{retryCooldown > 0
|
|
||||||
? t("Claw.Setup.login.pleaseWait", {
|
|
||||||
seconds: retryCooldown,
|
|
||||||
})
|
|
||||||
: t("Claw.Setup.login.loginButton")}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
<div style={{ textAlign: "center" }}>
|
|
||||||
<Text
|
|
||||||
style={{ fontSize: 11, color: "var(--color-text-tertiary)" }}
|
|
||||||
>
|
|
||||||
{t("Claw.Setup.login.supportMultipleAccountTypes")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
</Form>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -709,6 +604,22 @@ function SetupWizard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
dependenciesReady &&
|
||||||
|
currentStep === 2 &&
|
||||||
|
!completed &&
|
||||||
|
!checkingAuth &&
|
||||||
|
!isAlreadyLoggedIn
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<LoginPage
|
||||||
|
step1Config={step1Config}
|
||||||
|
initialUsername={username}
|
||||||
|
onLoginSuccess={handleLoginSuccess}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 阶段 2-4: 配置步骤
|
// 阶段 2-4: 配置步骤
|
||||||
return (
|
return (
|
||||||
<div style={styles.container}>
|
<div style={styles.container}>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<link rel="icon" type="image/png" href="/icon.png" />
|
<link rel="icon" type="image/png" href="/icon.png" />
|
||||||
<!-- 与 @shared/constants APP_DISPLAY_NAME 保持一致 -->
|
<!-- 与 @shared/constants APP_DISPLAY_NAME 保持一致 -->
|
||||||
<title>启明</title>
|
<title>陇电数字员工</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -3,32 +3,6 @@
|
|||||||
* 包含顶部栏、布局等样式
|
* 包含顶部栏、布局等样式
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* 顶部栏右侧信息区 */
|
|
||||||
.headerRight {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 用户名 */
|
|
||||||
.username {
|
|
||||||
color: #6B7280;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 状态徽章文字 */
|
|
||||||
.badgeText {
|
|
||||||
color: #6B7280;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 就绪、繁忙状态:橙色小点 */
|
|
||||||
.badgeIdle :global(.ant-badge-status-dot) {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
min-width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Webview 模式下顶部栏左侧操作区(替代 logo) */
|
/* Webview 模式下顶部栏左侧操作区(替代 logo) */
|
||||||
.headerWebviewActions {
|
.headerWebviewActions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
min-width: 240px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f1f2ef;
|
||||||
|
border-right: 1px solid #deded9;
|
||||||
|
color: #1f211f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
height: 122px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 38px 1fr 34px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 26px 12px 18px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandLogo {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
box-shadow: 0 8px 18px rgba(217, 116, 74, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandText {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandName {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #20211f;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandSubTitle {
|
||||||
|
margin-top: 1px;
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #a09d98;
|
||||||
|
letter-spacing: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyTrigger {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #e4e4df;
|
||||||
|
color: #656661;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 0.16s ease,
|
||||||
|
color 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyTrigger:hover {
|
||||||
|
background: #dadad4;
|
||||||
|
color: #252622;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 11px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navSection {
|
||||||
|
padding-bottom: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #deded8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navSection:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #8e8b84;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navItem {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 34px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 10px;
|
||||||
|
background: transparent;
|
||||||
|
color: #5e5d58;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 0.16s ease,
|
||||||
|
color 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navItem:hover {
|
||||||
|
background: #e8e8e2;
|
||||||
|
color: #2d2d29;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navItemActive {
|
||||||
|
background: #eaded7;
|
||||||
|
color: #d66c47;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navItemActive::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 10px;
|
||||||
|
width: 3px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #df7653;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navIcon {
|
||||||
|
width: 18px;
|
||||||
|
min-width: 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navLabel {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navBadge {
|
||||||
|
max-width: 46px;
|
||||||
|
height: 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 7px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #e6f0ff;
|
||||||
|
color: #4d8dea;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navBadgeSuccess {
|
||||||
|
max-width: 52px;
|
||||||
|
background: #dff5e7;
|
||||||
|
color: #16b268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 12px 11px 14px;
|
||||||
|
border-top: 1px solid #deded8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userCard {
|
||||||
|
height: 52px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #deded8;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f6f7f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #ec9a67, #df7653);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userMeta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userName {
|
||||||
|
color: #252622;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userEmail {
|
||||||
|
color: #8e8b84;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onlineDot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #b8b8b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onlineDotActive {
|
||||||
|
background: #21bf63;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoutButton {
|
||||||
|
width: 100%;
|
||||||
|
height: 34px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 7px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 9px;
|
||||||
|
padding: 0 12px;
|
||||||
|
background: #f1e3df;
|
||||||
|
color: #e64f45;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 0.16s ease,
|
||||||
|
color 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoutButton:hover {
|
||||||
|
background: #ead4cf;
|
||||||
|
color: #c8342e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyPopover :global(.ant-popover-inner) {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 12px 28px rgba(22, 24, 26, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyPopover :global(.ant-popover-arrow) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyMenu {
|
||||||
|
width: 176px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyTitle {
|
||||||
|
padding: 4px 8px 8px;
|
||||||
|
color: #8e8b84;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyItem {
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 7px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: #4e4e49;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyItem:hover {
|
||||||
|
background: #f4f4f1;
|
||||||
|
color: #d66c47;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyIcon {
|
||||||
|
width: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ import {
|
|||||||
describe("Constants", () => {
|
describe("Constants", () => {
|
||||||
describe("App Identity", () => {
|
describe("App Identity", () => {
|
||||||
it("should have consistent app name", () => {
|
it("should have consistent app name", () => {
|
||||||
expect(APP_DISPLAY_NAME).toBe("启明数字员工");
|
expect(APP_DISPLAY_NAME).toBe("陇电数字员工");
|
||||||
expect(APP_NAME_IDENTIFIER).toBe("qimingclaw");
|
expect(APP_NAME_IDENTIFIER).toBe("qimingclaw");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { AgentEngineType } from "@shared/types/electron";
|
|||||||
// ==================== 应用名称 ====================
|
// ==================== 应用名称 ====================
|
||||||
|
|
||||||
/** 应用对外显示名称(窗口标题、关于、安装包名称等),与 package.json build.productName 保持一致 */
|
/** 应用对外显示名称(窗口标题、关于、安装包名称等),与 package.json build.productName 保持一致 */
|
||||||
export const APP_DISPLAY_NAME = "启明数字员工";
|
export const APP_DISPLAY_NAME = "陇电数字员工";
|
||||||
|
|
||||||
/** 应用技术标识(进程名、目录名等,小写字母),与 appId 等保持一致 */
|
/** 应用技术标识(进程名、目录名等,小写字母),与 appId 等保持一致 */
|
||||||
export const APP_NAME_IDENTIFIER = "qimingclaw";
|
export const APP_NAME_IDENTIFIER = "qimingclaw";
|
||||||
|
|||||||