Files
goclaw/internal/gateway/methods/agents_files.go
T
Thieu Nguyen 8ad580521d refactor: deprecate standalone mode, managed mode is now default (#126)
* refactor: remove managed/standalone mode distinction from codebase

Standalone mode is deprecated; managed mode is now the only mode.
Remove redundant "managed mode" qualifiers from comments, docs,
and error messages. Error strings now reference "database stores"
instead of "managed mode" for clarity.

* improve(onboard): streamline onboard process and env setup

Simplify onboard wizard, extract helpers to dedicated file,
update env example and entrypoint for default managed mode,
clean up prepare-env script, update i18n catalogs.
2026-03-11 07:27:38 +07:00

306 lines
8.6 KiB
Go

package methods
import (
"context"
"encoding/json"
"os"
"path/filepath"
"slices"
"github.com/nextlevelbuilder/goclaw/internal/bootstrap"
"github.com/nextlevelbuilder/goclaw/internal/config"
"github.com/nextlevelbuilder/goclaw/internal/gateway"
"github.com/nextlevelbuilder/goclaw/internal/i18n"
"github.com/nextlevelbuilder/goclaw/internal/store"
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)
// allowedAgentFiles is the list of files exposed via agents.files.* RPCs.
// TOOLS.md excluded — not applicable.
var allowedAgentFiles = []string{
bootstrap.AgentsFile, bootstrap.SoulFile, bootstrap.IdentityFile,
bootstrap.UserFile, bootstrap.UserPredefinedFile, bootstrap.BootstrapFile, bootstrap.MemoryJSONFile,
}
// --- agents.files.list ---
// Matching TS src/gateway/server-methods/agents.ts:399-422
func (m *AgentsMethods) handleFilesList(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
locale := store.LocaleFromContext(ctx)
var params agentParams
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
if params.AgentID == "" {
params.AgentID = "default"
}
if m.agentStore != nil {
// --- DB-backed: list from store ---
ctx := context.Background()
ag, err := m.agentStore.GetByKey(ctx, params.AgentID)
if err != nil {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgAgentNotFound, params.AgentID)))
return
}
dbFiles, err := m.agentStore.GetAgentContextFiles(ctx, ag.ID)
if err != nil {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToList, "files")))
return
}
// Build a map for quick lookup
dbMap := make(map[string]store.AgentContextFileData, len(dbFiles))
for _, f := range dbFiles {
dbMap[f.FileName] = f
}
files := make([]map[string]any, 0, len(allowedAgentFiles))
for _, name := range allowedAgentFiles {
if f, ok := dbMap[name]; ok {
files = append(files, map[string]any{
"name": name,
"missing": false,
"size": len(f.Content),
})
} else {
files = append(files, map[string]any{
"name": name,
"missing": true,
})
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"files": files,
}))
return
}
// --- Fallback: filesystem ---
ws := m.resolveWorkspace(params.AgentID)
files := make([]map[string]any, 0, len(allowedAgentFiles))
for _, name := range allowedAgentFiles {
p := filepath.Join(ws, name)
info, err := os.Stat(p)
if err != nil {
files = append(files, map[string]any{
"name": name,
"path": p,
"missing": true,
})
} else {
files = append(files, map[string]any{
"name": name,
"path": p,
"missing": false,
"size": info.Size(),
"updatedAtMs": info.ModTime().UnixMilli(),
})
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"workspace": ws,
"files": files,
}))
}
// --- agents.files.get ---
// Matching TS src/gateway/server-methods/agents.ts:423-473
func (m *AgentsMethods) handleFilesGet(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
locale := store.LocaleFromContext(ctx)
var params struct {
AgentID string `json:"agentId"`
Name string `json:"name"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
if params.AgentID == "" {
params.AgentID = "default"
}
if params.Name == "" {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "name")))
return
}
if !isAllowedFile(params.Name) {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidRequest, "file not allowed: "+params.Name)))
return
}
if m.agentStore != nil {
// --- DB-backed: read from store ---
ctx := context.Background()
ag, err := m.agentStore.GetByKey(ctx, params.AgentID)
if err != nil {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgAgentNotFound, params.AgentID)))
return
}
dbFiles, err := m.agentStore.GetAgentContextFiles(ctx, ag.ID)
if err != nil {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToList, "files")))
return
}
for _, f := range dbFiles {
if f.FileName == params.Name {
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"file": map[string]any{
"name": params.Name,
"missing": false,
"size": len(f.Content),
"content": f.Content,
},
}))
return
}
}
// File not found in DB
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"file": map[string]any{
"name": params.Name,
"missing": true,
},
}))
return
}
// --- Fallback: filesystem ---
ws := m.resolveWorkspace(params.AgentID)
p := filepath.Join(ws, params.Name)
info, err := os.Stat(p)
if err != nil {
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"workspace": ws,
"file": map[string]any{
"name": params.Name,
"path": p,
"missing": true,
},
}))
return
}
content, _ := os.ReadFile(p)
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"workspace": ws,
"file": map[string]any{
"name": params.Name,
"path": p,
"missing": false,
"size": info.Size(),
"updatedAtMs": info.ModTime().UnixMilli(),
"content": string(content),
},
}))
}
// --- agents.files.set ---
// Matching TS src/gateway/server-methods/agents.ts:474-515
func (m *AgentsMethods) handleFilesSet(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
locale := store.LocaleFromContext(ctx)
var params struct {
AgentID string `json:"agentId"`
Name string `json:"name"`
Content string `json:"content"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
if params.AgentID == "" {
params.AgentID = "default"
}
if params.Name == "" {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "name")))
return
}
if !isAllowedFile(params.Name) {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidRequest, "file not allowed: "+params.Name)))
return
}
if m.agentStore != nil {
// --- DB-backed: write to store ---
ctx := context.Background()
ag, err := m.agentStore.GetByKey(ctx, params.AgentID)
if err != nil {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgAgentNotFound, params.AgentID)))
return
}
if err := m.agentStore.SetAgentContextFile(ctx, ag.ID, params.Name, params.Content); err != nil {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToSave, "file", err.Error())))
return
}
// Invalidate both caches so the new content is served immediately
// without waiting for the ContextFileInterceptor's 5-minute TTL to expire.
m.agents.InvalidateAgent(params.AgentID)
if m.interceptor != nil {
m.interceptor.InvalidateAgent(ag.ID)
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"file": map[string]any{
"name": params.Name,
"missing": false,
"size": len(params.Content),
"content": params.Content,
},
}))
return
}
// --- Fallback: filesystem ---
ws := m.resolveWorkspace(params.AgentID)
os.MkdirAll(ws, 0755)
p := filepath.Join(ws, params.Name)
if err := os.WriteFile(p, []byte(params.Content), 0644); err != nil {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToSave, "file", err.Error())))
return
}
info, _ := os.Stat(p)
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"agentId": params.AgentID,
"workspace": ws,
"file": map[string]any{
"name": params.Name,
"path": p,
"missing": false,
"size": info.Size(),
"updatedAtMs": info.ModTime().UnixMilli(),
"content": params.Content,
},
}))
}
// --- Helpers ---
func (m *AgentsMethods) resolveWorkspace(agentID string) string {
if spec, ok := m.cfg.Agents.List[agentID]; ok && spec.Workspace != "" {
return config.ExpandHome(spec.Workspace)
}
return config.ExpandHome(m.cfg.Agents.Defaults.Workspace)
}
func isAllowedFile(name string) bool {
return slices.Contains(allowedAgentFiles, name)
}