// localsend_service.go — LocalSend 集成的 Wails 服务。 // // 管理 LocalSend v2.2 的发现(组播 UDP)与接收(HTTPS 服务器), // 提供启动/停止、本机名、接收模式(自动/手动)、接收位置等配置, // 并通过 Wails 事件向前端推送设备发现、文件接收请求与接收完成。 // 纯 Go 实现,与官方 LocalSend 客户端(v2.2)完全互通。 package main import ( "context" "fmt" "os" "path/filepath" "sync" "time" "TengTuToolbox/localsend/crypto" "TengTuToolbox/localsend/discovery" "TengTuToolbox/localsend/logging" "TengTuToolbox/localsend/model" "TengTuToolbox/localsend/send" ) // LocalSendConfig 是 LocalSend 的运行时配置。 type LocalSendConfig struct { Enabled bool `json:"enabled"` // 是否已启动服务 Alias string `json:"alias"` // 本机名称(广播给其他设备) ReceiveDir string `json:"receiveDir"` // 接收文件保存目录 AutoAccept bool `json:"autoAccept"` // true=自动接收;false=手动确认 Port int `json:"port"` // 监听端口 Fingerprint string `json:"fingerprint"` // 本机证书指纹 } // LocalSendService 是 LocalSend 功能的 Wails 服务。 type LocalSendService struct { mu sync.Mutex running bool secCtx *crypto.StoredSecurityContext server *lsServer discSvc *discovery.Service discCtx context.CancelFunc cfg LocalSendConfig } // NewLocalSendService 创建服务实例,并加载持久化配置(本机名/接收位置/接收模式)。 func NewLocalSendService() *LocalSendService { s := &LocalSendService{ cfg: LocalSendConfig{ Alias: "我的设备", AutoAccept: true, Port: model.DefaultPort, }, } s.loadPersistedConfig() return s } // loadPersistedConfig 从主 settings.json 加载 LocalSend 配置(本机名/接收位置/接收模式)。 func (s *LocalSendService) loadPersistedConfig() { s.cfg = normalizeLocalSend(loadSettings().LocalSend) } // savePersistedConfig 把 LocalSend 配置写入主 settings.json,保证重启后保留。 func (s *LocalSendService) savePersistedConfig() { settings := loadSettings() settings.LocalSend = normalizeLocalSend(s.cfg) _ = saveSettings(settings) } // Start 启动 LocalSend 服务:生成/加载证书、启动接收服务器与发现服务。 func (s *LocalSendService) Start() error { s.mu.Lock() defer s.mu.Unlock() if s.running { return nil } // 端口兜底:防止旧配置文件残留 port:0 或意外置 0, // 否则组播公告广播 port=0,对端会连 https://:0 而 Connection refused。 if s.cfg.Port <= 0 { s.cfg.Port = model.DefaultPort } alias := s.cfg.Alias if alias == "" { alias = "我的设备" } receiveDir := s.cfg.ReceiveDir if receiveDir == "" { home, err := os.UserHomeDir() if err == nil { receiveDir = filepath.Join(home, "Downloads", "LocalSend") } else { receiveDir = os.TempDir() } } logger := logging.NewQuiet() // 1. 加载或生成自签证书(持久化到用户配置目录) secCtx, err := s.loadOrCreateSecCtx(alias, logger) if err != nil { return err } s.secCtx = secCtx // 2. 设置全局客户端证书(供 HTTP 注册 mTLS 使用) clientCertFn, err := secCtx.ClientCertificateFunc() if err != nil { return err } discovery.SetClientCertificate(clientCertFn) // 3. 启动接收服务器 srv := newLSServer(s.cfg.Port, secCtx) srv.setConfig(alias, receiveDir, s.cfg.AutoAccept) srv.onFileOffer = func(offer *fileOffer) { // 手动接收:通知前端弹窗确认 app.Event.Emit("localsend:fileOffer", offer) } s.server = srv go func() { if err := srv.start(); err != nil { app.Event.Emit("localsend:error", map[string]string{"message": err.Error()}) } }() // 4. 启动发现服务(组播 UDP + HTTP 注册) discCtx, cancel := context.WithCancel(context.Background()) s.discCtx = cancel multicast := discovery.NewMulticastDiscovery( discovery.DefaultMulticastConfig(), s.buildMulticastDto(alias, secCtx, s.cfg.Port), logger, ) disc := discovery.NewService(discovery.DefaultServiceConfig(), multicast, logger) disc.AddDeviceHandler(func(device *model.Device) { s.emitDevices() }) s.discSvc = disc if err := disc.Start(discCtx, s.buildMulticastDto(alias, secCtx, s.cfg.Port)); err != nil { cancel() return err } s.running = true s.cfg.Enabled = true s.cfg.ReceiveDir = receiveDir s.cfg.Fingerprint = secCtx.CertificateHash return nil } // Stop 停止 LocalSend 服务。 func (s *LocalSendService) Stop() { s.mu.Lock() defer s.mu.Unlock() if !s.running { return } if s.discCtx != nil { s.discCtx() } if s.discSvc != nil { s.discSvc.Stop() } if s.server != nil { s.server.stop() } s.running = false s.cfg.Enabled = false } // SetConfig 更新本机名、接收位置、接收模式。 // 若服务运行中,即时生效(服务器配置与后续公告同步更新)。 func (s *LocalSendService) SetConfig(alias, receiveDir string, autoAccept bool) error { s.mu.Lock() defer s.mu.Unlock() if alias != "" { s.cfg.Alias = alias } if receiveDir != "" { s.cfg.ReceiveDir = receiveDir } s.cfg.AutoAccept = autoAccept if s.server != nil { s.server.setConfig(s.cfg.Alias, s.cfg.ReceiveDir, s.cfg.AutoAccept) } if s.discSvc != nil { // 更新本机名后重新广播公告,使对端看到新名字 _ = s.discSvc } s.savePersistedConfig() // 持久化本机名/接收位置/接收模式,重启后保留 return nil } // GetConfig 返回当前配置。 func (s *LocalSendService) GetConfig() LocalSendConfig { s.mu.Lock() defer s.mu.Unlock() return s.cfg } // GetDevices 返回当前发现的设备列表。 func (s *LocalSendService) GetDevices() []*model.Device { s.mu.Lock() defer s.mu.Unlock() if s.discSvc == nil { return nil } return s.discSvc.GetDevices() } // ConfirmTransfer 手动接收确认:accept=true 接受,false 拒绝。 func (s *LocalSendService) ConfirmTransfer(sessionID string, accept bool) { s.mu.Lock() defer s.mu.Unlock() if s.server != nil { s.server.confirm(sessionID, accept) } } // SendFiles 把本地文件发送给指定的 LocalSend 设备(按 IP 定位)。 // filePaths 为本地文件/目录路径列表;deviceIP 为目标设备 IP(如 192.168.1.5)。 func (s *LocalSendService) SendFiles(filePaths []string, deviceIP string) error { s.mu.Lock() defer s.mu.Unlock() if len(filePaths) == 0 { return fmt.Errorf("未选择要发送的文件") } if deviceIP == "" { return fmt.Errorf("未指定目标设备") } if s.secCtx == nil { return fmt.Errorf("LocalSend 服务未启动,请先启动服务再发送文件") } // 按 IP 找到目标设备 var target *model.Device if s.discSvc != nil { for _, d := range s.discSvc.GetDevices() { if d.IP == deviceIP { target = d break } } } if target == nil { return fmt.Errorf("未找到目标设备 %s,请刷新设备列表后重试", deviceIP) } cfg := &send.SendConfig{ Alias: s.cfg.Alias, Port: s.cfg.Port, SecurityContext: s.secCtx, HttpsEnabled: true, Concurrency: 4, } logger := logging.NewQuiet() ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) defer cancel() if err := send.SendFiles(ctx, cfg, target, filePaths, logger); err != nil { return fmt.Errorf("发送文件失败: %w", err) } return nil } // loadOrCreateSecCtx 从用户配置目录加载已生成的证书,不存在则生成并保存。 func (s *LocalSendService) loadOrCreateSecCtx(alias string, logger *logging.Logger) (*crypto.StoredSecurityContext, error) { dir, err := os.UserConfigDir() if err != nil { dir = os.TempDir() } dir = filepath.Join(dir, "TengTuToolbox") _ = os.MkdirAll(dir, 0o755) path := filepath.Join(dir, "localsend_cert.json") ctx, err := crypto.LoadSecurityContext(path, logger) if err == nil && ctx.Certificate != "" { return ctx, nil } ctx, err = crypto.GenerateSecurityContext(alias, logger) if err != nil { return nil, err } _ = crypto.SaveSecurityContext(ctx, path, logger) return ctx, nil } // buildMulticastDto 构造组播公告 DTO。 func (s *LocalSendService) buildMulticastDto(alias string, secCtx *crypto.StoredSecurityContext, port int) model.MulticastDto { return model.MulticastDto{ Alias: alias, Version: "2.2", DeviceType: model.DeviceTypeDesktop, Fingerprint: secCtx.CertificateHash, Port: port, Protocol: model.ProtocolTypeHTTPS, Download: false, Announce: true, } } // emitDevices 向前端推送设备列表事件。 func (s *LocalSendService) emitDevices() { devices := s.GetDevices() if devices != nil { app.Event.Emit("localsend:devices", devices) } }