feat(client): add offline digital employee overview
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 已启动",
|
||||
|
||||
@@ -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 已啟動",
|
||||
|
||||
@@ -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 已啟動",
|
||||
|
||||
Reference in New Issue
Block a user