From 43780305da675ad2e985e6281003f247dab3a1ed Mon Sep 17 00:00:00 2001 From: dushixiang Date: Thu, 25 Dec 2025 18:54:29 +0800 Subject: [PATCH 01/18] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E8=84=9A=E6=9C=AC=EF=BC=8C=E6=8F=90=E9=AB=98?= =?UTF-8?q?=20curl=20=E4=BC=98=E5=85=88=E7=BA=A7=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=20wget=20=E4=B8=8D=E5=85=BC=E5=AE=B9=E7=9A=84?= =?UTF-8?q?=E8=AF=AD=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/agent_handler.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/handler/agent_handler.go b/internal/handler/agent_handler.go index 0f99c97..b3ec974 100644 --- a/internal/handler/agent_handler.go +++ b/internal/handler/agent_handler.go @@ -854,10 +854,10 @@ download_agent() { echo_info "正在下载探针..." - if command -v wget &> /dev/null; then - wget -q --show-progress "$download_url" -O "$temp_file" - elif command -v curl &> /dev/null; then + if command -v curl &> /dev/null; then curl -# -L "$download_url" -o "$temp_file" + elif command -v wget &> /dev/null; then + wget -q "$download_url" -O "$temp_file" else echo_error "未找到 wget 或 curl 命令,请先安装其中之一" exit 1 From 7944fb09dc9184115987f1c86be478f1f522b816 Mon Sep 17 00:00:00 2001 From: dushixiang Date: Thu, 25 Dec 2025 19:12:40 +0800 Subject: [PATCH 02/18] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8E=A2=E9=92=88?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=20home=20=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/agent/config/config.go | 6 ++---- pkg/agent/id/id.go | 8 ++------ pkg/agent/utils/homedir.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 pkg/agent/utils/homedir.go diff --git a/pkg/agent/config/config.go b/pkg/agent/config/config.go index b919356..d911b95 100644 --- a/pkg/agent/config/config.go +++ b/pkg/agent/config/config.go @@ -9,6 +9,7 @@ import ( "runtime" "time" + "github.com/dushixiang/pika/pkg/agent/utils" "gopkg.in/yaml.v3" ) @@ -107,10 +108,7 @@ func DefaultConfig() *Config { // GetDefaultConfigPath 获取默认配置文件路径 func GetDefaultConfigPath() string { - homeDir, err := os.UserHomeDir() - if err != nil { - homeDir = "." - } + var homeDir = utils.GetSafeHomeDir() return filepath.Join(homeDir, ".pika", "agent.yaml") } diff --git a/pkg/agent/id/id.go b/pkg/agent/id/id.go index 81ac6fc..b4d3032 100644 --- a/pkg/agent/id/id.go +++ b/pkg/agent/id/id.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" + "github.com/dushixiang/pika/pkg/agent/utils" "github.com/google/uuid" ) @@ -24,12 +25,7 @@ func NewManager() *Manager { // getIDFilePath 获取 ID 文件路径 func getIDFilePath() string { // 获取用户主目录 - homeDir, err := os.UserHomeDir() - if err != nil { - // 如果无法获取主目录,使用当前目录 - homeDir = "." - } - + var homeDir = utils.GetSafeHomeDir() // 统一使用 ~/.pika/agent.id return filepath.Join(homeDir, ".pika", "agent.id") } diff --git a/pkg/agent/utils/homedir.go b/pkg/agent/utils/homedir.go new file mode 100644 index 0000000..03ad987 --- /dev/null +++ b/pkg/agent/utils/homedir.go @@ -0,0 +1,34 @@ +package utils + +import ( + "os" + "os/user" +) + +func GetSafeHomeDir() string { + // 1. 尝试从环境变量拿(最快,符合大多数 Unix/Windows 惯例) + if h, err := os.UserHomeDir(); err == nil && h != "" { + return h + } + + // 2. 尝试从系统底层数据库拿(绕过环境变量,直接看 /etc/passwd) + if u, err := user.Current(); err == nil && u.HomeDir != "" { + return u.HomeDir + } + + // 3. 特殊环境兜底(针对 OpenWrt/嵌入式等 root 为主的系统) + // 如果是 root 权限运行但没设家目录,通常就是 /root + if os.Getuid() == 0 { + if _, err := os.Stat("/root"); err == nil { + return "/root" + } + } + + // 4. 极端情况兜底:使用当前程序运行目录或临时目录 + // 保证程序至少能跑起来,而不是直接 panic 或报错退出 + if pwd, err := os.Getwd(); err == nil { + return pwd + } + + return "./" +} From 502de9d81692cfd1858d91a66e8245fc3adb4acc Mon Sep 17 00:00:00 2001 From: dushixiang Date: Thu, 25 Dec 2025 19:24:59 +0800 Subject: [PATCH 03/18] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8E=A2=E9=92=88?= =?UTF-8?q?=E5=8D=B8=E8=BD=BD&=E6=9C=8D=E5=8A=A1=E7=AB=AF=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=8E=A2=E9=92=88=E8=87=AA=E5=8A=A8=E5=8D=B8=E8=BD=BD?= =?UTF-8?q?=E8=87=AA=E8=BA=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/agent/main.go | 18 ++----------- internal/handler/agent_handler.go | 23 +++++++++++++++- internal/protocol/message.go | 1 + pkg/agent/id/id.go | 6 ++--- pkg/agent/service/agent.go | 26 ++++++++++++++++++ pkg/agent/service/service.go | 44 +++++++++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 20 deletions(-) diff --git a/cmd/agent/main.go b/cmd/agent/main.go index 9e1421d..6a56e3b 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -237,23 +237,9 @@ func installService(cmd *cobra.Command, args []string) { // uninstallService 卸载服务 func uninstallService(cmd *cobra.Command, args []string) { - // 加载配置 - cfg, err := config.Load(configPath) - if err != nil { - log.Fatalf("❌ 加载配置失败: %v", err) + if err := service.UninstallAgent(configPath); err != nil { + log.Fatalf("❌ 卸载失败: %v", err) } - - // 创建服务管理器 - mgr, err := service.NewServiceManager(cfg) - if err != nil { - log.Fatalf("❌ 创建服务管理器失败: %v", err) - } - - // 卸载服务 - if err := mgr.Uninstall(); err != nil { - log.Fatalf("❌ 卸载服务失败: %v", err) - } - log.Println("✅ 服务卸载成功") } diff --git a/internal/handler/agent_handler.go b/internal/handler/agent_handler.go index b3ec974..9378ea4 100644 --- a/internal/handler/agent_handler.go +++ b/internal/handler/agent_handler.go @@ -713,8 +713,29 @@ func (h *AgentHandler) Delete(c echo.Context) error { return err } - // 如果探针在线,先断开连接 + // 如果探针在线,先发送卸载指令,然后断开连接 if client, exists := h.wsManager.GetClient(agentID); exists { + // 构建卸载消息 + uninstallMsg, err := json.Marshal(protocol.OutboundMessage{ + Type: protocol.MessageTypeUninstall, + Data: struct{}{}, // 空数据,卸载指令不需要额外参数 + }) + if err == nil { + // 发送卸载指令(忽略发送错误,继续删除流程) + if err := h.wsManager.SendToClient(agentID, uninstallMsg); err != nil { + h.logger.Warn("发送卸载指令失败", + zap.String("agentID", agentID), + zap.Error(err)) + } else { + h.logger.Info("已向探针发送卸载指令", + zap.String("agentID", agentID), + zap.String("name", agent.Name)) + // 等待一小段时间让探针处理卸载消息 + time.Sleep(500 * time.Millisecond) + } + } + + // 断开连接 client.Conn.Close() } diff --git a/internal/protocol/message.go b/internal/protocol/message.go index dadf299..150e15a 100644 --- a/internal/protocol/message.go +++ b/internal/protocol/message.go @@ -52,6 +52,7 @@ const ( MessageTypeHeartbeat MessageType = "heartbeat" MessageTypeCommand MessageType = "command" MessageTypeCommandResp MessageType = "command_response" + MessageTypeUninstall MessageType = "uninstall" // 指标消息 MessageTypeMetrics MessageType = "metrics" MessageTypeMonitorConfig MessageType = "monitor_config" diff --git a/pkg/agent/id/id.go b/pkg/agent/id/id.go index b4d3032..469c2b6 100644 --- a/pkg/agent/id/id.go +++ b/pkg/agent/id/id.go @@ -18,12 +18,12 @@ type Manager struct { // NewManager 创建 ID 管理器 func NewManager() *Manager { return &Manager{ - idFilePath: getIDFilePath(), + idFilePath: GetIDFilePath(), } } -// getIDFilePath 获取 ID 文件路径 -func getIDFilePath() string { +// GetIDFilePath 获取 ID 文件路径 +func GetIDFilePath() string { // 获取用户主目录 var homeDir = utils.GetSafeHomeDir() // 统一使用 ~/.pika/agent.id diff --git a/pkg/agent/service/agent.go b/pkg/agent/service/agent.go index 6ca3500..0e369c5 100644 --- a/pkg/agent/service/agent.go +++ b/pkg/agent/service/agent.go @@ -296,6 +296,8 @@ func (a *Agent) readLoop(conn *websocket.Conn, done chan struct{}) error { go a.handleTamperProtect(msg.Data) case protocol.MessageTypeDDNSConfig: go a.handleDDNSConfig(msg.Data) + case protocol.MessageTypeUninstall: + go a.handleUninstall() default: // 忽略其他类型 } @@ -829,3 +831,27 @@ func (a *Agent) getIPAddress(manager *collector.Manager, method, value string, i return "", fmt.Errorf("不支持的 IP 获取方式: %s", method) } } + +// handleUninstall 处理服务端发送的卸载指令 +func (a *Agent) handleUninstall() { + log.Println("📥 收到服务端卸载指令,开始执行卸载...") + + // 获取配置文件路径 + cfgPath := a.cfg.Path + if cfgPath == "" { + cfgPath = config.GetDefaultConfigPath() + } + + // 执行卸载操作 + if err := UninstallAgent(cfgPath); err != nil { + log.Printf("❌ 卸载失败: %v", err) + return + } + + log.Println("✅ 探针卸载成功,即将退出...") + + // 卸载成功后,触发停止信号 + if a.cancel != nil { + a.cancel() + } +} diff --git a/pkg/agent/service/service.go b/pkg/agent/service/service.go index 8638ca8..cbe4255 100644 --- a/pkg/agent/service/service.go +++ b/pkg/agent/service/service.go @@ -9,6 +9,7 @@ import ( "syscall" "github.com/dushixiang/pika/pkg/agent/config" + "github.com/dushixiang/pika/pkg/agent/id" "github.com/dushixiang/pika/pkg/agent/sysutil" "github.com/dushixiang/pika/pkg/agent/updater" "github.com/kardianos/service" @@ -235,3 +236,46 @@ func (m *ServiceManager) Run() error { return nil } + +// UninstallAgent 执行探针卸载操作(可被复用) +func UninstallAgent(cfgPath string) error { + // 加载配置 + cfg, err := config.Load(cfgPath) + if err != nil { + return fmt.Errorf("加载配置失败: %w", err) + } + + // 创建服务管理器 + mgr, err := NewServiceManager(cfg) + if err != nil { + return fmt.Errorf("创建服务管理器失败: %w", err) + } + + // 检查服务状态,如果在运行则停止 + status, err := mgr.Status() + if err != nil { + log.Printf("⚠️ 获取服务状态失败: %v", err) + } else if status != "已停止 (Stopped)" { + if err := mgr.Stop(); err != nil { + return fmt.Errorf("停止服务失败: %w", err) + } + } + + // 卸载服务 + if err := mgr.Uninstall(); err != nil { + return fmt.Errorf("卸载服务失败: %w", err) + } + + // 删除配置文件 + if err := os.Remove(cfgPath); err != nil { + log.Printf("⚠️ 删除配置文件失败: %v", err) + } + + // 删除探针 ID 文件 + idPath := id.GetIDFilePath() + if err := os.Remove(idPath); err != nil { + log.Printf("⚠️ 删除探针 ID 文件失败: %v", err) + } + + return nil +} From 6a658a028a08b2821b5d18f888eca2ff7e221881 Mon Sep 17 00:00:00 2001 From: dushixiang Date: Thu, 25 Dec 2025 19:27:20 +0800 Subject: [PATCH 04/18] =?UTF-8?q?=E6=8E=A2=E9=92=88=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=90=8D=E7=A7=B0=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/agent_handler.go | 7 +++---- web/src/pages/Agents/AgentList.tsx | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/handler/agent_handler.go b/internal/handler/agent_handler.go index 9378ea4..75ad790 100644 --- a/internal/handler/agent_handler.go +++ b/internal/handler/agent_handler.go @@ -288,16 +288,15 @@ func (h *AgentHandler) sendTamperConfig(conn *websocket.Conn, agentID string) er // Paging 探针分页查询 func (h *AgentHandler) Paging(c echo.Context) error { - hostname := c.QueryParam("hostname") - ip := c.QueryParam("ip") status := c.QueryParam("status") pr := orz.GetPageRequest(c, "name") builder := orz.NewPageBuilder(h.agentService.AgentRepo.Repository). PageRequest(pr). - Contains("hostname", hostname). - Contains("ip", ip) + Contains("name", c.QueryParam("name")). + Contains("hostname", c.QueryParam("hostname")). + Contains("ip", c.QueryParam("ip")) // 处理状态筛选 if status == "online" { diff --git a/web/src/pages/Agents/AgentList.tsx b/web/src/pages/Agents/AgentList.tsx index aca5af9..a4d340e 100644 --- a/web/src/pages/Agents/AgentList.tsx +++ b/web/src/pages/Agents/AgentList.tsx @@ -160,7 +160,6 @@ const AgentList = () => { title: '名称', dataIndex: 'name', key: 'name', - hideInSearch: true, fixed: 'left', render: (_, record) => (
@@ -266,6 +265,7 @@ const AgentList = () => { title: '版本', dataIndex: 'version', key: 'version', + hideInSearch: true, }, { title: '最后活跃时间', From fd37d6388565fd78f0f86ad71beeebc3d8d5614f Mon Sep 17 00:00:00 2001 From: dushixiang Date: Thu, 25 Dec 2025 19:30:32 +0800 Subject: [PATCH 05/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A3=9E=E4=B9=A6?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E5=8A=A0=E5=AF=86=E5=8F=82=E6=95=B0=E6=97=A0?= =?UTF-8?q?=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/service/notifier.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/internal/service/notifier.go b/internal/service/notifier.go index 7702d38..91c71bf 100644 --- a/internal/service/notifier.go +++ b/internal/service/notifier.go @@ -281,6 +281,7 @@ func (n *Notifier) sendWeCom(ctx context.Context, webhook, message string) error } var wecomAppAccessTokenCache = cache.New[string, string](time.Minute) + func (n *Notifier) getWecomAppToken(ctx context.Context, origin, corpId, corpSecret string) (string, error) { key := fmt.Sprintf("%s#%s", corpId, corpSecret) if token, found := wecomAppAccessTokenCache.Get(key); found { @@ -360,7 +361,7 @@ func (n *Notifier) sendWeComApp(ctx context.Context, origin, corpId, corpSecret } // sendFeishu 发送飞书通知 -func (n *Notifier) sendFeishu(ctx context.Context, webhook, message string) error { +func (n *Notifier) sendFeishu(ctx context.Context, webhook, signSecret, message string) error { body := map[string]interface{}{ "msg_type": "text", "content": map[string]string{ @@ -368,6 +369,23 @@ func (n *Notifier) sendFeishu(ctx context.Context, webhook, message string) erro }, } + // 如果有加签密钥,计算签名 + if signSecret != "" { + timestamp := time.Now().Unix() + stringToSign := fmt.Sprintf("%v", timestamp) + "\n" + signSecret + var data []byte + h := hmac.New(sha256.New, []byte(stringToSign)) + _, err := h.Write(data) + if err != nil { + return err + } + signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) + + // 将签名和时间戳加入请求头 + body["timestamp"] = fmt.Sprintf("%v", timestamp) + body["sign"] = signature + } + _, err := n.sendJSONRequest(ctx, webhook, body) if err != nil { return err @@ -772,7 +790,10 @@ func (n *Notifier) sendFeishuByConfig(ctx context.Context, config map[string]int // 构造 Webhook URL webhook := fmt.Sprintf("https://open.feishu.cn/open-apis/bot/v2/hook/%s", secretKey) - return n.sendFeishu(ctx, webhook, message) + // 检查是否有加签密钥 + signSecret, _ := config["signSecret"].(string) + + return n.sendFeishu(ctx, webhook, signSecret, message) } // sendTelegramByConfig 根据配置发送 Telegram 通知 From f7f8a0a221151b716123c9c213e1e6da421552c4 Mon Sep 17 00:00:00 2001 From: dushixiang Date: Thu, 25 Dec 2025 19:36:05 +0800 Subject: [PATCH 06/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A4=8D=E5=88=B6?= =?UTF-8?q?=E5=AF=86=E9=92=A5=E5=A4=B1=E8=B4=A5=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E4=B8=80=E9=94=AE=E5=AE=89=E8=A3=85=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/pages/Agents/AgentInstall.tsx | 14 ++++---------- web/src/pages/ApiKeys/ApiKeyList.tsx | 22 ++++++++++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/web/src/pages/Agents/AgentInstall.tsx b/web/src/pages/Agents/AgentInstall.tsx index f4aaa66..78b86b8 100644 --- a/web/src/pages/Agents/AgentInstall.tsx +++ b/web/src/pages/Agents/AgentInstall.tsx @@ -122,16 +122,10 @@ const AgentInstall = () => { }), []); // 复制到剪贴板 - const copyToClipboard = useCallback(async (text: string) => { - try { - copy(text); - message.success('已复制到剪贴板'); - } catch (error) { - console.error('Failed to copy:', error); - message.error('复制失败,请手动复制'); - } - }, [message]); - + const copyToClipboard = (text: string) => { + copy(text); + message.success('已复制到剪贴板'); + } // 获取一键安装命令(使用后端检测的地址) const installCommand = useMemo(() => { if (!backendServerUrl || !selectedApiKey) { diff --git a/web/src/pages/ApiKeys/ApiKeyList.tsx b/web/src/pages/ApiKeys/ApiKeyList.tsx index b482b26..e10f890 100644 --- a/web/src/pages/ApiKeys/ApiKeyList.tsx +++ b/web/src/pages/ApiKeys/ApiKeyList.tsx @@ -1,7 +1,7 @@ import {useRef, useState} from 'react'; import type {ActionType, ProColumns} from '@ant-design/pro-components'; import {ProTable} from '@ant-design/pro-components'; -import {App, Button, Divider, Form, Input, message as antMessage, Modal, Popconfirm, Tag,} from 'antd'; +import {App, Button, Divider, Form, Input, Modal, Popconfirm, Tag,} from 'antd'; import {Copy, Edit, Eye, EyeOff, Plus, Power, PowerOff, RefreshCw, Trash2} from 'lucide-react'; import { deleteApiKey, @@ -15,6 +15,7 @@ import type {ApiKey, GenerateApiKeyRequest} from '@/types'; import dayjs from 'dayjs'; import {getErrorMessage} from '@/lib/utils'; import {PageHeader} from '@/components'; +import copy from "copy-to-clipboard"; const ApiKeyList = () => { const {message: messageApi} = App.useApp(); @@ -110,8 +111,8 @@ const ApiKeyList = () => { }; const handleCopyApiKey = (key: string) => { - navigator.clipboard.writeText(key); - antMessage.success('已复制到剪贴板'); + copy(key) + messageApi.success('复制成功'); }; const toggleKeyVisibility = (id: string) => { @@ -139,7 +140,8 @@ const ApiKeyList = () => { const displayText = isVisible ? fullKey : (fullKey.length > 8 ? `${fullKey.substring(0, 8)}...` : fullKey); return (
- + {displayText}