Files
goclaw/internal/gateway/methods/agents_update.go
T
viettranx 517e6c89ab fix(agents): preserve emoji when updating agent config + validate emoji input
- Read existing IDENTITY.md before overwriting to preserve emoji field
- Config tab: merge existing other_config to prevent wiping emoji on save
- Emoji input: validate single emoji only with extractSingleEmoji()
- Select-all on focus for easy emoji replacement
2026-03-17 18:03:54 +07:00

186 lines
5.8 KiB
Go

package methods
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"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"
)
// --- agents.update ---
// Matching TS src/gateway/server-methods/agents.ts:288-346
func (m *AgentsMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
locale := store.LocaleFromContext(ctx)
var params struct {
AgentID string `json:"agentId"`
Name string `json:"name"`
Workspace string `json:"workspace"`
Model string `json:"model"`
Avatar string `json:"avatar"`
// Per-agent config overrides
ToolsConfig json.RawMessage `json:"tools_config,omitempty"`
SubagentsConfig json.RawMessage `json:"subagents_config,omitempty"`
SandboxConfig json.RawMessage `json:"sandbox_config,omitempty"`
MemoryConfig json.RawMessage `json:"memory_config,omitempty"`
CompactionConfig json.RawMessage `json:"compaction_config,omitempty"`
ContextPruning json.RawMessage `json:"context_pruning,omitempty"`
OtherConfig json.RawMessage `json:"other_config,omitempty"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
if params.AgentID == "" {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "agentId")))
return
}
if m.agentStore != nil {
// --- DB-backed: update agent in 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
}
updates := map[string]any{}
if params.Name != "" {
updates["display_name"] = params.Name
}
if params.Workspace != "" {
ws := config.ExpandHome(params.Workspace)
updates["workspace"] = ws
os.MkdirAll(ws, 0755)
}
if params.Model != "" {
updates["model"] = params.Model
}
// Per-agent JSONB config overrides
if len(params.ToolsConfig) > 0 {
updates["tools_config"] = []byte(params.ToolsConfig)
}
if len(params.SubagentsConfig) > 0 {
updates["subagents_config"] = []byte(params.SubagentsConfig)
}
if len(params.SandboxConfig) > 0 {
updates["sandbox_config"] = []byte(params.SandboxConfig)
}
if len(params.MemoryConfig) > 0 {
updates["memory_config"] = []byte(params.MemoryConfig)
}
if len(params.CompactionConfig) > 0 {
updates["compaction_config"] = []byte(params.CompactionConfig)
}
if len(params.ContextPruning) > 0 {
updates["context_pruning"] = []byte(params.ContextPruning)
}
if len(params.OtherConfig) > 0 {
updates["other_config"] = []byte(params.OtherConfig)
}
if len(updates) > 0 {
if err := m.agentStore.Update(ctx, ag.ID, updates); err != nil {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToUpdate, "agent", fmt.Sprintf("%v", err))))
return
}
}
// Update identity in DB bootstrap — preserve existing fields not being changed.
if params.Avatar != "" || params.Name != "" {
// Read existing identity to preserve emoji and other fields.
existingEmoji, existingAvatar, existingName := "", "", ""
if dbFiles, err := m.agentStore.GetAgentContextFiles(ctx, ag.ID); err == nil {
for _, f := range dbFiles {
if f.FileName == "IDENTITY.md" {
if identity := parseIdentityContent(f.Content); identity != nil {
existingEmoji = identity["Emoji"]
existingAvatar = identity["Avatar"]
existingName = identity["Name"]
}
break
}
}
}
name := params.Name
if name == "" {
name = existingName
}
avatar := params.Avatar
if avatar == "" {
avatar = existingAvatar
}
content := buildIdentityContent(name, existingEmoji, avatar)
if err := m.agentStore.SetAgentContextFile(ctx, ag.ID, "IDENTITY.md", content); err != nil {
slog.Warn("failed to update IDENTITY.md", "agent", params.AgentID, "error", err)
}
// Invalidate interceptor cache so updated IDENTITY.md is served immediately
if m.interceptor != nil {
m.interceptor.InvalidateAgent(ag.ID)
}
}
m.agents.InvalidateAgent(params.AgentID)
} else {
// --- Fallback: config.json ---
spec, ok := m.cfg.Agents.List[params.AgentID]
if !ok {
if params.AgentID != "default" {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgAgentNotFound, params.AgentID)))
return
}
}
if params.Name != "" {
spec.DisplayName = params.Name
}
if params.Workspace != "" {
spec.Workspace = config.ExpandHome(params.Workspace)
os.MkdirAll(spec.Workspace, 0755)
}
if params.Model != "" {
spec.Model = params.Model
}
if params.AgentID == "default" {
if params.Model != "" {
m.cfg.Agents.Defaults.Model = params.Model
}
if params.Workspace != "" {
m.cfg.Agents.Defaults.Workspace = params.Workspace
}
} else {
m.cfg.Agents.List[params.AgentID] = spec
}
if params.Avatar != "" {
ws := spec.Workspace
if ws == "" {
ws = config.ExpandHome(m.cfg.Agents.Defaults.Workspace)
}
identityPath := filepath.Join(ws, "IDENTITY.md")
appendIdentityFields(identityPath, "", "", params.Avatar)
}
if err := config.Save(m.cfgPath, m.cfg); err != nil {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToSave, "config", err.Error())))
return
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"ok": true,
"agentId": params.AgentID,
}))
emitAudit(m.eventBus, client, "agent.updated", "agent", params.AgentID)
}