feat(client): add offline digital employee overview

This commit is contained in:
baiyanyun
2026-06-04 19:42:37 +08:00
parent 44fe160880
commit 2c3f2c8c54
35 changed files with 2902 additions and 7 deletions

View File

@@ -28,6 +28,7 @@ import {
ArrowLeftOutlined,
ReloadOutlined,
ApiOutlined,
AppstoreOutlined,
} from "@ant-design/icons";
import {
setupService,
@@ -57,6 +58,7 @@ import AboutPage from "./components/pages/AboutPage";
import LogViewer from "./components/pages/LogViewer";
import PermissionsPage from "./components/pages/PermissionsPage";
import SessionsPage from "./components/pages/SessionsPage";
import OverviewPage from "./components/pages/OverviewPage";
import MCPSettings from "./components/settings/MCPSettings";
import type { WebviewHeaderActions } from "./components/pages/SessionsPage";
import { createLogger } from "./services/utils/rendererLog";
@@ -104,6 +106,7 @@ export function useI18nLang(): I18nContextValue {
// Tab 类型定义(对齐 Tauri 客户端)
type TabKey =
| "client"
| "overview"
| "sessions"
| "mcp"
| "settings"
@@ -1054,6 +1057,11 @@ function App() {
icon: <DashboardOutlined />,
label: t("Claw.Menu.client"),
},
{
key: "overview",
icon: <AppstoreOutlined />,
label: t("Claw.Menu.overview"),
},
{
key: "sessions",
icon: <TeamOutlined />,
@@ -1353,6 +1361,7 @@ function App() {
onLoginStarted={handleLoginStarted}
/>
)}
{activeTab === "overview" && <OverviewPage />}
{activeTab === "sessions" && (
<SessionsPage
autoOpen={sessionsAutoOpen}

View File

@@ -0,0 +1,161 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Alert, Button, Input, message } from "antd";
import { ReloadOutlined, SaveOutlined } from "@ant-design/icons";
import { APP_DISPLAY_NAME } from "@shared/constants";
import { t } from "../../services/core/i18n";
import styles from "../../styles/components/OverviewPage.module.css";
const OVERVIEW_URL_SETTING_KEY = "overview.digital_employee_url";
const DEFAULT_OVERVIEW_URL = "sgrobot-digital/index.html#/digital";
function normalizeOverviewUrl(value: string): string {
const trimmed = value.trim();
if (!trimmed) return DEFAULT_OVERVIEW_URL;
const url = new URL(trimmed, window.location.href);
const legacySgRobotEndpoint =
(url.hostname === "localhost" && url.port === "5173") ||
(url.hostname === "127.0.0.1" && url.port === "42617");
if (legacySgRobotEndpoint && url.pathname === "/_app/digital") {
return DEFAULT_OVERVIEW_URL;
}
if (!["http:", "https:", "file:"].includes(url.protocol)) {
throw new Error("Unsupported protocol");
}
return trimmed;
}
function OverviewPage() {
const webviewRef = useRef<HTMLElement | null>(null);
const [url, setUrl] = useState(DEFAULT_OVERVIEW_URL);
const [draftUrl, setDraftUrl] = useState(DEFAULT_OVERVIEW_URL);
const [error, setError] = useState<string | null>(null);
const [userAgent, setUserAgent] = useState<string | undefined>();
useEffect(() => {
window.electronAPI?.app
.getVersion()
.then((version) => {
setUserAgent(navigator.userAgent + ` ${APP_DISPLAY_NAME}/${version}`);
})
.catch(() => {});
}, []);
useEffect(() => {
let mounted = true;
window.electronAPI?.settings
.get(OVERVIEW_URL_SETTING_KEY)
.then((saved) => {
if (!mounted || typeof saved !== "string" || !saved.trim()) return;
const normalized = normalizeOverviewUrl(saved);
setUrl(normalized);
setDraftUrl(normalized);
})
.catch(() => {});
return () => {
mounted = false;
};
}, []);
useEffect(() => {
const el = webviewRef.current as any;
if (!el) return;
const handleStartLoading = () => setError(null);
const handleFailLoad = (e: any) => {
if (e.errorCode && e.errorCode !== -3) {
setError(
t(
"Claw.Overview.loadFailed",
e.errorDescription || t("Claw.EmbeddedWebview.unknownError"),
String(e.errorCode),
),
);
}
};
el.addEventListener("did-start-loading", handleStartLoading);
el.addEventListener("did-fail-load", handleFailLoad);
return () => {
el.removeEventListener("did-start-loading", handleStartLoading);
el.removeEventListener("did-fail-load", handleFailLoad);
};
}, [url]);
const canSave = useMemo(() => draftUrl.trim() !== url, [draftUrl, url]);
const webviewUrl = useMemo(
() => new URL(url, window.location.href).toString(),
[url],
);
const handleReload = useCallback(() => {
setError(null);
(webviewRef.current as any)?.reload?.();
}, []);
const handleSave = useCallback(async () => {
try {
const normalized = normalizeOverviewUrl(draftUrl);
await window.electronAPI?.settings.set(
OVERVIEW_URL_SETTING_KEY,
normalized,
);
setUrl(normalized);
setDraftUrl(normalized);
setError(null);
message.success(t("Claw.Overview.urlSaved"));
} catch {
message.error(t("Claw.Overview.invalidUrl"));
}
}, [draftUrl]);
return (
<div className={styles.container}>
<div className={styles.toolbar}>
<span className={styles.title}>{t("Claw.Overview.title")}</span>
<Input
size="small"
className={styles.urlInput}
value={draftUrl}
onChange={(e) => setDraftUrl(e.target.value)}
onPressEnter={handleSave}
/>
<Button
size="small"
icon={<SaveOutlined />}
onClick={handleSave}
disabled={!canSave}
>
{t("Claw.Common.save")}
</Button>
<Button size="small" icon={<ReloadOutlined />} onClick={handleReload}>
{t("Claw.Common.refresh")}
</Button>
</div>
{error && (
<Alert
message={error}
type="error"
showIcon
closable
onClose={() => setError(null)}
className={styles.error}
/>
)}
<webview
ref={webviewRef as any}
src={webviewUrl}
useragent={userAgent}
className={styles.webview}
allowpopups={"true" as any}
/>
</div>
);
}
export default OverviewPage;

View File

@@ -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>

View File

@@ -0,0 +1,42 @@
.container {
display: flex;
flex: 1;
min-height: 0;
height: 100%;
flex-direction: column;
background: var(--color-bg-layout);
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg-container);
flex-shrink: 0;
}
.title {
flex: 0 0 auto;
font-size: 13px;
font-weight: 600;
color: var(--color-text);
}
.urlInput {
flex: 1;
min-width: 180px;
}
.error {
margin: 8px 12px 0;
flex-shrink: 0;
}
.webview {
flex: 1;
width: 100%;
min-height: 0;
border: none;
}

View File

@@ -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");
});

