Files
goclaw/internal/http/tools_invoke.go
T
viettranx 344e2ac7d1 feat(i18n): add full i18n support for backend and web UI
- Add i18next + react-i18next with namespace-split locale files (27 namespaces x 3 languages)
- Add language switcher in topbar (EN/VI/ZH) with localStorage persistence
- Replace hardcoded strings in 160+ React components with t() translations
- Add Go message catalog (internal/i18n) with T(locale, key, args...) function
- Replace 81 hardcoded error strings in gateway methods and HTTP handlers
- Add locale context propagation: WS connect param + HTTP Accept-Language header
- Keep technical terms in English: Agent, Session, Channel, Provider, Skill, Team, MCP, Cron
- Update CLAUDE.md and review-pr skill with i18n compliance checks
2026-03-09 22:22:42 +07:00

154 lines
4.4 KiB
Go

package http
import (
"encoding/json"
"log/slog"
"net/http"
"github.com/nextlevelbuilder/goclaw/internal/i18n"
"github.com/nextlevelbuilder/goclaw/internal/store"
"github.com/nextlevelbuilder/goclaw/internal/tools"
)
// ToolsInvokeHandler handles POST /v1/tools/invoke (direct tool invocation).
type ToolsInvokeHandler struct {
registry *tools.Registry
token string
agentStore store.AgentStore // nil if not configured
}
// NewToolsInvokeHandler creates a handler for the tools invoke endpoint.
func NewToolsInvokeHandler(registry *tools.Registry, token string, agentStore store.AgentStore) *ToolsInvokeHandler {
return &ToolsInvokeHandler{
registry: registry,
token: token,
agentStore: agentStore,
}
}
type toolsInvokeRequest struct {
Tool string `json:"tool"`
Action string `json:"action,omitempty"`
Args map[string]interface{} `json:"args"`
SessionKey string `json:"sessionKey,omitempty"`
AgentID string `json:"agentId,omitempty"`
DryRun bool `json:"dryRun,omitempty"`
Channel string `json:"channel,omitempty"` // tool context: channel name
ChatID string `json:"chatId,omitempty"` // tool context: chat ID
PeerKind string `json:"peerKind,omitempty"` // tool context: "direct" or "group"
}
func (h *ToolsInvokeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
locale := extractLocale(r)
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": i18n.T(locale, i18n.MsgMethodNotAllowed)})
return
}
if h.token != "" {
if extractBearerToken(r) != h.token {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
return
}
}
var req toolsInvokeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
return
}
if req.Tool == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "tool")})
return
}
slog.Info("tools invoke request", "tool", req.Tool, "dry_run", req.DryRun)
if req.DryRun {
// Just check if tool exists and return its schema
tool, ok := h.registry.Get(req.Tool)
if !ok {
writeToolError(w, http.StatusNotFound, "NOT_FOUND", i18n.T(locale, i18n.MsgNotFound, "tool", req.Tool))
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"tool": req.Tool,
"description": tool.Description(),
"parameters": tool.Parameters(),
"dryRun": true,
})
return
}
// Inject userID and agentID into context for interceptors (bootstrap, memory).
ctx := r.Context()
if userID := extractUserID(r); userID != "" {
ctx = store.WithUserID(ctx, userID)
}
agentIDStr := req.AgentID
if agentIDStr == "" {
agentIDStr = extractAgentID(r, "")
}
if agentIDStr != "" && h.agentStore != nil {
ag, err := h.agentStore.GetByKey(ctx, agentIDStr)
if err == nil {
ctx = store.WithAgentID(ctx, ag.ID)
}
}
// Inject tool context keys (channel, chatID, peerKind) for message routing.
if req.Channel != "" {
ctx = tools.WithToolChannel(ctx, req.Channel)
}
if req.ChatID != "" {
ctx = tools.WithToolChatID(ctx, req.ChatID)
}
if req.PeerKind != "" {
ctx = tools.WithToolPeerKind(ctx, req.PeerKind)
}
// Execute the tool
args := req.Args
if args == nil {
args = make(map[string]interface{})
}
// If action is specified, add it to args
if req.Action != "" {
args["action"] = req.Action
}
result := h.registry.ExecuteWithContext(ctx, req.Tool, args, "http", "api", "direct", "", nil)
if result.IsError {
writeToolError(w, http.StatusBadRequest, "TOOL_ERROR", result.ForLLM)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"result": map[string]interface{}{
"output": result.ForLLM,
"forUser": result.ForUser,
"metadata": map[string]interface{}{},
},
})
}
func writeToolError(w http.ResponseWriter, status int, code, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": map[string]string{
"code": code,
"message": message,
},
})
}