优化安装脚本

This commit is contained in:
dushixiang committed 2025-11-28 11:01:39 +08:00
1 parent d60443170a
commit 144ea30a23
1 file changed
+241 -208
+241 -208
View File
@@ -1,4 +1,4 @@
import React, {type ReactElement, useEffect, useState} from 'react';
import React, {type ReactElement, useCallback, useEffect, useMemo, useState} from 'react';
import {Alert, App, Button, Card, Select, Space, Tabs, Typography} from 'antd';
import {CopyIcon} from 'lucide-react';
import {listApiKeys} from '../../api/apiKey';
@@ -11,84 +11,120 @@ import {useNavigate} from "react-router-dom";
const {Paragraph, Text} = Typography;
const {TabPane} = Tabs;
// 常量定义
const AGENT_NAME = 'pika-agent';
const AGENT_NAME_EXE = 'pika-agent.exe';
const CONFIG_PATH = '~/.pika/agent.yaml';
const DEFAULT_OS = 'linux-amd64' as const;
// 类型定义
interface OSConfig {
name: string;
icon: ReactElement;
downloadUrl: string;
}
interface InstallStep {
title: string;
command: string;
}
type OSType = 'linux-amd64' | 'linux-arm64' | 'darwin-amd64' | 'darwin-arm64' | 'windows-amd64' | 'windows-arm64';
const AgentInstall = () => {
const [selectedOS, setSelectedOS] = useState<string>('linux-amd64');
const [selectedOS, setSelectedOS] = useState<OSType>(DEFAULT_OS);
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
const [selectedApiKey, setSelectedApiKey] = useState<string>('');
const [loading, setLoading] = useState<boolean>(true);
const {message} = App.useApp();
const serverUrl = window.location.origin;
let navigate = useNavigate();
const navigate = useNavigate();
const serverUrl = useMemo(() => window.location.origin, []);
// 加载API密钥列表
useEffect(() => {
const fetchApiKeys = async () => {
setLoading(true);
try {
const keys = await listApiKeys();
const enabledKeys = keys.data?.items.filter(k => k.enabled);
const enabledKeys = keys.data?.items.filter(k => k.enabled) || [];
setApiKeys(enabledKeys);
if (enabledKeys.length > 0) {
setSelectedApiKey(enabledKeys[0].key);
}
} catch (error) {
console.error('Failed to load API keys:', error);
message.error('加载 API Token 失败');
} finally {
setLoading(false);
}
};
fetchApiKeys();
}, []);
void fetchApiKeys();
}, [message]);
const osConfigs: Record<string, OSConfig> = {
// 操作系统配置
const osConfigs: Record<OSType, OSConfig> = useMemo(() => ({
'linux-amd64': {
name: 'Linux (amd64)',
icon: <img src={linuxPng} alt="Linux" className={'h-4 w-4'}/>,
icon: <img src={linuxPng} alt="Linux" className="h-4 w-4"/>,
downloadUrl: '/api/agent/downloads/agent-linux-amd64',
},
'linux-arm64': {
name: 'Linux (arm64)',
icon: <img src={linuxPng} alt="Linux" className={'h-4 w-4'}/>,
icon: <img src={linuxPng} alt="Linux" className="h-4 w-4"/>,
downloadUrl: '/api/agent/downloads/agent-linux-arm64',
},
'darwin-amd64': {
name: 'macOS (amd64)',
icon: <img src={applePng} alt="macOS" className={'h-4 w-4'}/>,
icon: <img src={applePng} alt="macOS" className="h-4 w-4"/>,
downloadUrl: '/api/agent/downloads/agent-darwin-amd64',
},
'darwin-arm64': {
name: 'macOS (arm64)',
icon: <img src={applePng} alt="macOS" className={'h-4 w-4'}/>,
icon: <img src={applePng} alt="macOS" className="h-4 w-4"/>,
downloadUrl: '/api/agent/downloads/agent-darwin-arm64',
},
'windows-amd64': {
name: 'Windows (amd64)',
icon: <img src={windowsPng} alt="Windows" className={'h-4 w-4'}/>,
icon: <img src={windowsPng} alt="Windows" className="h-4 w-4"/>,
downloadUrl: '/api/agent/downloads/agent-windows-amd64.exe',
},
'windows-arm64': {
name: 'Windows (arm64)',
icon: <img src={windowsPng} alt="Windows" className={'h-4 w-4'}/>,
icon: <img src={windowsPng} alt="Windows" className="h-4 w-4"/>,
downloadUrl: '/api/agent/downloads/agent-windows-arm64.exe',
},
};
}), []);
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
message.success('已复制到剪贴板');
};
// 复制到剪贴板
const copyToClipboard = useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text);
message.success('已复制到剪贴板');
} catch (error) {
console.error('Failed to copy:', error);
message.error('复制失败,请手动复制');
}
}, [message]);
// 获取一键安装命令
const getInstallCommand = () => {
// Linux/macOS 使用一键安装脚本
return `curl -fsSL ${serverUrl}/api/agent/install.sh?token=${selectedApiKey} | sudo bash`;
};
const installCommand = useMemo(
() => `curl -fsSL ${serverUrl}/api/agent/install.sh?token=${selectedApiKey} | sudo bash`,
[serverUrl, selectedApiKey]
);
const ApiChooser = () => {
return <Card type={'inner'} title={'选择 API Token'}>
// API Key 选择器选项
const apiKeyOptions = useMemo(
() => apiKeys.map(key => ({
label: `${key.name} (${key.key.substring(0, 8)}...)`,
value: key.key,
})),
[apiKeys]
);
// API Token 选择组件
const ApiChooser = useCallback(() => (
<Card type="inner" title="选择 API Token">
<Space direction="vertical" className="w-full">
{apiKeys.length === 0 ? (
<Alert
@@ -107,21 +143,21 @@ const AgentInstall = () => {
className="w-full mt-2"
value={selectedApiKey}
onChange={setSelectedApiKey}
options={apiKeys.map(key => ({
label: `${key.name} (${key.key.substring(0, 8)}...)`,
value: key.key,
}))}
options={apiKeyOptions}
loading={loading}
placeholder="请选择 API Token"
/>
)}
</Space>
</Card>
}
), [apiKeys, selectedApiKey, apiKeyOptions, loading]);
const InstallByOneClick = () => {
return <Space direction={'vertical'} className={'w-full'}>
// 一键安装组件
const InstallByOneClick = useCallback(() => (
<Space direction="vertical" className="w-full">
<ApiChooser/>
<Alert
description={'一键安装脚本仅支持 Linux/macOS 系统。'}
description="一键安装脚本仅支持 Linux/macOS 系统。"
type="info"
showIcon
className="mt-2"
@@ -130,167 +166,28 @@ const AgentInstall = () => {
<Paragraph type="secondary" className="mb-3">
脚本会自动检测系统架构并下载对应版本的探针,然后完成注册和安装。
</Paragraph>
<pre className="m-0 overflow-auto text-sm">
<code>{getInstallCommand()}</code>
</pre>
<pre className="m-0 overflow-auto text-sm bg-gray-50 p-3 rounded">
<code>{installCommand}</code>
</pre>
<Button
type={'link'}
onClick={() => {
copyToClipboard(getInstallCommand());
}}
icon={<CopyIcon className={'h-4 w-4'}/>}
type="link"
onClick={() => void copyToClipboard(installCommand)}
icon={<CopyIcon className="h-4 w-4"/>}
style={{margin: 0, padding: 0}}
disabled={!selectedApiKey}
>
复制命令
</Button>
</Card>
<ServiceHelper os={'pika-agent'}/>
<ServiceHelper os={AGENT_NAME}/>
<ConfigHelper/>
</Space>
}
), [ApiChooser, installCommand, copyToClipboard, selectedApiKey]);
const ServiceHelper = ({os}: { os: string }) => {
return <Card type="inner" title="服务管理命令">
<Paragraph type="secondary" className="mb-3">
注册完成后,您可以使用以下命令管理探针服务:
</Paragraph>
<pre className="m-0 overflow-auto text-sm">
<code>{getCommonCommands(os)}</code>
</pre>
</Card>
}
const ConfigHelper = () => {
return <Card type="inner" title="配置文件说明">
<Paragraph>
注册完成后,配置文件会保存在:
</Paragraph>
<ul className="space-y-2">
<li>
<Text code>~/.pika/agent.yaml</Text> - 配置文件路径
</li>
<li>
您可以手动编辑此文件来修改配置,修改后需要重启服务生效
</li>
</ul>
</Card>
}
// 获取手动安装步骤
const getManualInstallSteps = (os: string) => {
const token = selectedApiKey;
const config = osConfigs[os];
if (os.startsWith('windows')) {
const agentName = 'pika-agent.exe';
return [
{
title: '1. 下载探针',
command: `# 使用 PowerShell 下载
Invoke-WebRequest -Uri "${serverUrl}${config.downloadUrl}" -OutFile "${agentName}"
# 或者使用浏览器直接下载
# ${serverUrl}${config.downloadUrl}`
},
{
title: '2. 注册探针',
command: `.\\${agentName} register --endpoint "${serverUrl}" --token "${token}"`
},
{
title: '3. 验证安装',
command: `.\\${agentName} status`
}
];
} else {
const agentName = 'pika-agent';
return [
{
title: '1. 下载探针',
command: `# 使用 wget 下载
wget ${serverUrl}${config.downloadUrl} -O ${agentName}
# 或使用 curl 下载
curl -L ${serverUrl}${config.downloadUrl} -o ${agentName}`
},
{
title: '2. 赋予执行权限',
command: `chmod +x ${agentName}`
},
{
title: '3. 移动到系统路径',
command: `sudo mv ${agentName} /usr/local/bin/${agentName}`
},
{
title: '4. 注册探针',
command: `sudo ${agentName} register --endpoint "${serverUrl}" --token "${token}"`
},
{
title: '5. 验证安装',
command: `sudo ${agentName} status`
}
];
}
};
const InstallByManual = () => {
return <Space direction={'vertical'} className={'w-full'}>
<ApiChooser/>
<Tabs
activeKey={selectedOS}
onChange={setSelectedOS}
size="large"
>
{Object.entries(osConfigs).map(([key, config]) => (
<TabPane
tab={
<div className={'flex items-center gap-2'}>
{config.icon}
<span>{config.name}</span>
</div>
}
key={key}
>
<Space direction={'vertical'} className={'w-full'}>
{/* 手动安装步骤 */}
<Card type="inner" title="手动安装步骤">
<Space direction="vertical" className="w-full" size="middle">
{getManualInstallSteps(key).map((step, index) => (
<div key={index}>
<Text strong className="block mb-2">{step.title}</Text>
<pre className="m-0 overflow-auto text-sm bg-gray-50 p-3 rounded">
<code>{step.command}</code>
</pre>
<Button
type={'link'}
onClick={() => {
copyToClipboard(step.command);
}}
icon={<CopyIcon className={'h-4 w-4'}/>}
size="small"
style={{margin: 0, padding: 0}}
>
复制
</Button>
</div>
))}
</Space>
</Card>
<ServiceHelper os={key}/>
<ConfigHelper/>
</Space>
</TabPane>
))}
</Tabs>
</Space>
}
// 常用命令
const getCommonCommands = (os: string) => {
const agentCmd = os.startsWith('windows') ? '.\\pika-agent.exe' : 'pika-agent';
// 常用服务管理命令
const getCommonCommands = useCallback((os: string) => {
const agentCmd = os.startsWith('windows') ? `.\\${AGENT_NAME_EXE}` : AGENT_NAME;
const sudo = os.startsWith('windows') ? '' : 'sudo ';
return `# 查看服务状态
@@ -310,39 +207,175 @@ ${sudo}${agentCmd} uninstall
# 查看版本
${agentCmd} version`;
};
}, []);
// 服务管理命令组件
const ServiceHelper = useCallback(({os}: { os: string }) => (
<Card type="inner" title="服务管理命令">
<Paragraph type="secondary" className="mb-3">
注册完成后,您可以使用以下命令管理探针服务:
</Paragraph>
<pre className="m-0 overflow-auto text-sm bg-gray-50 p-3 rounded">
<code>{getCommonCommands(os)}</code>
</pre>
</Card>
), [getCommonCommands]);
// 配置文件说明组件
const ConfigHelper = useCallback(() => (
<Card type="inner" title="配置文件说明">
<Paragraph>
注册完成后,配置文件会保存在:
</Paragraph>
<ul className="space-y-2">
<li>
<Text code>{CONFIG_PATH}</Text> - 配置文件路径
</li>
<li>
您可以手动编辑此文件来修改配置,修改后需要重启服务生效
</li>
</ul>
</Card>
), []);
// 获取手动安装步骤
const getManualInstallSteps = useCallback((os: OSType): InstallStep[] => {
const config = osConfigs[os];
if (os.startsWith('windows')) {
return [
{
title: '1. 下载探针',
command: `# 使用 PowerShell 下载
Invoke-WebRequest -Uri "${serverUrl}${config.downloadUrl}" -OutFile "${AGENT_NAME_EXE}"
# 或者使用浏览器直接下载
# ${serverUrl}${config.downloadUrl}`
},
{
title: '2. 注册探针',
command: `.\\${AGENT_NAME_EXE} register --endpoint "${serverUrl}" --token "${selectedApiKey}"`
},
{
title: '3. 验证安装',
command: `.\\${AGENT_NAME_EXE} status`
}
];
}
return [
{
title: '1. 下载探针',
command: `# 使用 wget 下载
wget ${serverUrl}${config.downloadUrl} -O ${AGENT_NAME}
# 或使用 curl 下载
curl -L ${serverUrl}${config.downloadUrl} -o ${AGENT_NAME}`
},
{
title: '2. 赋予执行权限',
command: `chmod +x ${AGENT_NAME}`
},
{
title: '3. 移动到系统路径',
command: `sudo mv ${AGENT_NAME} /usr/local/bin/${AGENT_NAME}`
},
{
title: '4. 注册探针',
command: `sudo ${AGENT_NAME} register --endpoint "${serverUrl}" --token "${selectedApiKey}"`
},
{
title: '5. 验证安装',
command: `sudo ${AGENT_NAME} status`
}
];
}, [osConfigs, serverUrl, selectedApiKey]);
// 手动安装组件
const InstallByManual = useCallback(() => (
<Space direction="vertical" className="w-full">
<ApiChooser/>
<Tabs
activeKey={selectedOS}
onChange={(key) => setSelectedOS(key as OSType)}
>
{Object.entries(osConfigs).map(([key, config]) => (
<TabPane
tab={
<div className="flex items-center gap-2">
{config.icon}
<span>{config.name}</span>
</div>
}
key={key}
>
<Space direction="vertical" className="w-full">
<Card type="inner" title="手动安装步骤">
<Space direction="vertical" className="w-full" size="middle">
{getManualInstallSteps(key as OSType).map((step, index) => (
<div key={index}>
<Text strong className="block mb-2">{step.title}</Text>
<pre className="m-0 overflow-auto text-sm bg-gray-50 p-3 rounded">
<code>{step.command}</code>
</pre>
<Button
type="link"
onClick={() => void copyToClipboard(step.command)}
icon={<CopyIcon className="h-4 w-4"/>}
size="small"
style={{margin: 0, padding: 0}}
disabled={!selectedApiKey}
>
复制
</Button>
</div>
))}
</Space>
</Card>
<ServiceHelper os={key}/>
<ConfigHelper/>
</Space>
</TabPane>
))}
</Tabs>
</Space>
), [ApiChooser, selectedOS, osConfigs, getManualInstallSteps, copyToClipboard, selectedApiKey, ServiceHelper, ConfigHelper]);
// 主选项卡配置
const tabItems = useMemo(() => [
{
label: '一键安装',
key: 'one-click',
children: <InstallByOneClick/>
},
{
label: '手动安装',
key: 'manual',
children: <InstallByManual/>
},
], [InstallByOneClick, InstallByManual]);
return (
<Space direction={'vertical'}>
<Space direction="vertical" className="w-full">
<div className="flex gap-2 items-center">
<div className="text-sm cursor-pointer hover:underline"
onClick={() => navigate(-1)}
>返回 |
<div
className="text-sm cursor-pointer hover:underline"
onClick={() => navigate(-1)}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && navigate(-1)}
>
返回 |
</div>
<h1 className="text-2xl font-semibold text-gray-900">探针部署指南</h1>
</div>
<Tabs
style={{
marginTop: 24,
}}
tabPosition={'left'}
items={[
{
label: '一键安装',
key: 'one-click',
children: <InstallByOneClick/>
},
{
label: '手动安装',
key: 'manual',
children: <InstallByManual/>
},
]}
style={{ marginTop: 24 }}
tabPosition="left"
items={tabItems}
/>
</Space>
);
};