View File

@@ -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";

View File

@@ -164,6 +164,7 @@
"Claw.Agent.Status.stopped": "Stopped",
"Claw.Agent.Status.error": "Error",
"Claw.Menu.client": "Client",
"Claw.Menu.overview": "Overview",
"Claw.Menu.session": "Session",
"Claw.Menu.mcp": "MCP",
"Claw.Menu.settings": "Settings",
@@ -254,6 +255,10 @@
"Claw.EmbeddedWebview.back": "Back",
"Claw.EmbeddedWebview.refresh": "Refresh",
"Claw.EmbeddedWebview.unknownError": "Unknown error",
"Claw.Overview.title": "Digital Employee Overview",
"Claw.Overview.loadFailed": "Digital employee page failed to load: {0} ({1})",
"Claw.Overview.urlSaved": "Overview URL saved",
"Claw.Overview.invalidUrl": "Enter a valid http/https URL",
"Claw.AgentRunner.configSaved": "Config saved",
"Claw.AgentRunner.stopped": "Agent Runner stopped",
"Claw.AgentRunner.started": "Agent Runner started",

View File

@@ -164,6 +164,7 @@
"Claw.Agent.Status.stopped": "已停止",
"Claw.Agent.Status.error": "错误",
"Claw.Menu.client": "客户端",
"Claw.Menu.overview": "概览",
"Claw.Menu.session": "会话",
"Claw.Menu.mcp": "MCP",
"Claw.Menu.settings": "设置",
@@ -254,6 +255,10 @@
"Claw.EmbeddedWebview.back": "返回",
"Claw.EmbeddedWebview.refresh": "刷新",
"Claw.EmbeddedWebview.unknownError": "未知错误",
"Claw.Overview.title": "数字员工概览",
"Claw.Overview.loadFailed": "数字员工页面加载失败: {0} ({1})",
"Claw.Overview.urlSaved": "概览地址已保存",
"Claw.Overview.invalidUrl": "请输入有效的 http/https 地址",
"Claw.AgentRunner.configSaved": "配置已保存",
"Claw.AgentRunner.stopped": "Agent Runner 已停止",
"Claw.AgentRunner.started": "Agent Runner 已启动",

View File

@@ -164,6 +164,7 @@
"Claw.Agent.Status.stopped": "已停止",
"Claw.Agent.Status.error": "錯誤",
"Claw.Menu.client": "客戶端",
"Claw.Menu.overview": "概覽",
"Claw.Menu.session": "工作階段",
"Claw.Menu.mcp": "MCP",
"Claw.Menu.settings": "設定",
@@ -254,6 +255,10 @@
"Claw.EmbeddedWebview.back": "返回",
"Claw.EmbeddedWebview.refresh": "重新整理",
"Claw.EmbeddedWebview.unknownError": "未知錯誤",
"Claw.Overview.title": "數字員工概覽",
"Claw.Overview.loadFailed": "數字員工頁面載入失敗: {0} ({1})",
"Claw.Overview.urlSaved": "概覽位址已儲存",
"Claw.Overview.invalidUrl": "請輸入有效的 http/https 位址",
"Claw.AgentRunner.configSaved": "配置已儲存",
"Claw.AgentRunner.stopped": "Agent Runner 已停止",
"Claw.AgentRunner.started": "Agent Runner 已啟動",

View File

@@ -164,6 +164,7 @@
"Claw.Agent.Status.stopped": "已停止",
"Claw.Agent.Status.error": "錯誤",
"Claw.Menu.client": "客戶端",
"Claw.Menu.overview": "概覽",
"Claw.Menu.session": "工作階段",
"Claw.Menu.mcp": "MCP",
"Claw.Menu.settings": "設定",
@@ -254,6 +255,10 @@
"Claw.EmbeddedWebview.back": "返回",
"Claw.EmbeddedWebview.refresh": "重新整理",
"Claw.EmbeddedWebview.unknownError": "未知錯誤",
"Claw.Overview.title": "數字員工概覽",
"Claw.Overview.loadFailed": "數字員工頁面載入失敗: {0} ({1})",
"Claw.Overview.urlSaved": "概覽位址已儲存",
"Claw.Overview.invalidUrl": "請輸入有效的 http/https 位址",
"Claw.AgentRunner.configSaved": "配置已儲存",
"Claw.AgentRunner.stopped": "Agent Runner 已停止",
"Claw.AgentRunner.started": "Agent Runner 已啟動",