mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 08:18:19 +00:00
Backend: - Add cross-agent routes for all vault CRUD (/v1/vault/documents/*, /v1/vault/links/*) alongside existing per-agent routes for backward compat. - All handlers skip agent ownership check when agentID is empty. - Path traversal validation (reject .. and leading /) on create. - Scope defaults to "shared" when creating without agent. - Enrichment progress broadcast via WS event (vault.enrich.progress) using existing MessageBus infrastructure. - GET /v1/vault/enrichment/status for polling fallback. Frontend: - Fix VaultDocument.agent_id type to optional/nullable — prevents runtime crash on shared/team docs without agent. - All vault hooks use cross-agent URLs (/v1/vault/...) — no more skipping docs with null agent_id in graph/links/mutations. - Remove dead agentId params from useUpdateDocument, useDeleteDocument, useCreateLink, useDeleteLink, useVaultLinks, LinkBadge. - Pass team_id from selected team filter to create dialog. - WS-based enrichment progress bar on vault page (useWsEvent hook). - Disable rescan button during enrichment, auto-clear after 3s.
220 lines
8.1 KiB
Go
220 lines
8.1 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
|
httpapi "github.com/nextlevelbuilder/goclaw/internal/http"
|
|
mcpbridge "github.com/nextlevelbuilder/goclaw/internal/mcp"
|
|
"github.com/nextlevelbuilder/goclaw/internal/media"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
|
|
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
|
)
|
|
|
|
// httpHandlers bundles the results of wireHTTP() for passing to wireHTTPHandlersOnServer.
|
|
type httpHandlers struct {
|
|
agents *httpapi.AgentsHandler
|
|
skills *httpapi.SkillsHandler
|
|
traces *httpapi.TracesHandler
|
|
mcp *httpapi.MCPHandler
|
|
channelInstances *httpapi.ChannelInstancesHandler
|
|
providers *httpapi.ProvidersHandler
|
|
builtinTools *httpapi.BuiltinToolsHandler
|
|
pendingMessages *httpapi.PendingMessagesHandler
|
|
teamEvents *httpapi.TeamEventsHandler
|
|
secureCLI *httpapi.SecureCLIHandler
|
|
secureCLIGrant *httpapi.SecureCLIGrantHandler
|
|
mcpUserCreds *httpapi.MCPUserCredentialsHandler
|
|
}
|
|
|
|
// wireHTTPHandlersOnServer registers all HTTP handler objects onto the gateway server.
|
|
// Called after wireHTTP() and wireExtras() have returned their results.
|
|
func (d *gatewayDeps) wireHTTPHandlersOnServer(
|
|
h httpHandlers,
|
|
wakeH *httpapi.WakeHandler,
|
|
mcpPool *mcpbridge.Pool,
|
|
postTurn tools.PostTurnProcessor,
|
|
mediaStore *media.Store,
|
|
) {
|
|
if h.providers != nil {
|
|
h.providers.SetAPIBaseFallback(d.cfg.Providers.APIBaseForType)
|
|
}
|
|
if h.agents != nil {
|
|
d.server.SetAgentsHandler(h.agents)
|
|
}
|
|
if h.skills != nil {
|
|
d.server.SetSkillsHandler(h.skills)
|
|
}
|
|
if h.traces != nil {
|
|
d.server.SetTracesHandler(h.traces)
|
|
}
|
|
// External wake/trigger API — wakeH was created by caller before invoking this method.
|
|
d.server.SetWakeHandler(wakeH)
|
|
if h.mcp != nil {
|
|
if mcpPool != nil {
|
|
h.mcp.SetPoolEvictor(mcpPool)
|
|
}
|
|
d.server.SetMCPHandler(h.mcp)
|
|
}
|
|
if h.mcpUserCreds != nil {
|
|
d.server.SetMCPUserCredentialsHandler(h.mcpUserCreds)
|
|
}
|
|
if h.channelInstances != nil {
|
|
d.server.SetChannelInstancesHandler(h.channelInstances)
|
|
}
|
|
if h.providers != nil {
|
|
d.server.SetProvidersHandler(h.providers)
|
|
}
|
|
if h.teamEvents != nil {
|
|
d.server.SetTeamEventsHandler(h.teamEvents)
|
|
}
|
|
if d.pgStores != nil && d.pgStores.Teams != nil {
|
|
d.server.SetTeamAttachmentsHandler(httpapi.NewTeamAttachmentsHandler(d.pgStores.Teams, d.workspace))
|
|
d.server.SetWorkspaceUploadHandler(httpapi.NewWorkspaceUploadHandler(d.pgStores.Teams, d.workspace, d.msgBus))
|
|
}
|
|
if h.builtinTools != nil {
|
|
d.server.SetBuiltinToolsHandler(h.builtinTools)
|
|
}
|
|
if h.pendingMessages != nil {
|
|
if pc := d.cfg.Channels.PendingCompaction; pc != nil {
|
|
h.pendingMessages.SetKeepRecent(pc.KeepRecent)
|
|
h.pendingMessages.SetMaxTokens(pc.MaxTokens)
|
|
h.pendingMessages.SetProviderModel(pc.Provider, pc.Model)
|
|
}
|
|
d.server.SetPendingMessagesHandler(h.pendingMessages)
|
|
}
|
|
if h.secureCLI != nil {
|
|
d.server.SetSecureCLIHandler(h.secureCLI)
|
|
}
|
|
if h.secureCLIGrant != nil {
|
|
d.server.SetSecureCLIGrantHandler(h.secureCLIGrant)
|
|
}
|
|
|
|
// Activity audit log API
|
|
if d.pgStores.Activity != nil {
|
|
d.server.SetActivityHandler(httpapi.NewActivityHandler(d.pgStores.Activity))
|
|
}
|
|
|
|
// System configs API
|
|
if d.pgStores.SystemConfigs != nil {
|
|
d.server.SetSystemConfigsHandler(httpapi.NewSystemConfigsHandler(d.pgStores.SystemConfigs, d.msgBus))
|
|
|
|
// Refresh in-memory config when system_configs change via HTTP API
|
|
d.msgBus.Subscribe(bus.TopicSystemConfigChanged, func(evt bus.Event) {
|
|
// Use tenant context from the request that triggered the change
|
|
ctx := context.Background()
|
|
if reqCtx, ok := evt.Payload.(context.Context); ok {
|
|
ctx = reqCtx
|
|
} else {
|
|
ctx = store.WithTenantID(ctx, store.MasterTenantID)
|
|
}
|
|
if sysConfigs, err := d.pgStores.SystemConfigs.List(ctx); err == nil && len(sysConfigs) > 0 {
|
|
d.cfg.ApplySystemConfigs(sysConfigs)
|
|
// Update PGMemoryStore chunk config so new documents use updated settings
|
|
if mem := d.cfg.Agents.Defaults.Memory; mem != nil {
|
|
if pgMem, ok := d.pgStores.Memory.(*pg.PGMemoryStore); ok {
|
|
pgMem.UpdateChunkConfig(mem.MaxChunkLen, mem.ChunkOverlap)
|
|
}
|
|
}
|
|
slog.Debug("system_configs refreshed to in-memory config", "keys", len(sysConfigs))
|
|
}
|
|
})
|
|
}
|
|
|
|
// Usage analytics API
|
|
if d.pgStores.Snapshots != nil {
|
|
d.server.SetUsageHandler(httpapi.NewUsageHandler(d.pgStores.Snapshots, d.pgStores.DB))
|
|
}
|
|
|
|
// Runtime package management (install/uninstall system/pip/npm packages)
|
|
d.server.SetPackagesHandler(httpapi.NewPackagesHandler())
|
|
|
|
// API documentation (OpenAPI spec + Swagger UI at /docs)
|
|
d.server.SetDocsHandler(httpapi.NewDocsHandler())
|
|
|
|
// Edition info (public, no auth — used by desktop UI comparison modal)
|
|
d.server.SetEditionHandler(httpapi.NewEditionHandler())
|
|
|
|
if d.pgStores != nil && d.pgStores.APIKeys != nil {
|
|
d.server.SetAPIKeysHandler(httpapi.NewAPIKeysHandler(d.pgStores.APIKeys, d.msgBus))
|
|
d.server.SetAPIKeyStore(d.pgStores.APIKeys)
|
|
httpapi.InitAPIKeyCache(d.pgStores.APIKeys, d.msgBus)
|
|
}
|
|
|
|
// Allow browser-paired users to access HTTP APIs
|
|
if d.pgStores.Pairing != nil {
|
|
httpapi.InitPairingAuth(d.pgStores.Pairing)
|
|
}
|
|
|
|
// Memory management API
|
|
if d.pgStores != nil && d.pgStores.Memory != nil {
|
|
d.server.SetMemoryHandler(httpapi.NewMemoryHandler(d.pgStores.Memory))
|
|
}
|
|
|
|
// Knowledge graph API
|
|
if d.pgStores != nil && d.pgStores.KnowledgeGraph != nil {
|
|
d.server.SetKnowledgeGraphHandler(httpapi.NewKnowledgeGraphHandler(d.pgStores.KnowledgeGraph, d.providerRegistry))
|
|
}
|
|
|
|
// V3: Evolution metrics + suggestions API
|
|
if d.pgStores != nil && d.pgStores.EvolutionMetrics != nil && d.pgStores.EvolutionSuggestions != nil {
|
|
var evoOpts []httpapi.EvolutionHandlerOpt
|
|
if manageStore, ok := d.pgStores.Skills.(store.SkillManageStore); ok && d.skillsLoader != nil {
|
|
evoOpts = append(evoOpts, httpapi.WithSkillCreation(manageStore, d.skillsLoader, d.dataDir))
|
|
}
|
|
if d.pgStores.Agents != nil {
|
|
evoOpts = append(evoOpts, httpapi.WithAgentStore(d.pgStores.Agents))
|
|
}
|
|
if d.pgStores.BuiltinToolTenantCfgs != nil {
|
|
evoOpts = append(evoOpts, httpapi.WithToolTenantCfgs(d.pgStores.BuiltinToolTenantCfgs))
|
|
}
|
|
d.server.SetEvolutionHandler(httpapi.NewEvolutionHandler(d.pgStores.EvolutionMetrics, d.pgStores.EvolutionSuggestions, evoOpts...))
|
|
}
|
|
|
|
// V3: Knowledge Vault document API
|
|
if d.pgStores != nil && d.pgStores.Vault != nil {
|
|
vh := httpapi.NewVaultHandler(d.pgStores.Vault, d.pgStores.Teams, d.workspace, d.domainBus, d.pgStores.Agents, d.pgStores.Teams)
|
|
vh.SetEnrichProgress(d.enrichProgress)
|
|
d.server.SetVaultHandler(vh)
|
|
}
|
|
|
|
// V3: Episodic memory summaries API
|
|
if d.pgStores != nil && d.pgStores.Episodic != nil {
|
|
d.server.SetEpisodicHandler(httpapi.NewEpisodicHandler(d.pgStores.Episodic))
|
|
}
|
|
|
|
// V3: Orchestration mode API (read-only)
|
|
if d.pgStores != nil && d.pgStores.Agents != nil {
|
|
d.server.SetOrchestrationHandler(httpapi.NewOrchestrationHandler(d.pgStores.Agents, d.pgStores.Teams, d.pgStores.AgentLinks))
|
|
}
|
|
|
|
// V3: Per-agent v3 feature flags API
|
|
if d.pgStores != nil && d.pgStores.Agents != nil {
|
|
d.server.SetV3FlagsHandler(httpapi.NewV3FlagsHandler(d.pgStores.Agents))
|
|
}
|
|
|
|
// Workspace file serving endpoint — serves files by absolute path, auth-token protected.
|
|
d.server.SetFilesHandler(httpapi.NewFilesHandler(d.workspace, d.dataDir))
|
|
|
|
// Storage file management — browse/delete files under the resolved workspace directory.
|
|
d.server.SetStorageHandler(httpapi.NewStorageHandler(d.workspace))
|
|
|
|
// Media upload endpoint — accepts multipart file uploads, returns temp path + MIME type.
|
|
d.server.SetMediaUploadHandler(httpapi.NewMediaUploadHandler())
|
|
|
|
// Media serve endpoint — serves persisted media files by ID for WS/web clients.
|
|
if mediaStore != nil {
|
|
d.server.SetMediaServeHandler(httpapi.NewMediaServeHandler(mediaStore))
|
|
}
|
|
|
|
// Seed + apply builtin tool disables
|
|
if d.pgStores.BuiltinTools != nil {
|
|
seedBuiltinTools(context.Background(), d.pgStores.BuiltinTools)
|
|
migrateBuiltinToolSettings(context.Background(), d.pgStores.BuiltinTools)
|
|
backfillWebFetchSettings(context.Background(), d.pgStores.BuiltinTools)
|
|
applyBuiltinToolDisables(context.Background(), d.pgStores.BuiltinTools, d.toolsReg)
|
|
}
|
|
}
|