feat(frontend) : moving frontend to webpage
This commit is contained in:
@@ -62,12 +62,19 @@ go install github.com/sjzar/chatlog@latest
|
||||
|
||||
### Terminal UI 模式
|
||||
|
||||
最简单的使用方式是通过 Terminal UI 界面操作:
|
||||
最简单的使用方式是通过 web 界面操作:
|
||||
|
||||
```bash
|
||||
chatlog
|
||||
```
|
||||
|
||||
如果遇到了问题,可以按下面的命令打开调试/控制台前端模式:
|
||||
|
||||
```bash
|
||||
chatlog --debug // 打开调试模式
|
||||
chatlog --console // 打开控制台前端模式
|
||||
```
|
||||
|
||||
操作方法:
|
||||
|
||||
- 使用 `↑` `↓` 键选择菜单项
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# TODO
|
||||
|
||||
- [ ] 修复 html 渲染部分头像缺失的问题
|
||||
- [x] 美化搜索界面使风格一致
|
||||
- [ ] 实现统一接口管理设置的功能
|
||||
- [ ] 使用 python 重新实现语音转文字功能(或使用 docker 部署 whisper)
|
||||
- [ ] 美化搜索界面使风格一致
|
||||
- [x] 实现统一接口管理设置的功能
|
||||
- [x] 使用 python 重新实现语音转文字功能(或使用 docker 部署 whisper)
|
||||
|
||||
+4
-1
@@ -14,7 +14,10 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var Debug bool
|
||||
var (
|
||||
Debug bool
|
||||
Console bool
|
||||
)
|
||||
|
||||
func initLog(cmd *cobra.Command, args []string) {
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
|
||||
+27
-1
@@ -1,7 +1,10 @@
|
||||
package chatlog
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/chatlog"
|
||||
"github.com/sjzar/chatlog/pkg/util"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -12,6 +15,7 @@ func init() {
|
||||
cobra.MousetrapHelpText = ""
|
||||
|
||||
rootCmd.PersistentFlags().BoolVar(&Debug, "debug", false, "debug")
|
||||
rootCmd.PersistentFlags().BoolVar(&Console, "console", false, "run with console interface")
|
||||
rootCmd.PersistentPreRun = initLog
|
||||
}
|
||||
|
||||
@@ -30,12 +34,34 @@ var rootCmd = &cobra.Command{
|
||||
CompletionOptions: cobra.CompletionOptions{
|
||||
HiddenDefaultCmd: true,
|
||||
},
|
||||
PreRun: initTuiLog,
|
||||
PreRun: prepareRoot,
|
||||
Run: Root,
|
||||
}
|
||||
|
||||
func prepareRoot(cmd *cobra.Command, args []string) {
|
||||
if Console || !Debug {
|
||||
initTuiLog(cmd, args)
|
||||
}
|
||||
}
|
||||
|
||||
func Root(cmd *cobra.Command, args []string) {
|
||||
m := chatlog.New()
|
||||
mode := chatlog.RunModeHeadless
|
||||
autoOpen := true
|
||||
if Console {
|
||||
mode = chatlog.RunModeConsole
|
||||
autoOpen = false
|
||||
}
|
||||
m.SetRunOptions(chatlog.RunOptions{
|
||||
Mode: mode,
|
||||
AutoOpenBrowser: autoOpen,
|
||||
AutoOpenBrowserSet: true,
|
||||
})
|
||||
|
||||
if runtime.GOOS == "windows" && !Console && !Debug {
|
||||
util.HideConsoleWindow()
|
||||
}
|
||||
|
||||
if err := m.Run(""); err != nil {
|
||||
log.Err(err).Msg("failed to run chatlog instance")
|
||||
}
|
||||
|
||||
@@ -1176,7 +1176,7 @@ func (a *App) settingDataKey() {
|
||||
|
||||
// 添加按钮 - 点击保存时才设置数据密钥
|
||||
formView.AddButton("保存", func() {
|
||||
a.ctx.DataKey = tempDataKey // 设置数据密钥
|
||||
a.ctx.SetDataKey(tempDataKey)
|
||||
a.mainPages.RemovePage("submenu2")
|
||||
a.refreshSettingsMenu()
|
||||
a.showInfo("数据密钥已设置")
|
||||
@@ -1230,7 +1230,7 @@ func (a *App) settingDataDir() {
|
||||
|
||||
// 添加按钮 - 点击保存时才设置数据目录
|
||||
formView.AddButton("保存", func() {
|
||||
a.ctx.DataDir = tempDataDir // 设置数据目录
|
||||
a.ctx.SetDataDir(tempDataDir)
|
||||
a.mainPages.RemovePage("submenu2")
|
||||
a.refreshSettingsMenu()
|
||||
a.showInfo("数据目录已设置为 " + a.ctx.DataDir)
|
||||
|
||||
@@ -63,3 +63,31 @@ func (c *ServerConfig) GetWebhook() *Webhook {
|
||||
func (c *ServerConfig) GetSpeech() *SpeechConfig {
|
||||
return c.Speech
|
||||
}
|
||||
|
||||
func (c *ServerConfig) SetHTTPAddr(addr string) {
|
||||
c.HTTPAddr = addr
|
||||
}
|
||||
|
||||
func (c *ServerConfig) SetWorkDir(dir string) {
|
||||
c.WorkDir = dir
|
||||
}
|
||||
|
||||
func (c *ServerConfig) SetDataDir(dir string) {
|
||||
c.DataDir = dir
|
||||
}
|
||||
|
||||
func (c *ServerConfig) SetImgKey(key string) {
|
||||
c.ImgKey = key
|
||||
}
|
||||
|
||||
func (c *ServerConfig) SetDataKey(key string) {
|
||||
c.DataKey = key
|
||||
}
|
||||
|
||||
func (c *ServerConfig) IsHTTPEnabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *ServerConfig) IsAutoDecrypt() bool {
|
||||
return c.AutoDecrypt
|
||||
}
|
||||
|
||||
@@ -244,6 +244,10 @@ func (c *Context) GetDataKey() string {
|
||||
return c.DataKey
|
||||
}
|
||||
|
||||
func (c *Context) GetImgKey() string {
|
||||
return c.ImgKey
|
||||
}
|
||||
|
||||
func (c *Context) GetHTTPAddr() string {
|
||||
if c.HTTPAddr == "" {
|
||||
c.HTTPAddr = DefalutHTTPAddr
|
||||
@@ -251,6 +255,14 @@ func (c *Context) GetHTTPAddr() string {
|
||||
return c.HTTPAddr
|
||||
}
|
||||
|
||||
func (c *Context) IsHTTPEnabled() bool {
|
||||
return c.HTTPEnabled
|
||||
}
|
||||
|
||||
func (c *Context) IsAutoDecrypt() bool {
|
||||
return c.AutoDecrypt
|
||||
}
|
||||
|
||||
func (c *Context) GetWebhook() *conf.Webhook {
|
||||
return c.conf.Webhook
|
||||
}
|
||||
@@ -311,6 +323,16 @@ func (c *Context) SetImgKey(key string) {
|
||||
c.UpdateConfig()
|
||||
}
|
||||
|
||||
func (c *Context) SetDataKey(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.DataKey == key {
|
||||
return
|
||||
}
|
||||
c.DataKey = key
|
||||
c.UpdateConfig()
|
||||
}
|
||||
|
||||
func (c *Context) SetAutoDecrypt(enabled bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (s *Service) handleActionGetDataKey(c *gin.Context) {
|
||||
if s.control == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "control service unavailable"})
|
||||
return
|
||||
}
|
||||
if err := s.control.GetDataKey(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Service) handleActionDecrypt(c *gin.Context) {
|
||||
if s.control == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "control service unavailable"})
|
||||
return
|
||||
}
|
||||
if err := s.control.DecryptDBFiles(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Service) handleActionStartHTTP(c *gin.Context) {
|
||||
if s.control == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "control service unavailable"})
|
||||
return
|
||||
}
|
||||
if s.conf.IsHTTPEnabled() {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "already_running"})
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
if err := s.control.StartService(); err != nil {
|
||||
log.Err(err).Msg("failed to start http service via api")
|
||||
}
|
||||
}()
|
||||
c.JSON(http.StatusAccepted, gin.H{"status": "starting"})
|
||||
}
|
||||
|
||||
func (s *Service) handleActionStopHTTP(c *gin.Context) {
|
||||
if s.control == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "control service unavailable"})
|
||||
return
|
||||
}
|
||||
if !s.conf.IsHTTPEnabled() {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "already_stopped"})
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
if err := s.control.StopService(); err != nil {
|
||||
log.Err(err).Msg("failed to stop http service via api")
|
||||
}
|
||||
}()
|
||||
c.JSON(http.StatusAccepted, gin.H{"status": "stopping"})
|
||||
}
|
||||
|
||||
func (s *Service) handleActionStartAutoDecrypt(c *gin.Context) {
|
||||
if s.control == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "control service unavailable"})
|
||||
return
|
||||
}
|
||||
if err := s.control.StartAutoDecrypt(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Service) handleActionStopAutoDecrypt(c *gin.Context) {
|
||||
if s.control == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "control service unavailable"})
|
||||
return
|
||||
}
|
||||
if err := s.control.StopAutoDecrypt(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sjzar/chatlog/internal/chatlog/conf"
|
||||
)
|
||||
|
||||
type settingRequest struct {
|
||||
HTTPAddr *string `json:"http_addr"`
|
||||
WorkDir *string `json:"work_dir"`
|
||||
DataDir *string `json:"data_dir"`
|
||||
DataKey *string `json:"data_key"`
|
||||
ImgKey *string `json:"img_key"`
|
||||
Speech *conf.SpeechConfig `json:"speech"`
|
||||
}
|
||||
|
||||
type settingResponse struct {
|
||||
HTTPAddr string `json:"http_addr"`
|
||||
HTTPEnabled bool `json:"http_enabled"`
|
||||
WorkDir string `json:"work_dir"`
|
||||
DataDir string `json:"data_dir"`
|
||||
DataKey string `json:"data_key"`
|
||||
ImgKey string `json:"img_key"`
|
||||
AutoDecrypt bool `json:"auto_decrypt"`
|
||||
Speech *conf.SpeechConfig `json:"speech"`
|
||||
}
|
||||
|
||||
func (s *Service) handleGetSetting(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, s.buildSettingResponse())
|
||||
}
|
||||
|
||||
func (s *Service) handleUpdateSetting(c *gin.Context) {
|
||||
var req settingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload", "detail": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.HTTPAddr != nil {
|
||||
if s.control == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "control service unavailable"})
|
||||
return
|
||||
}
|
||||
trimmed := strings.TrimSpace(*req.HTTPAddr)
|
||||
if err := s.control.SetHTTPAddr(trimmed); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.WorkDir != nil {
|
||||
s.conf.SetWorkDir(strings.TrimSpace(*req.WorkDir))
|
||||
}
|
||||
|
||||
if req.DataDir != nil {
|
||||
s.conf.SetDataDir(strings.TrimSpace(*req.DataDir))
|
||||
}
|
||||
|
||||
if req.DataKey != nil {
|
||||
s.conf.SetDataKey(strings.TrimSpace(*req.DataKey))
|
||||
}
|
||||
|
||||
if req.ImgKey != nil {
|
||||
s.conf.SetImgKey(strings.TrimSpace(*req.ImgKey))
|
||||
}
|
||||
|
||||
if req.Speech != nil {
|
||||
if s.control == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "control service unavailable"})
|
||||
return
|
||||
}
|
||||
speechCopy := *req.Speech
|
||||
if err := s.control.SaveSpeechConfig(&speechCopy); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, s.buildSettingResponse())
|
||||
}
|
||||
|
||||
func (s *Service) buildSettingResponse() settingResponse {
|
||||
resp := settingResponse{
|
||||
HTTPAddr: s.conf.GetHTTPAddr(),
|
||||
HTTPEnabled: s.conf.IsHTTPEnabled(),
|
||||
WorkDir: s.conf.GetWorkDir(),
|
||||
DataDir: s.conf.GetDataDir(),
|
||||
DataKey: s.conf.GetDataKey(),
|
||||
ImgKey: s.conf.GetImgKey(),
|
||||
AutoDecrypt: s.conf.IsAutoDecrypt(),
|
||||
}
|
||||
|
||||
if cfg := s.conf.GetSpeech(); cfg != nil {
|
||||
copyCfg := *cfg
|
||||
resp.Speech = ©Cfg
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
@@ -162,15 +162,27 @@ func (s *Service) initMediaRouter() {
|
||||
}
|
||||
|
||||
func (s *Service) initAPIRouter() {
|
||||
api := s.router.Group("/api/v1", s.checkDBStateMiddleware())
|
||||
api := s.router.Group("/api/v1")
|
||||
{
|
||||
api.GET("/chatlog", s.handleChatlog)
|
||||
api.GET("/contact", s.handleContacts)
|
||||
api.GET("/chatroom", s.handleChatRooms)
|
||||
api.GET("/session", s.handleSessions)
|
||||
api.GET("/diary", s.handleDiary)
|
||||
api.GET("/dashboard", s.handleDashboard)
|
||||
api.GET("/search", s.handleSearch)
|
||||
api.GET("/setting", s.handleGetSetting)
|
||||
api.POST("/setting", s.handleUpdateSetting)
|
||||
|
||||
actions := api.Group("/actions")
|
||||
actions.POST("/get-data-key", s.handleActionGetDataKey)
|
||||
actions.POST("/decrypt", s.handleActionDecrypt)
|
||||
actions.POST("/http/start", s.handleActionStartHTTP)
|
||||
actions.POST("/http/stop", s.handleActionStopHTTP)
|
||||
actions.POST("/auto-decrypt/start", s.handleActionStartAutoDecrypt)
|
||||
actions.POST("/auto-decrypt/stop", s.handleActionStopAutoDecrypt)
|
||||
|
||||
dataAPI := api.Group("", s.checkDBStateMiddleware())
|
||||
dataAPI.GET("/chatlog", s.handleChatlog)
|
||||
dataAPI.GET("/contact", s.handleContacts)
|
||||
dataAPI.GET("/chatroom", s.handleChatRooms)
|
||||
dataAPI.GET("/session", s.handleSessions)
|
||||
dataAPI.GET("/diary", s.handleDiary)
|
||||
dataAPI.GET("/dashboard", s.handleDashboard)
|
||||
dataAPI.GET("/search", s.handleSearch)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ import (
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
conf Config
|
||||
db *database.Service
|
||||
conf Config
|
||||
db *database.Service
|
||||
control Control
|
||||
|
||||
router *gin.Engine
|
||||
server *http.Server
|
||||
@@ -33,11 +34,32 @@ type Service struct {
|
||||
|
||||
type Config interface {
|
||||
GetHTTPAddr() string
|
||||
SetHTTPAddr(string)
|
||||
GetDataDir() string
|
||||
SetDataDir(string)
|
||||
GetWorkDir() string
|
||||
SetWorkDir(string)
|
||||
GetDataKey() string
|
||||
SetDataKey(string)
|
||||
GetImgKey() string
|
||||
SetImgKey(string)
|
||||
IsHTTPEnabled() bool
|
||||
IsAutoDecrypt() bool
|
||||
GetSpeech() *conf.SpeechConfig
|
||||
}
|
||||
|
||||
func NewService(conf Config, db *database.Service) *Service {
|
||||
type Control interface {
|
||||
GetDataKey() error
|
||||
DecryptDBFiles() error
|
||||
StartService() error
|
||||
StopService() error
|
||||
StartAutoDecrypt() error
|
||||
StopAutoDecrypt() error
|
||||
SaveSpeechConfig(cfg *conf.SpeechConfig) error
|
||||
SetHTTPAddr(addr string) error
|
||||
}
|
||||
|
||||
func NewService(conf Config, db *database.Service, control Control) *Service {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.New()
|
||||
|
||||
@@ -55,9 +77,10 @@ func NewService(conf Config, db *database.Service) *Service {
|
||||
)
|
||||
|
||||
s := &Service{
|
||||
conf: conf,
|
||||
db: db,
|
||||
router: router,
|
||||
conf: conf,
|
||||
db: db,
|
||||
control: control,
|
||||
router: router,
|
||||
}
|
||||
|
||||
s.initMCPServer()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+106
-10
@@ -4,8 +4,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -22,6 +24,19 @@ import (
|
||||
|
||||
const initialDecryptPollInterval = 5 * time.Second
|
||||
|
||||
type RunMode int
|
||||
|
||||
const (
|
||||
RunModeHeadless RunMode = iota
|
||||
RunModeConsole
|
||||
)
|
||||
|
||||
type RunOptions struct {
|
||||
Mode RunMode
|
||||
AutoOpenBrowser bool
|
||||
AutoOpenBrowserSet bool
|
||||
}
|
||||
|
||||
// Manager 管理聊天日志应用
|
||||
type Manager struct {
|
||||
ctx *ctx.Context
|
||||
@@ -36,13 +51,32 @@ type Manager struct {
|
||||
// Terminal UI
|
||||
app *App
|
||||
|
||||
options RunOptions
|
||||
|
||||
initialDecryptOnce sync.Once
|
||||
initialDecryptMu sync.Mutex
|
||||
initialDecryptLastErr string
|
||||
}
|
||||
|
||||
func New() *Manager {
|
||||
return &Manager{}
|
||||
return &Manager{
|
||||
options: RunOptions{
|
||||
Mode: RunModeHeadless,
|
||||
AutoOpenBrowser: true,
|
||||
AutoOpenBrowserSet: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) SetRunOptions(opts RunOptions) {
|
||||
if opts.Mode != RunModeConsole {
|
||||
opts.Mode = RunModeHeadless
|
||||
}
|
||||
if !opts.AutoOpenBrowserSet {
|
||||
opts.AutoOpenBrowser = m.options.AutoOpenBrowser
|
||||
opts.AutoOpenBrowserSet = m.options.AutoOpenBrowserSet
|
||||
}
|
||||
m.options = opts
|
||||
}
|
||||
|
||||
func (m *Manager) Run(configPath string) error {
|
||||
@@ -57,7 +91,7 @@ func (m *Manager) Run(configPath string) error {
|
||||
|
||||
m.db = database.NewService(m.ctx)
|
||||
|
||||
m.http = http.NewService(m.ctx, m.db)
|
||||
m.http = http.NewService(m.ctx, m.db, m)
|
||||
|
||||
instances := m.wechat.GetWeChatInstances()
|
||||
m.ctx.SetWeChatInstances(instances)
|
||||
@@ -67,18 +101,76 @@ func (m *Manager) Run(configPath string) error {
|
||||
|
||||
m.startInitialDecryptWatcher()
|
||||
|
||||
if m.ctx.HTTPEnabled {
|
||||
// 启动HTTP服务
|
||||
wantHTTP := m.ctx.HTTPEnabled || m.options.Mode == RunModeHeadless
|
||||
if wantHTTP {
|
||||
if err := m.StartService(); err != nil {
|
||||
m.StopService()
|
||||
m.stopService()
|
||||
if m.options.Mode == RunModeHeadless {
|
||||
return err
|
||||
}
|
||||
log.Err(err).Msg("failed to start HTTP service")
|
||||
}
|
||||
}
|
||||
// 启动终端UI
|
||||
m.app = NewApp(m.ctx, m)
|
||||
m.app.Run() // 阻塞
|
||||
|
||||
if m.options.Mode == RunModeConsole {
|
||||
m.app = NewApp(m.ctx, m)
|
||||
return m.app.Run()
|
||||
}
|
||||
|
||||
if url := m.webInterfaceURL(); url != "" {
|
||||
log.Info().Str("url", url).Msg("Chatlog web interface available")
|
||||
if m.options.AutoOpenBrowser {
|
||||
m.launchBrowser(url)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Msg("Chatlog is running in headless mode. Press Ctrl+C to exit.")
|
||||
m.waitForShutdown()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) webInterfaceURL() string {
|
||||
if m.ctx == nil {
|
||||
return ""
|
||||
}
|
||||
addr := strings.TrimSpace(m.ctx.GetHTTPAddr())
|
||||
if addr == "" {
|
||||
return ""
|
||||
}
|
||||
return util.ComposeLANURL(addr)
|
||||
}
|
||||
|
||||
func (m *Manager) launchBrowser(url string) {
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
log.Debug().Str("url", url).Msg("launching default browser")
|
||||
if err := util.OpenBrowser(url); err != nil {
|
||||
log.Warn().Err(err).Str("url", url).Msg("failed to open browser")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *Manager) waitForShutdown() {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
|
||||
defer signal.Stop(sigCh)
|
||||
|
||||
sig := <-sigCh
|
||||
log.Info().Msgf("received signal %s, shutting down", sig)
|
||||
|
||||
if m.wechat != nil && m.ctx != nil && m.ctx.IsAutoDecrypt() {
|
||||
if err := m.wechat.StopAutoDecrypt(); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to stop auto decrypt during shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.stopService(); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to stop services during shutdown")
|
||||
}
|
||||
|
||||
log.Info().Msg("Shutdown complete")
|
||||
}
|
||||
|
||||
func (m *Manager) startInitialDecryptWatcher() {
|
||||
m.initialDecryptOnce.Do(func() {
|
||||
go m.initialDecryptLoop()
|
||||
@@ -260,7 +352,11 @@ func (m *Manager) SetHTTPAddr(text string) error {
|
||||
} else {
|
||||
addr = text
|
||||
}
|
||||
m.ctx.SetHTTPAddr(addr)
|
||||
if m.ctx != nil {
|
||||
m.ctx.SetHTTPAddr(addr)
|
||||
} else if m.sc != nil {
|
||||
m.sc.SetHTTPAddr(addr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -476,7 +572,7 @@ func (m *Manager) CommandHTTPServer(configPath string, cmdConf map[string]any) e
|
||||
|
||||
m.db = database.NewService(m.sc)
|
||||
|
||||
m.http = http.NewService(m.sc, m.db)
|
||||
m.http = http.NewService(m.sc, m.db, m)
|
||||
|
||||
if m.sc.GetAutoDecrypt() {
|
||||
if err := m.wechat.StartAutoDecrypt(); err != nil {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package util
|
||||
|
||||
import "fmt"
|
||||
|
||||
// OpenBrowser launches the user's default browser with the provided URL.
|
||||
func OpenBrowser(url string) error {
|
||||
cmd, err := browserCommand(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start browser: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//go:build !windows
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func browserCommand(url string) (*exec.Cmd, error) {
|
||||
if url == "" {
|
||||
return nil, errors.New("empty url")
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return exec.Command("open", url), nil
|
||||
default:
|
||||
return exec.Command("xdg-open", url), nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//go:build windows
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func browserCommand(url string) (*exec.Cmd, error) {
|
||||
if url == "" {
|
||||
return nil, errors.New("empty url")
|
||||
}
|
||||
cmd := exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
return cmd, nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
//go:build !windows
|
||||
|
||||
package util
|
||||
|
||||
// HideConsoleWindow is a no-op on non-Windows platforms.
|
||||
func HideConsoleWindow() {}
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build windows
|
||||
|
||||
package util
|
||||
|
||||
import "syscall"
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procFreeConsole = kernel32.NewProc("FreeConsole")
|
||||
)
|
||||
|
||||
// HideConsoleWindow detaches the process from the current console so no window is shown.
|
||||
func HideConsoleWindow() {
|
||||
procFreeConsole.Call()
|
||||
}
|
||||
Reference in New Issue
Block a user