reactor(cli): move memoh cli to tui

1. Split the oversized `cmd/agent` entrypoint into a multi-file package and update dev/build scripts to use the package path instead of compiling `main.go` directly.
2. Add a new `memoh` terminal UI for local bot chat, with Bubble Tea
This commit is contained in:
晨苒
2026-04-14 00:39:34 +08:00
parent 8c9f222783
commit d50eeea114
32 changed files with 2140 additions and 1962 deletions
+1056
View File
File diff suppressed because it is too large Load Diff
+5 -1300
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,5 +4,5 @@ dir = "{{cwd}}"
[tasks.start]
alias = "dev"
description = "Start server"
run = "go run cmd/agent/main.go"
run = "go run cmd/agent"
depends = ["//:go-install"]
+160
View File
@@ -0,0 +1,160 @@
package main
import (
"log/slog"
"go.uber.org/fx"
"go.uber.org/fx/fxevent"
"github.com/memohai/memoh/internal/accounts"
"github.com/memohai/memoh/internal/acl"
"github.com/memohai/memoh/internal/bind"
"github.com/memohai/memoh/internal/boot"
"github.com/memohai/memoh/internal/bots"
"github.com/memohai/memoh/internal/browsercontexts"
"github.com/memohai/memoh/internal/channel"
"github.com/memohai/memoh/internal/channel/adapters/local"
"github.com/memohai/memoh/internal/channel/adapters/weixin"
"github.com/memohai/memoh/internal/channel/identities"
"github.com/memohai/memoh/internal/compaction"
"github.com/memohai/memoh/internal/conversation"
emailpkg "github.com/memohai/memoh/internal/email"
"github.com/memohai/memoh/internal/handlers"
"github.com/memohai/memoh/internal/heartbeat"
"github.com/memohai/memoh/internal/mcp"
memprovider "github.com/memohai/memoh/internal/memory/adapters"
"github.com/memohai/memoh/internal/message/event"
"github.com/memohai/memoh/internal/models"
"github.com/memohai/memoh/internal/policy"
"github.com/memohai/memoh/internal/schedule"
"github.com/memohai/memoh/internal/searchproviders"
"github.com/memohai/memoh/internal/settings"
ttspkg "github.com/memohai/memoh/internal/tts"
)
func runServe() {
fx.New(options()).Run()
}
func options() fx.Option {
return fx.Options(
fx.Provide(
provideConfig,
boot.ProvideRuntimeConfig,
provideLogger,
provideContainerService,
provideDBConn,
provideDBQueries,
provideWorkspaceManager,
provideMemoryLLM,
memprovider.NewService,
provideMemoryProviderRegistry,
models.NewService,
bots.NewService,
accounts.NewService,
acl.NewService,
settings.NewService,
provideProvidersService,
searchproviders.NewService,
browsercontexts.NewService,
policy.NewService,
mcp.NewConnectionService,
conversation.NewService,
identities.NewService,
bind.NewService,
event.NewHub,
provideTtsRegistry,
ttspkg.NewService,
provideTtsTempStore,
emailpkg.NewDBOAuthTokenStore,
provideEmailRegistry,
emailpkg.NewService,
emailpkg.NewOutboxService,
provideEmailChatGateway,
provideEmailTrigger,
emailpkg.NewManager,
provideRouteService,
provideSessionService,
provideMessageService,
provideMediaService,
providePipeline,
provideEventStore,
provideDiscussDriver,
local.NewRouteHub,
provideChannelRegistry,
channel.NewStore,
provideChannelRouter,
provideChannelManager,
provideChannelLifecycleService,
provideAgent,
provideChatResolver,
provideScheduleTriggerer,
provideHeartbeatSessionCreator,
provideScheduleSessionCreator,
schedule.NewService,
provideHeartbeatTriggerer,
heartbeat.NewService,
compaction.NewService,
provideContainerdHandler,
provideFederationGateway,
provideToolGatewayService,
provideBackgroundManager,
provideToolProviders,
provideServerHandler(handlers.NewPingHandler),
provideServerHandler(provideAuthHandler),
provideServerHandler(provideMemoryHandler),
provideServerHandler(provideMessageHandler),
provideServerHandler(provideSessionHandler),
provideServerHandler(handlers.NewSwaggerHandler),
provideServerHandler(handlers.NewProvidersHandler),
provideServerHandler(handlers.NewProviderOAuthHandler),
provideServerHandler(handlers.NewSearchProvidersHandler),
provideServerHandler(handlers.NewModelsHandler),
provideServerHandler(handlers.NewSettingsHandler),
provideServerHandler(handlers.NewACLHandler),
provideServerHandler(handlers.NewBindHandler),
provideServerHandler(handlers.NewScheduleHandler),
provideServerHandler(handlers.NewHeartbeatHandler),
provideServerHandler(handlers.NewCompactionHandler),
provideServerHandler(handlers.NewChannelHandler),
provideServerHandler(channel.NewWebhookServerHandler),
provideServerHandler(weixin.NewQRServerHandler),
provideServerHandler(provideUsersHandler),
provideServerHandler(handlers.NewMemoryProvidersHandler),
provideServerHandler(handlers.NewSpeechHandler),
provideServerHandler(handlers.NewBotTtsHandler),
provideServerHandler(handlers.NewEmailProvidersHandler),
provideServerHandler(handlers.NewEmailBindingsHandler),
provideServerHandler(handlers.NewEmailOutboxHandler),
provideServerHandler(handlers.NewEmailWebhookHandler),
provideServerHandler(provideEmailOAuthHandler),
provideServerHandler(handlers.NewMCPHandler),
provideServerHandler(handlers.NewMCPOAuthHandler),
provideOAuthService,
provideServerHandler(handlers.NewTokenUsageHandler),
provideServerHandler(handlers.NewSessionInfoHandler),
provideServerHandler(handlers.NewBrowserContextsHandler),
provideServerHandler(handlers.NewSupermarketHandler),
provideServerHandler(provideWebHandler),
provideServer,
),
fx.Invoke(
injectToolProviders,
startRegistrySync,
startMemoryProviderBootstrap,
startSearchProviderBootstrap,
startScheduleService,
startHeartbeatService,
wireResolverOutbound,
startChannelManager,
startEmailManager,
startContainerReconciliation,
startBackgroundTaskCleanup,
startTtsTempStoreCleanup,
startServer,
),
fx.WithLogger(func(logger *slog.Logger) fxevent.Logger {
return &fxevent.SlogLogger{Logger: logger.With(slog.String("component", "fx"))}
}),
)
}
+58
View File
@@ -0,0 +1,58 @@
package main
import (
"fmt"
"io/fs"
"log/slog"
"os"
dbembed "github.com/memohai/memoh/db"
"github.com/memohai/memoh/internal/config"
"github.com/memohai/memoh/internal/db"
"github.com/memohai/memoh/internal/logger"
"github.com/memohai/memoh/internal/version"
)
func provideConfig() (config.Config, error) {
cfgPath := os.Getenv("CONFIG_PATH")
cfg, err := config.Load(cfgPath)
if err != nil {
return config.Config{}, fmt.Errorf("load config: %w", err)
}
return cfg, nil
}
func migrationsFS() fs.FS {
sub, err := fs.Sub(dbembed.MigrationsFS, "migrations")
if err != nil {
panic(fmt.Sprintf("embedded migrations: %v", err))
}
return sub
}
func runMigrateCommand(args []string) error {
cfg, err := provideConfig()
if err != nil {
return fmt.Errorf("config: %w", err)
}
logger.Init(cfg.Log.Level, cfg.Log.Format)
log := logger.L
migrateCmd := args[0]
var migrateArgs []string
if len(args) > 1 {
migrateArgs = args[1:]
}
if err := db.RunMigrate(log, cfg.Postgres, migrationsFS(), migrateCmd, migrateArgs); err != nil {
log.Error("migration failed", slog.Any("error", err))
return err
}
return nil
}
func runVersion() error {
fmt.Printf("memoh-server %s\n", version.GetInfo())
return nil
}