mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat(command): extend slash command system with new commands and UX improvements
Add 9 new command groups (/model, /memory, /search, /browser, /usage, /email, /heartbeat, /skill, /fs) and improve existing commands by hiding internal UUIDs, resolving IDs to human-readable names in /settings, and switching /schedule to name-based references.
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
stdpath "path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -34,6 +35,7 @@ import (
|
||||
"github.com/memohai/memoh/internal/channel/identities"
|
||||
"github.com/memohai/memoh/internal/channel/inbound"
|
||||
"github.com/memohai/memoh/internal/channel/route"
|
||||
"github.com/memohai/memoh/internal/command"
|
||||
"github.com/memohai/memoh/internal/config"
|
||||
ctr "github.com/memohai/memoh/internal/containerd"
|
||||
"github.com/memohai/memoh/internal/conversation"
|
||||
@@ -423,6 +425,21 @@ func provideChannelRouter(
|
||||
bindService *bind.Service,
|
||||
mediaService *media.Service,
|
||||
inboxService *inbox.Service,
|
||||
subagentService *subagent.Service,
|
||||
scheduleService *schedule.Service,
|
||||
settingsService *settings.Service,
|
||||
mcpConnService *mcp.ConnectionService,
|
||||
modelsService *models.Service,
|
||||
providersService *providers.Service,
|
||||
memProvService *memprovider.Service,
|
||||
searchProvService *searchproviders.Service,
|
||||
browserCtxService *browsercontexts.Service,
|
||||
emailService *emailpkg.Service,
|
||||
emailOutboxService *emailpkg.OutboxService,
|
||||
heartbeatService *heartbeat.Service,
|
||||
queries *dbsqlc.Queries,
|
||||
containerdHandler *handlers.ContainerdHandler,
|
||||
manager *mcp.Manager,
|
||||
rc *boot.RuntimeConfig,
|
||||
) *inbound.ChannelInboundProcessor {
|
||||
adapter, ok := registry.Get(qq.Type)
|
||||
@@ -440,6 +457,26 @@ func provideChannelRouter(
|
||||
processor.SetMediaService(mediaService)
|
||||
processor.SetStreamObserver(local.NewRouteHubBroadcaster(hub))
|
||||
processor.SetInboxService(inboxService)
|
||||
processor.SetCommandHandler(command.NewHandler(
|
||||
log,
|
||||
&command.BotMemberRoleAdapter{BotService: botService},
|
||||
subagentService,
|
||||
scheduleService,
|
||||
settingsService,
|
||||
mcpConnService,
|
||||
inboxService,
|
||||
modelsService,
|
||||
providersService,
|
||||
memProvService,
|
||||
searchProvService,
|
||||
browserCtxService,
|
||||
emailService,
|
||||
emailOutboxService,
|
||||
heartbeatService,
|
||||
queries,
|
||||
&commandSkillLoaderAdapter{handler: containerdHandler},
|
||||
&commandContainerFSAdapter{manager: manager},
|
||||
))
|
||||
return processor
|
||||
}
|
||||
|
||||
@@ -928,3 +965,54 @@ func (a *gatewayAssetLoaderAdapter) OpenForGateway(ctx context.Context, botID, c
|
||||
}
|
||||
return reader, strings.TrimSpace(asset.Mime), nil
|
||||
}
|
||||
|
||||
// commandSkillLoaderAdapter bridges handlers.ContainerdHandler to command.SkillLoader.
|
||||
type commandSkillLoaderAdapter struct {
|
||||
handler *handlers.ContainerdHandler
|
||||
}
|
||||
|
||||
func (a *commandSkillLoaderAdapter) LoadSkills(ctx context.Context, botID string) ([]command.Skill, error) {
|
||||
items, err := a.handler.LoadSkills(ctx, botID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
skills := make([]command.Skill, len(items))
|
||||
for i, item := range items {
|
||||
skills[i] = command.Skill{Name: item.Name, Description: item.Description}
|
||||
}
|
||||
return skills, nil
|
||||
}
|
||||
|
||||
// commandContainerFSAdapter bridges mcp.Manager to command.ContainerFS.
|
||||
type commandContainerFSAdapter struct {
|
||||
manager *mcp.Manager
|
||||
}
|
||||
|
||||
func (a *commandContainerFSAdapter) ListDir(ctx context.Context, botID, dirPath string) ([]command.FSEntry, error) {
|
||||
client, err := a.manager.MCPClient(ctx, botID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := client.ListDir(ctx, dirPath, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]command.FSEntry, len(entries))
|
||||
for i, e := range entries {
|
||||
name := stdpath.Base(e.GetPath())
|
||||
result[i] = command.FSEntry{Name: name, IsDir: e.GetIsDir(), Size: e.GetSize()}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *commandContainerFSAdapter) ReadFile(ctx context.Context, botID, filePath string) (string, error) {
|
||||
client, err := a.manager.MCPClient(ctx, botID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := client.ReadFile(ctx, filePath, 0, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.GetContent(), nil
|
||||
}
|
||||
|
||||
+88
-1
@@ -8,6 +8,7 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
stdpath "path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -34,6 +35,7 @@ import (
|
||||
"github.com/memohai/memoh/internal/channel/identities"
|
||||
"github.com/memohai/memoh/internal/channel/inbound"
|
||||
"github.com/memohai/memoh/internal/channel/route"
|
||||
"github.com/memohai/memoh/internal/command"
|
||||
"github.com/memohai/memoh/internal/config"
|
||||
ctr "github.com/memohai/memoh/internal/containerd"
|
||||
"github.com/memohai/memoh/internal/conversation"
|
||||
@@ -46,8 +48,10 @@ import (
|
||||
emailmailgun "github.com/memohai/memoh/internal/email/adapters/mailgun"
|
||||
"github.com/memohai/memoh/internal/handlers"
|
||||
"github.com/memohai/memoh/internal/healthcheck"
|
||||
"github.com/memohai/memoh/internal/browsercontexts"
|
||||
channelchecker "github.com/memohai/memoh/internal/healthcheck/checkers/channel"
|
||||
mcpchecker "github.com/memohai/memoh/internal/healthcheck/checkers/mcp"
|
||||
"github.com/memohai/memoh/internal/heartbeat"
|
||||
"github.com/memohai/memoh/internal/inbox"
|
||||
"github.com/memohai/memoh/internal/logger"
|
||||
"github.com/memohai/memoh/internal/mcp"
|
||||
@@ -122,8 +126,11 @@ func runServe() {
|
||||
provideChannelManager,
|
||||
provideChannelLifecycleService,
|
||||
provideChatResolver,
|
||||
browsercontexts.NewService,
|
||||
provideScheduleTriggerer,
|
||||
schedule.NewService,
|
||||
provideHeartbeatTriggerer,
|
||||
heartbeat.NewService,
|
||||
provideContainerdHandler,
|
||||
provideFederationGateway,
|
||||
provideToolGatewayService,
|
||||
@@ -162,6 +169,7 @@ func runServe() {
|
||||
fx.Invoke(
|
||||
startMemoryProviderBootstrap,
|
||||
startScheduleService,
|
||||
startHeartbeatService,
|
||||
startChannelManager,
|
||||
startEmailManager,
|
||||
startContainerReconciliation,
|
||||
@@ -267,6 +275,10 @@ func provideScheduleTriggerer(resolver *flow.Resolver) schedule.Triggerer {
|
||||
return flow.NewScheduleGateway(resolver)
|
||||
}
|
||||
|
||||
func provideHeartbeatTriggerer(resolver *flow.Resolver) heartbeat.Triggerer {
|
||||
return flow.NewHeartbeatGateway(resolver)
|
||||
}
|
||||
|
||||
func provideChatResolver(log *slog.Logger, cfg config.Config, modelsService *models.Service, queries *dbsqlc.Queries, chatService *conversation.Service, msgService *message.DBService, settingsService *settings.Service, mediaService *media.Service, containerdHandler *handlers.ContainerdHandler, inboxService *inbox.Service, memoryRegistry *memprovider.Registry) *flow.Resolver {
|
||||
resolver := flow.NewResolver(log, modelsService, queries, chatService, msgService, settingsService, cfg.AgentGateway.BaseURL(), 120*time.Second)
|
||||
resolver.SetMemoryRegistry(memoryRegistry)
|
||||
@@ -292,11 +304,31 @@ func provideChannelRegistry(log *slog.Logger, hub *local.RouteHub, mediaService
|
||||
return registry
|
||||
}
|
||||
|
||||
func provideChannelRouter(log *slog.Logger, registry *channel.Registry, hub *local.RouteHub, routeService *route.DBService, msgService *message.DBService, resolver *flow.Resolver, identityService *identities.Service, botService *bots.Service, policyService *policy.Service, preauthService *preauth.Service, bindService *bind.Service, mediaService *media.Service, inboxService *inbox.Service, rc *boot.RuntimeConfig) *inbound.ChannelInboundProcessor {
|
||||
func provideChannelRouter(log *slog.Logger, registry *channel.Registry, hub *local.RouteHub, routeService *route.DBService, msgService *message.DBService, resolver *flow.Resolver, identityService *identities.Service, botService *bots.Service, policyService *policy.Service, preauthService *preauth.Service, bindService *bind.Service, mediaService *media.Service, inboxService *inbox.Service, subagentService *subagent.Service, scheduleService *schedule.Service, settingsService *settings.Service, mcpConnService *mcp.ConnectionService, modelsService *models.Service, providersService *providers.Service, memProvService *memprovider.Service, searchProvService *searchproviders.Service, browserCtxService *browsercontexts.Service, emailService *emailpkg.Service, emailOutboxService *emailpkg.OutboxService, heartbeatService *heartbeat.Service, queries *dbsqlc.Queries, containerdHandler *handlers.ContainerdHandler, manager *mcp.Manager, rc *boot.RuntimeConfig) *inbound.ChannelInboundProcessor {
|
||||
processor := inbound.NewChannelInboundProcessor(log, registry, routeService, msgService, resolver, identityService, botService, policyService, preauthService, bindService, rc.JwtSecret, 5*time.Minute)
|
||||
processor.SetMediaService(mediaService)
|
||||
processor.SetStreamObserver(local.NewRouteHubBroadcaster(hub))
|
||||
processor.SetInboxService(inboxService)
|
||||
processor.SetCommandHandler(command.NewHandler(
|
||||
log,
|
||||
&command.BotMemberRoleAdapter{BotService: botService},
|
||||
subagentService,
|
||||
scheduleService,
|
||||
settingsService,
|
||||
mcpConnService,
|
||||
inboxService,
|
||||
modelsService,
|
||||
providersService,
|
||||
memProvService,
|
||||
searchProvService,
|
||||
browserCtxService,
|
||||
emailService,
|
||||
emailOutboxService,
|
||||
heartbeatService,
|
||||
queries,
|
||||
&commandSkillLoaderAdapter{handler: containerdHandler},
|
||||
&commandContainerFSAdapter{manager: manager},
|
||||
))
|
||||
return processor
|
||||
}
|
||||
|
||||
@@ -515,6 +547,10 @@ func startScheduleService(lc fx.Lifecycle, scheduleService *schedule.Service) {
|
||||
lc.Append(fx.Hook{OnStart: func(ctx context.Context) error { return scheduleService.Bootstrap(ctx) }})
|
||||
}
|
||||
|
||||
func startHeartbeatService(lc fx.Lifecycle, heartbeatService *heartbeat.Service) {
|
||||
lc.Append(fx.Hook{OnStart: func(ctx context.Context) error { return heartbeatService.Bootstrap(ctx) }})
|
||||
}
|
||||
|
||||
func startChannelManager(lc fx.Lifecycle, channelManager *channel.Manager) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
lc.Append(fx.Hook{
|
||||
@@ -818,3 +854,54 @@ func (a *gatewayAssetLoaderAdapter) OpenForGateway(ctx context.Context, botID, c
|
||||
}
|
||||
return reader, strings.TrimSpace(asset.Mime), nil
|
||||
}
|
||||
|
||||
// commandSkillLoaderAdapter bridges handlers.ContainerdHandler to command.SkillLoader.
|
||||
type commandSkillLoaderAdapter struct {
|
||||
handler *handlers.ContainerdHandler
|
||||
}
|
||||
|
||||
func (a *commandSkillLoaderAdapter) LoadSkills(ctx context.Context, botID string) ([]command.Skill, error) {
|
||||
items, err := a.handler.LoadSkills(ctx, botID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
skills := make([]command.Skill, len(items))
|
||||
for i, item := range items {
|
||||
skills[i] = command.Skill{Name: item.Name, Description: item.Description}
|
||||
}
|
||||
return skills, nil
|
||||
}
|
||||
|
||||
// commandContainerFSAdapter bridges mcp.Manager to command.ContainerFS.
|
||||
type commandContainerFSAdapter struct {
|
||||
manager *mcp.Manager
|
||||
}
|
||||
|
||||
func (a *commandContainerFSAdapter) ListDir(ctx context.Context, botID, dirPath string) ([]command.FSEntry, error) {
|
||||
client, err := a.manager.MCPClient(ctx, botID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := client.ListDir(ctx, dirPath, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]command.FSEntry, len(entries))
|
||||
for i, e := range entries {
|
||||
name := stdpath.Base(e.GetPath())
|
||||
result[i] = command.FSEntry{Name: name, IsDir: e.GetIsDir(), Size: e.GetSize()}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *commandContainerFSAdapter) ReadFile(ctx context.Context, botID, filePath string) (string, error) {
|
||||
client, err := a.manager.MCPClient(ctx, botID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := client.ReadFile(ctx, filePath, 0, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.GetContent(), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user