重构客户端登录页并更新品牌图标
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@qiming-ai/qimingclaw",
|
||||
"version": "0.9.7",
|
||||
"description": "启明数字员工",
|
||||
"description": "陇电数字员工",
|
||||
"main": "dist/main/main.js",
|
||||
"author": "Qiming Team",
|
||||
"license": "MIT",
|
||||
@@ -128,7 +128,7 @@
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.qiming.qimingclaw",
|
||||
"productName": "启明数字员工",
|
||||
"productName": "陇电数字员工",
|
||||
"afterSign": "scripts/build/after-sign.js",
|
||||
"electronLanguages": [
|
||||
"en",
|
||||
@@ -297,8 +297,8 @@
|
||||
"hardenedRuntime": true,
|
||||
"extendInfo": {
|
||||
"CFBundleIdentifier": "com.qiming.agent",
|
||||
"CFBundleName": "启明数字员工",
|
||||
"CFBundleDisplayName": "启明数字员工",
|
||||
"CFBundleName": "陇电数字员工",
|
||||
"CFBundleDisplayName": "陇电数字员工",
|
||||
"NSDesktopFolderUsageDescription": "需要访问桌面文件夹以支持文件操作",
|
||||
"NSDocumentsFolderUsageDescription": "需要访问文档文件夹以支持文件操作",
|
||||
"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 |
@@ -39,6 +39,7 @@ import {
|
||||
syncConfigToServer,
|
||||
normalizeServerHost,
|
||||
loginAndRegister,
|
||||
isLoggedIn as checkIsLoggedIn,
|
||||
} from "./services/core/auth";
|
||||
import {
|
||||
APP_DISPLAY_NAME,
|
||||
@@ -49,6 +50,7 @@ import type { QuickInitConfig } from "@shared/types/quickInit";
|
||||
import type { UpdateState } from "@shared/types/updateTypes";
|
||||
import { t, getCurrentLang } from "./services/core/i18n";
|
||||
import SetupWizard from "./components/setup/SetupWizard";
|
||||
import LoginPage from "./components/setup/LoginPage";
|
||||
import SetupDependencies from "./components/setup/SetupDependencies";
|
||||
import ClientPage from "./components/pages/ClientPage";
|
||||
import SettingsPage from "./components/pages/SettingsPage";
|
||||
@@ -187,6 +189,9 @@ function App() {
|
||||
// 初始化向导状态
|
||||
// ============================================
|
||||
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 loginStartedRef = useRef(false);
|
||||
@@ -392,6 +397,10 @@ function App() {
|
||||
const checkSetup = async () => {
|
||||
try {
|
||||
const completed = await setupService.isSetupCompleted();
|
||||
const step1 = await setupService.getStep1Config();
|
||||
setLoginStep1Config(step1);
|
||||
const loggedIn = await checkIsLoggedIn();
|
||||
setIsLoggedIn(loggedIn);
|
||||
|
||||
// 每次启动优先读取 quick init 配置
|
||||
if (completed) {
|
||||
@@ -411,6 +420,7 @@ function App() {
|
||||
} catch (error) {
|
||||
log.error("Failed to check setup status:", error);
|
||||
setIsSetupComplete(true);
|
||||
setIsLoggedIn(false);
|
||||
}
|
||||
};
|
||||
checkSetup();
|
||||
@@ -445,6 +455,8 @@ function App() {
|
||||
// ============================================
|
||||
const handleAuthChange = useCallback(async () => {
|
||||
const user = await authService.getAuthUser();
|
||||
const loggedIn = await checkIsLoggedIn();
|
||||
setIsLoggedIn(loggedIn);
|
||||
if (user) {
|
||||
setUsername(
|
||||
user.displayName || user.username || t("Claw.App.defaultUsername"),
|
||||
@@ -459,6 +471,29 @@ function App() {
|
||||
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 手动升级)
|
||||
@@ -1106,7 +1141,8 @@ function App() {
|
||||
// ============================================
|
||||
if (
|
||||
isSetupComplete === null ||
|
||||
(isSetupComplete && needsRequiredDepsReinstall === null)
|
||||
isLoggedIn === null ||
|
||||
(isLoggedIn && isSetupComplete && needsRequiredDepsReinstall === null)
|
||||
) {
|
||||
return (
|
||||
<I18nContext.Provider value={i18nContextValue}>
|
||||
@@ -1120,6 +1156,20 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<I18nContext.Provider value={i18nContextValue}>
|
||||
<ConfigProvider theme={currentTheme}>
|
||||
<LoginPage
|
||||
step1Config={loginStep1Config}
|
||||
onLoginStart={ensureDefaultLoginConfig}
|
||||
onLoginSuccess={handleFullscreenLoginSuccess}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
</I18nContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 渲染:初始化向导
|
||||
// ============================================
|
||||
|
||||
@@ -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,
|
||||
UserOutlined,
|
||||
CheckCircleOutlined,
|
||||
RobotOutlined,
|
||||
FolderOpenOutlined,
|
||||
LockOutlined,
|
||||
GlobalOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import {
|
||||
setupService,
|
||||
@@ -46,9 +43,7 @@ import {
|
||||
import {
|
||||
loginAndRegister,
|
||||
normalizeServerHost,
|
||||
isLoggedIn as checkIsLoggedIn,
|
||||
getCurrentAuth,
|
||||
getAuthErrorMessage,
|
||||
logout,
|
||||
} from "../../services/core/auth";
|
||||
import { AUTH_KEYS } from "@shared/constants";
|
||||
@@ -56,6 +51,7 @@ import type { QuickInitConfig } from "@shared/types/quickInit";
|
||||
import SetupDependencies, {
|
||||
type MockDependenciesApi,
|
||||
} from "./SetupDependencies";
|
||||
import LoginPage from "./LoginPage";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -94,19 +90,15 @@ function SetupWizard({
|
||||
const [dependenciesReady, setDependenciesReady] = useState<boolean | null>(
|
||||
null,
|
||||
);
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [currentStep, setCurrentStep] = useState(2);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [step1Config, setStep1Config] =
|
||||
useState<Step1Config>(DEFAULT_STEP1_CONFIG);
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [completed, setCompleted] = useState(false);
|
||||
const [domain, setDomain] = useState("");
|
||||
const [loginLoading, setLoginLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState("");
|
||||
const [retryCooldown, setRetryCooldown] = useState(0);
|
||||
const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false);
|
||||
const [checkingAuth, setCheckingAuth] = useState(false);
|
||||
const [checkingAuth, setCheckingAuth] = useState(true);
|
||||
const [quickIniting, setQuickIniting] = useState(false);
|
||||
const quickInitAttempted = useRef(false);
|
||||
|
||||
@@ -117,14 +109,14 @@ function SetupWizard({
|
||||
if (skipDependencyCheck) {
|
||||
setDependenciesReady(true);
|
||||
const state = await setupService.getSetupState();
|
||||
if (state.completed) {
|
||||
setCompleted(true);
|
||||
onCompleteRef.current();
|
||||
} else {
|
||||
setCurrentStep(state.step1Completed ? 2 : 1);
|
||||
}
|
||||
const config = await setupService.getStep1Config();
|
||||
setStep1Config(config);
|
||||
if (state.completed) {
|
||||
setCompleted(true);
|
||||
onComplete();
|
||||
} else {
|
||||
setCurrentStep(2);
|
||||
}
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -173,7 +165,7 @@ function SetupWizard({
|
||||
);
|
||||
}
|
||||
}
|
||||
setCurrentStep(state.step1Completed ? 2 : 1);
|
||||
setCurrentStep(2);
|
||||
}
|
||||
} else {
|
||||
// 有依赖缺失,进入安装流程
|
||||
@@ -203,6 +195,7 @@ function SetupWizard({
|
||||
try {
|
||||
// 1. 保存 step1 配置
|
||||
const step1: Step1Config = {
|
||||
...DEFAULT_STEP1_CONFIG,
|
||||
serverHost: normalizeServerHost(config.serverHost),
|
||||
agentPort: config.agentPort,
|
||||
fileServerPort: config.fileServerPort,
|
||||
@@ -247,9 +240,7 @@ function SetupWizard({
|
||||
error,
|
||||
);
|
||||
setQuickIniting(false);
|
||||
// step1 可能已保存,从 step1 或 step2 继续
|
||||
const state = await setupService.getSetupState();
|
||||
setCurrentStep(state.step1Completed ? 2 : 1);
|
||||
setCurrentStep(2);
|
||||
}
|
||||
},
|
||||
[onComplete],
|
||||
@@ -272,9 +263,23 @@ function SetupWizard({
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentStep(1);
|
||||
setCurrentStep(2);
|
||||
}, [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
|
||||
const checkLoginStatus = useCallback(async () => {
|
||||
setCheckingAuth(true);
|
||||
@@ -300,15 +305,6 @@ function SetupWizard({
|
||||
}
|
||||
}, [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 () => {
|
||||
if (!step1Config.fileServerPort) {
|
||||
message.warning(t("Claw.Setup.basicConfig.fileServerPortRequired"));
|
||||
@@ -327,49 +323,13 @@ function SetupWizard({
|
||||
await setupService.saveStep1Config(step1Config);
|
||||
setCurrentStep(2);
|
||||
message.success(t("Claw.Setup.basicConfig.saved"));
|
||||
} catch (error) {
|
||||
} catch {
|
||||
message.error(t("Claw.Setup.basicConfig.saveFailed"));
|
||||
} finally {
|
||||
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 () => {
|
||||
try {
|
||||
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 () => {
|
||||
try {
|
||||
await logout();
|
||||
@@ -551,85 +520,11 @@ function SetupWizard({
|
||||
}
|
||||
|
||||
return (
|
||||
<Form layout="vertical">
|
||||
{loginError && (
|
||||
<Alert
|
||||
message={t("Claw.Setup.login.failed")}
|
||||
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>
|
||||
<LoginPage
|
||||
step1Config={step1Config}
|
||||
initialUsername={username}
|
||||
onLoginSuccess={handleLoginSuccess}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
@@ -709,6 +604,22 @@ function SetupWizard({
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
dependenciesReady &&
|
||||
currentStep === 2 &&
|
||||
!completed &&
|
||||
!checkingAuth &&
|
||||
!isAlreadyLoggedIn
|
||||
) {
|
||||
return (
|
||||
<LoginPage
|
||||
step1Config={step1Config}
|
||||
initialUsername={username}
|
||||
onLoginSuccess={handleLoginSuccess}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 阶段 2-4: 配置步骤
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<link rel="icon" type="image/png" href="/icon.png" />
|
||||
<!-- 与 @shared/constants APP_DISPLAY_NAME 保持一致 -->
|
||||
<title>启明</title>
|
||||
<title>陇电数字员工</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
describe("Constants", () => {
|
||||
describe("App Identity", () => {
|
||||
it("should have consistent app name", () => {
|
||||
expect(APP_DISPLAY_NAME).toBe("启明数字员工");
|
||||
expect(APP_DISPLAY_NAME).toBe("陇电数字员工");
|
||||
expect(APP_NAME_IDENTIFIER).toBe("qimingclaw");
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { AgentEngineType } from "@shared/types/electron";
|
||||
// ==================== 应用名称 ====================
|
||||
|
||||
/** 应用对外显示名称(窗口标题、关于、安装包名称等),与 package.json build.productName 保持一致 */
|
||||
export const APP_DISPLAY_NAME = "启明数字员工";
|
||||
export const APP_DISPLAY_NAME = "陇电数字员工";
|
||||
|
||||
/** 应用技术标识(进程名、目录名等,小写字母),与 appId 等保持一致 */
|
||||
export const APP_NAME_IDENTIFIER = "qimingclaw";
|
||||
|
||||