mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 18:18:37 +00:00
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
This commit is contained in:
1 parent
3187196bd8
commit
344e2ac7d1
312 files changed
+10361
-2441
No files matched your search
@@ -33,6 +33,7 @@ internal/
|
||||
├── crypto/ AES-256-GCM encryption for API keys
|
||||
├── sandbox/ Docker-based code sandbox
|
||||
├── tts/ Text-to-Speech (OpenAI, ElevenLabs, Edge, MiniMax)
|
||||
├── i18n/ Message catalog: T(locale, key, args...) + per-locale catalogs (en/vi/zh)
|
||||
pkg/protocol/ Wire types (frames, methods, errors, events)
|
||||
pkg/browser/ Browser automation (Rod + CDP)
|
||||
migrations/ PostgreSQL migration files
|
||||
@@ -46,11 +47,12 @@ ui/web/ React SPA (pnpm, Vite, Tailwind, Radix UI)
|
||||
- **Context files:** `agent_context_files` (agent-level) + `user_context_files` (per-user), routed via `ContextFileInterceptor`
|
||||
- **Providers:** Anthropic (native HTTP+SSE) and OpenAI-compat (generic). Both use `RetryDo()` for retries. Loads from `llm_providers` table with encrypted API keys
|
||||
- **Agent loop:** `RunRequest` → think→act→observe → `RunResult`. Events: `run.started`, `run.completed`, `chunk`, `tool.call`, `tool.result`. Auto-summarization at >75% context
|
||||
- **Context propagation:** `store.WithAgentType(ctx)`, `store.WithUserID(ctx)`, `store.WithAgentID(ctx)`
|
||||
- **Context propagation:** `store.WithAgentType(ctx)`, `store.WithUserID(ctx)`, `store.WithAgentID(ctx)`, `store.WithLocale(ctx)`
|
||||
- **WebSocket protocol (v3):** Frame types `req`/`res`/`event`. First request must be `connect`
|
||||
- **Config:** JSON5 at `GOCLAW_CONFIG` env. Secrets in `.env.local` or env vars, never in config.json
|
||||
- **Security:** Rate limiting, input guard (detection-only), CORS, shell deny patterns, SSRF protection, path traversal prevention, AES-256-GCM encryption. All security logs: `slog.Warn("security.*")`
|
||||
- **Telegram formatting:** LLM output → `SanitizeAssistantContent()` → `markdownToTelegramHTML()` → `chunkHTML()` → `sendHTML()`. Tables rendered as ASCII in `<pre>` tags
|
||||
- **i18n:** Web UI uses `i18next` with namespace-split locale files in `ui/web/src/i18n/locales/{lang}/`. Backend uses `internal/i18n` message catalog with `i18n.T(locale, key, args...)`. Locale propagated via `store.WithLocale(ctx)` — WS `connect` param `locale`, HTTP `Accept-Language` header. Supported: en (default), vi, zh. New user-facing strings: add key to `internal/i18n/keys.go`, add translations to all 3 catalog files. New UI strings: add key to all 3 locale dirs. Bootstrap templates (SOUL.md, etc.) stay English-only (LLM consumption).
|
||||
|
||||
## Running
|
||||
|
||||
@@ -78,3 +80,4 @@ Go conventions to follow:
|
||||
- Use `append(dst, src...)` instead of loop-based append
|
||||
- Always handle errors; don't ignore return values
|
||||
- **Migrations:** When adding a new SQL migration file in `migrations/`, bump `RequiredSchemaVersion` in `internal/upgrade/version.go` to match the new migration number
|
||||
- **i18n strings:** When adding user-facing error messages, add key to `internal/i18n/keys.go` and translations to `catalog_en.go`, `catalog_vi.go`, `catalog_zh.go`. For UI strings, add to all locale JSON files in `ui/web/src/i18n/locales/{en,vi,zh}/`
|
||||
@@ -26,6 +26,8 @@ type Client struct {
|
||||
connectedAt time.Time // when the client connected
|
||||
remoteAddr string // peer IP (extracted from proxy headers or RemoteAddr)
|
||||
|
||||
locale string // user's preferred locale (e.g. "en", "vi", "zh")
|
||||
|
||||
// Browser pairing state
|
||||
pairingCode string // 8-char code if pending approval
|
||||
pairingPending bool // true while waiting for admin approval
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/agent"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -39,20 +40,21 @@ type linksListParams struct {
|
||||
Direction string `json:"direction"` // "from" (default), "to", "all"
|
||||
}
|
||||
|
||||
func (m *AgentLinksMethods) handleList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *AgentLinksMethods) handleList(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.linkStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "agent links not configured"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgLinksNotConfigured)))
|
||||
return
|
||||
}
|
||||
|
||||
var params linksListParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
if params.AgentID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "agentId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "agentId")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -62,7 +64,6 @@ func (m *AgentLinksMethods) handleList(_ context.Context, client *gateway.Client
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var links []store.AgentLinkData
|
||||
|
||||
switch params.Direction {
|
||||
@@ -104,20 +105,21 @@ type linksCreateParams struct {
|
||||
Settings json.RawMessage `json:"settings"`
|
||||
}
|
||||
|
||||
func (m *AgentLinksMethods) handleCreate(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *AgentLinksMethods) handleCreate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.linkStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "agent links not configured"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgLinksNotConfigured)))
|
||||
return
|
||||
}
|
||||
|
||||
var params linksCreateParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
if params.SourceAgent == "" || params.TargetAgent == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "sourceAgent and targetAgent are required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "sourceAgent and targetAgent")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -126,7 +128,7 @@ func (m *AgentLinksMethods) handleCreate(_ context.Context, client *gateway.Clie
|
||||
direction = store.LinkDirectionOutbound
|
||||
}
|
||||
if direction != store.LinkDirectionOutbound && direction != store.LinkDirectionInbound && direction != store.LinkDirectionBidirectional {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "direction must be outbound, inbound, or bidirectional"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidDirection)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -142,13 +144,13 @@ func (m *AgentLinksMethods) handleCreate(_ context.Context, client *gateway.Clie
|
||||
}
|
||||
|
||||
if sourceAgent.ID == targetAgent.ID {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "source and target must be different agents"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgSourceTargetSame)))
|
||||
return
|
||||
}
|
||||
|
||||
// Delegation targets must be predefined agents (open agents have no agent-level context files)
|
||||
if targetAgent.AgentType == store.AgentTypeOpen {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "cannot delegate to open agents — only predefined agents can be delegation targets"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgCannotDelegateOpen)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -168,9 +170,8 @@ func (m *AgentLinksMethods) handleCreate(_ context.Context, client *gateway.Clie
|
||||
CreatedBy: client.UserID(),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := m.linkStore.CreateLink(ctx, link); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to create link: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToCreate, "link", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -217,33 +218,34 @@ type linksUpdateParams struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func (m *AgentLinksMethods) handleUpdate(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *AgentLinksMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.linkStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "agent links not configured"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgLinksNotConfigured)))
|
||||
return
|
||||
}
|
||||
|
||||
var params linksUpdateParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
if params.LinkID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "linkId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "linkId")))
|
||||
return
|
||||
}
|
||||
|
||||
linkID, err := uuid.Parse(params.LinkID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid linkId"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "linkId")))
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]any{}
|
||||
if params.Direction != "" {
|
||||
if params.Direction != store.LinkDirectionOutbound && params.Direction != store.LinkDirectionInbound && params.Direction != store.LinkDirectionBidirectional {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "direction must be outbound, inbound, or bidirectional"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidDirection)))
|
||||
return
|
||||
}
|
||||
updates["direction"] = params.Direction
|
||||
@@ -259,20 +261,19 @@ func (m *AgentLinksMethods) handleUpdate(_ context.Context, client *gateway.Clie
|
||||
}
|
||||
if params.Status != "" {
|
||||
if params.Status != store.LinkStatusActive && params.Status != store.LinkStatusDisabled {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "status must be active or disabled"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidLinkStatus)))
|
||||
return
|
||||
}
|
||||
updates["status"] = params.Status
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "no updates provided"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgNoUpdatesProvided)))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := m.linkStore.UpdateLink(ctx, linkID, updates); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to update link: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToUpdate, "link", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -311,36 +312,35 @@ type linksDeleteParams struct {
|
||||
LinkID string `json:"linkId"`
|
||||
}
|
||||
|
||||
func (m *AgentLinksMethods) handleDelete(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *AgentLinksMethods) handleDelete(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.linkStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "agent links not configured"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgLinksNotConfigured)))
|
||||
return
|
||||
}
|
||||
|
||||
var params linksDeleteParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
if params.LinkID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "linkId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "linkId")))
|
||||
return
|
||||
}
|
||||
|
||||
linkID, err := uuid.Parse(params.LinkID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid linkId"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "linkId")))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Fetch link before deleting to get agent IDs for cache invalidation
|
||||
link, _ := m.linkStore.GetLink(ctx, linkID)
|
||||
|
||||
if err := m.linkStore.DeleteLink(ctx, linkID); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to delete link: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToDelete, "link", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/agent"
|
||||
"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/internal/tools"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
@@ -103,9 +104,10 @@ func (m *AgentsMethods) handleAgentWait(_ context.Context, client *gateway.Clien
|
||||
|
||||
func (m *AgentsMethods) handleList(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
if m.agentStore != nil {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
userID := client.UserID()
|
||||
if userID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "user context required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgUserCtxRequired)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -118,7 +120,7 @@ func (m *AgentsMethods) handleList(ctx context.Context, client *gateway.Client,
|
||||
}
|
||||
if err != nil {
|
||||
slog.Warn("agents.list: store query failed", "error", err)
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to list agents"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToList, "agents")))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
@@ -18,7 +19,8 @@ import (
|
||||
// --- agents.create ---
|
||||
// Matching TS src/gateway/server-methods/agents.ts:216-287
|
||||
|
||||
func (m *AgentsMethods) handleCreate(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *AgentsMethods) handleCreate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
Name string `json:"name"`
|
||||
Workspace string `json:"workspace"`
|
||||
@@ -40,7 +42,7 @@ func (m *AgentsMethods) handleCreate(_ context.Context, client *gateway.Client,
|
||||
}
|
||||
|
||||
if params.Name == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "name is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "name")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -51,7 +53,7 @@ func (m *AgentsMethods) handleCreate(_ context.Context, client *gateway.Client,
|
||||
|
||||
agentID := config.NormalizeAgentID(params.Name)
|
||||
if agentID == "default" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "cannot create agent with reserved id 'default'"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidRequest, "cannot create agent with reserved id 'default'")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -69,7 +71,7 @@ func (m *AgentsMethods) handleCreate(_ context.Context, client *gateway.Client,
|
||||
|
||||
// Check if agent already exists in DB
|
||||
if existing, _ := m.agentStore.GetByKey(ctx, agentID); existing != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "agent already exists: "+agentID))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgAlreadyExists, "agent", agentID)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -98,7 +100,7 @@ func (m *AgentsMethods) handleCreate(_ context.Context, client *gateway.Client,
|
||||
OtherConfig: params.OtherConfig,
|
||||
}
|
||||
if err := m.agentStore.Create(ctx, agentData); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, fmt.Sprintf("failed to create agent: %v", err)))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToCreate, "agent", fmt.Sprintf("%v", err))))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -120,7 +122,7 @@ func (m *AgentsMethods) handleCreate(_ context.Context, client *gateway.Client,
|
||||
} else {
|
||||
// --- Fallback: config.json + filesystem ---
|
||||
if _, ok := m.cfg.Agents.List[agentID]; ok {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "agent already exists: "+agentID))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgAlreadyExists, "agent", agentID)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -140,7 +142,7 @@ func (m *AgentsMethods) handleCreate(_ context.Context, client *gateway.Client,
|
||||
m.cfg.Agents.List[agentID] = spec
|
||||
|
||||
if err := config.Save(m.cfgPath, m.cfg); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to save config: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToSave, "config", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,16 @@ import (
|
||||
|
||||
"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.delete ---
|
||||
// Matching TS src/gateway/server-methods/agents.ts:347-398
|
||||
|
||||
func (m *AgentsMethods) handleDelete(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *AgentsMethods) handleDelete(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
AgentID string `json:"agentId"`
|
||||
DeleteFiles bool `json:"deleteFiles"`
|
||||
@@ -25,11 +28,11 @@ func (m *AgentsMethods) handleDelete(_ context.Context, client *gateway.Client,
|
||||
}
|
||||
|
||||
if params.AgentID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "agentId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "agentId")))
|
||||
return
|
||||
}
|
||||
if params.AgentID == "default" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "cannot delete the default agent"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgCannotDeleteDefault)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -40,12 +43,12 @@ func (m *AgentsMethods) handleDelete(_ context.Context, client *gateway.Client,
|
||||
ctx := context.Background()
|
||||
ag, err := m.agentStore.GetByKey(ctx, params.AgentID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "agent not found: "+params.AgentID))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgAgentNotFound, params.AgentID)))
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.agentStore.Delete(ctx, ag.ID); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, fmt.Sprintf("failed to delete agent: %v", err)))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToDelete, "agent", fmt.Sprintf("%v", err))))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -60,7 +63,7 @@ func (m *AgentsMethods) handleDelete(_ context.Context, client *gateway.Client,
|
||||
// --- Fallback: config.json ---
|
||||
spec, ok := m.cfg.Agents.List[params.AgentID]
|
||||
if !ok {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "agent not found: "+params.AgentID))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgAgentNotFound, params.AgentID)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -79,7 +82,7 @@ func (m *AgentsMethods) handleDelete(_ context.Context, client *gateway.Client,
|
||||
m.agents.Remove(params.AgentID)
|
||||
|
||||
if err := config.Save(m.cfgPath, m.cfg); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to save config: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToSave, "config", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
@@ -23,7 +24,8 @@ var allowedAgentFiles = []string{
|
||||
// --- agents.files.list ---
|
||||
// Matching TS src/gateway/server-methods/agents.ts:399-422
|
||||
|
||||
func (m *AgentsMethods) handleFilesList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
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, ¶ms)
|
||||
@@ -37,13 +39,13 @@ func (m *AgentsMethods) handleFilesList(_ context.Context, client *gateway.Clien
|
||||
ctx := context.Background()
|
||||
ag, err := m.agentStore.GetByKey(ctx, params.AgentID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "agent not found: "+params.AgentID))
|
||||
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, "failed to list files: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToList, "files")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -110,7 +112,8 @@ func (m *AgentsMethods) handleFilesList(_ context.Context, client *gateway.Clien
|
||||
// --- agents.files.get ---
|
||||
// Matching TS src/gateway/server-methods/agents.ts:423-473
|
||||
|
||||
func (m *AgentsMethods) handleFilesGet(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
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"`
|
||||
@@ -122,11 +125,11 @@ func (m *AgentsMethods) handleFilesGet(_ context.Context, client *gateway.Client
|
||||
params.AgentID = "default"
|
||||
}
|
||||
if params.Name == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "name is required"))
|
||||
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, "file not allowed: "+params.Name))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidRequest, "file not allowed: "+params.Name)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -135,13 +138,13 @@ func (m *AgentsMethods) handleFilesGet(_ context.Context, client *gateway.Client
|
||||
ctx := context.Background()
|
||||
ag, err := m.agentStore.GetByKey(ctx, params.AgentID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "agent not found: "+params.AgentID))
|
||||
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, "failed to get files: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToList, "files")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -207,7 +210,8 @@ func (m *AgentsMethods) handleFilesGet(_ context.Context, client *gateway.Client
|
||||
// --- agents.files.set ---
|
||||
// Matching TS src/gateway/server-methods/agents.ts:474-515
|
||||
|
||||
func (m *AgentsMethods) handleFilesSet(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
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"`
|
||||
@@ -220,11 +224,11 @@ func (m *AgentsMethods) handleFilesSet(_ context.Context, client *gateway.Client
|
||||
params.AgentID = "default"
|
||||
}
|
||||
if params.Name == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "name is required"))
|
||||
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, "file not allowed: "+params.Name))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidRequest, "file not allowed: "+params.Name)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -233,12 +237,12 @@ func (m *AgentsMethods) handleFilesSet(_ context.Context, client *gateway.Client
|
||||
ctx := context.Background()
|
||||
ag, err := m.agentStore.GetByKey(ctx, params.AgentID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "agent not found: "+params.AgentID))
|
||||
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, "failed to write file: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToSave, "file", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -267,7 +271,7 @@ func (m *AgentsMethods) handleFilesSet(_ context.Context, client *gateway.Client
|
||||
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, "failed to write file: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToSave, "file", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,16 @@ import (
|
||||
|
||||
"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(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
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"`
|
||||
@@ -37,7 +40,7 @@ func (m *AgentsMethods) handleUpdate(_ context.Context, client *gateway.Client,
|
||||
}
|
||||
|
||||
if params.AgentID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "agentId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "agentId")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,7 +49,7 @@ func (m *AgentsMethods) handleUpdate(_ context.Context, client *gateway.Client,
|
||||
ctx := context.Background()
|
||||
ag, err := m.agentStore.GetByKey(ctx, params.AgentID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "agent not found: "+params.AgentID))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgAgentNotFound, params.AgentID)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -87,7 +90,7 @@ func (m *AgentsMethods) handleUpdate(_ context.Context, client *gateway.Client,
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := m.agentStore.Update(ctx, ag.ID, updates); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, fmt.Sprintf("failed to update agent: %v", err)))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToUpdate, "agent", fmt.Sprintf("%v", err))))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -110,7 +113,7 @@ func (m *AgentsMethods) handleUpdate(_ context.Context, client *gateway.Client,
|
||||
spec, ok := m.cfg.Agents.List[params.AgentID]
|
||||
if !ok {
|
||||
if params.AgentID != "default" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "agent not found: "+params.AgentID))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgAgentNotFound, params.AgentID)))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -147,7 +150,7 @@ func (m *AgentsMethods) handleUpdate(_ context.Context, client *gateway.Client,
|
||||
}
|
||||
|
||||
if err := config.Save(m.cfgPath, m.cfg); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to save config: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToSave, "config", err.Error())))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -44,10 +45,11 @@ func (m *ChannelInstancesMethods) emitCacheInvalidate() {
|
||||
}
|
||||
|
||||
func (m *ChannelInstancesMethods) handleList(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
instances, err := m.store.ListAll(ctx)
|
||||
if err != nil {
|
||||
slog.Error("channels.instances.list", "error", err)
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to list channel instances"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToList, "channel instances")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,6 +65,7 @@ func (m *ChannelInstancesMethods) handleList(ctx context.Context, client *gatewa
|
||||
}
|
||||
|
||||
func (m *ChannelInstancesMethods) handleGet(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
@@ -72,13 +75,13 @@ func (m *ChannelInstancesMethods) handleGet(ctx context.Context, client *gateway
|
||||
|
||||
id, err := uuid.Parse(params.ID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid instance ID"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "instance")))
|
||||
return
|
||||
}
|
||||
|
||||
inst, err := m.store.Get(ctx, id)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "instance not found"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgInstanceNotFound)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -86,6 +89,7 @@ func (m *ChannelInstancesMethods) handleGet(ctx context.Context, client *gateway
|
||||
}
|
||||
|
||||
func (m *ChannelInstancesMethods) handleCreate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
@@ -100,18 +104,18 @@ func (m *ChannelInstancesMethods) handleCreate(ctx context.Context, client *gate
|
||||
}
|
||||
|
||||
if params.Name == "" || params.ChannelType == "" || params.AgentID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "name, channel_type, and agent_id are required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "name, channel_type, and agent_id")))
|
||||
return
|
||||
}
|
||||
|
||||
if !isValidChannelType(params.ChannelType) {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid channel_type"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidChannelType)))
|
||||
return
|
||||
}
|
||||
|
||||
agentID, err := uuid.Parse(params.AgentID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid agent_id"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "agent_id")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -132,7 +136,7 @@ func (m *ChannelInstancesMethods) handleCreate(ctx context.Context, client *gate
|
||||
|
||||
if err := m.store.Create(ctx, inst); err != nil {
|
||||
slog.Error("channels.instances.create", "error", err)
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to create instance: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToCreate, "instance", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -141,6 +145,7 @@ func (m *ChannelInstancesMethods) handleCreate(ctx context.Context, client *gate
|
||||
}
|
||||
|
||||
func (m *ChannelInstancesMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
ID string `json:"id"`
|
||||
Updates json.RawMessage `json:"updates"`
|
||||
@@ -151,19 +156,19 @@ func (m *ChannelInstancesMethods) handleUpdate(ctx context.Context, client *gate
|
||||
|
||||
id, err := uuid.Parse(params.ID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid instance ID"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "instance")))
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := json.Unmarshal(params.Updates, &updates); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid updates"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidUpdates)))
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.store.Update(ctx, id, updates); err != nil {
|
||||
slog.Error("channels.instances.update", "error", err)
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to update instance: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToUpdate, "instance", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -172,6 +177,7 @@ func (m *ChannelInstancesMethods) handleUpdate(ctx context.Context, client *gate
|
||||
}
|
||||
|
||||
func (m *ChannelInstancesMethods) handleDelete(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
@@ -181,24 +187,24 @@ func (m *ChannelInstancesMethods) handleDelete(ctx context.Context, client *gate
|
||||
|
||||
id, err := uuid.Parse(params.ID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid instance ID"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "instance")))
|
||||
return
|
||||
}
|
||||
|
||||
// Look up instance to check if it's a default (seeded) instance.
|
||||
inst, err := m.store.Get(ctx, id)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "instance not found"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInstanceNotFound)))
|
||||
return
|
||||
}
|
||||
if store.IsDefaultChannelInstance(inst.Name) {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "cannot delete default channel instance"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgCannotDeleteDefaultInst)))
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.store.Delete(ctx, id); err != nil {
|
||||
slog.Error("channels.instances.delete", "error", err)
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to delete instance: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToDelete, "instance", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/channels"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
|
||||
@@ -39,7 +41,8 @@ func (m *ChannelsMethods) handleStatus(_ context.Context, client *gateway.Client
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *ChannelsMethods) handleToggle(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *ChannelsMethods) handleToggle(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
// Channel toggling requires restarting the channel, which is a Phase 3 feature.
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "channels.toggle not yet implemented"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgNotImplemented, "channels.toggle")))
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/channels/media"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/sessions"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
@@ -44,6 +45,7 @@ type chatSendParams struct {
|
||||
}
|
||||
|
||||
func (m *ChatMethods) handleSend(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
// Rate limit check per user/client
|
||||
if m.rateLimiter != nil && m.rateLimiter.Enabled() {
|
||||
key := client.UserID()
|
||||
@@ -51,14 +53,14 @@ func (m *ChatMethods) handleSend(ctx context.Context, client *gateway.Client, re
|
||||
key = client.ID()
|
||||
}
|
||||
if !m.rateLimiter.Allow(key) {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "rate limit exceeded — please wait before sending more messages"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRateLimitExceeded)))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var params chatSendParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -83,7 +85,7 @@ func (m *ChatMethods) handleSend(ctx context.Context, client *gateway.Client, re
|
||||
|
||||
userID := client.UserID()
|
||||
if userID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "user_id is required in managed mode — provide it in the connect handshake"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgUserIDRequired)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -171,9 +173,10 @@ type chatHistoryParams struct {
|
||||
}
|
||||
|
||||
func (m *ChatMethods) handleHistory(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params chatHistoryParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -195,23 +198,24 @@ func (m *ChatMethods) handleHistory(ctx context.Context, client *gateway.Client,
|
||||
|
||||
// handleInject injects a message into a session transcript without running the agent.
|
||||
// Matching TS chat.inject (src/gateway/server-methods/chat.ts:686-746).
|
||||
func (m *ChatMethods) handleInject(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *ChatMethods) handleInject(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
SessionKey string `json:"sessionKey"`
|
||||
Message string `json:"message"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
if params.SessionKey == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "sessionKey is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "sessionKey")))
|
||||
return
|
||||
}
|
||||
if params.Message == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "message is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgMsgRequired)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -249,18 +253,19 @@ func (m *ChatMethods) handleInject(_ context.Context, client *gateway.Client, re
|
||||
// Response:
|
||||
//
|
||||
// { ok: true, aborted: bool, runIds: []string }
|
||||
func (m *ChatMethods) handleAbort(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *ChatMethods) handleAbort(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
RunID string `json:"runId"`
|
||||
SessionKey string `json:"sessionKey"`
|
||||
}
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
if params.SessionKey == "" && params.RunID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "sessionKey or runId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "sessionKey or runId")))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"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"
|
||||
)
|
||||
@@ -45,6 +46,7 @@ func (m *ConfigMethods) handleGet(_ context.Context, client *gateway.Client, req
|
||||
// handleApply replaces the entire config with the provided JSON5 raw content.
|
||||
// Matching TS config.apply (src/gateway/server-methods/config.ts:435-486).
|
||||
func (m *ConfigMethods) handleApply(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
Raw string `json:"raw"`
|
||||
BaseHash string `json:"baseHash"`
|
||||
@@ -54,20 +56,20 @@ func (m *ConfigMethods) handleApply(ctx context.Context, client *gateway.Client,
|
||||
}
|
||||
|
||||
if params.Raw == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "raw config is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRawConfigRequired)))
|
||||
return
|
||||
}
|
||||
|
||||
// Optimistic concurrency: validate hash if provided
|
||||
if params.BaseHash != "" && params.BaseHash != m.cfg.Hash() {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "config has changed (hash mismatch)"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgConfigHashMismatch)))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the new config
|
||||
newCfg := config.Default()
|
||||
if err := json5.Unmarshal([]byte(params.Raw), newCfg); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid config: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidRequest, err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -77,7 +79,7 @@ func (m *ConfigMethods) handleApply(ctx context.Context, client *gateway.Client,
|
||||
|
||||
// Save to disk
|
||||
if err := config.Save(m.cfgPath, newCfg); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to save config: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToSave, "config", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,6 +105,7 @@ func (m *ConfigMethods) handleApply(ctx context.Context, client *gateway.Client,
|
||||
// handlePatch merges a partial config update into the current config.
|
||||
// Matching TS config.patch (src/gateway/server-methods/config.ts:321-434).
|
||||
func (m *ConfigMethods) handlePatch(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
Raw string `json:"raw"`
|
||||
BaseHash string `json:"baseHash"`
|
||||
@@ -112,33 +115,33 @@ func (m *ConfigMethods) handlePatch(ctx context.Context, client *gateway.Client,
|
||||
}
|
||||
|
||||
if params.Raw == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "raw patch is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRawPatchRequired)))
|
||||
return
|
||||
}
|
||||
|
||||
// Optimistic concurrency
|
||||
if params.BaseHash != "" && params.BaseHash != m.cfg.Hash() {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "config has changed (hash mismatch)"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgConfigHashMismatch)))
|
||||
return
|
||||
}
|
||||
|
||||
// Merge strategy: serialize current -> deserialize patch on top -> save
|
||||
currentJSON, err := json.Marshal(m.cfg)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to serialize current config"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgInternalError, "failed to serialize current config")))
|
||||
return
|
||||
}
|
||||
|
||||
// Start from current config as base
|
||||
merged := config.Default()
|
||||
if err := json.Unmarshal(currentJSON, merged); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to clone config"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgInternalError, "failed to clone config")))
|
||||
return
|
||||
}
|
||||
|
||||
// Apply patch on top
|
||||
if err := json5.Unmarshal([]byte(params.Raw), merged); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid patch: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidRequest, err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -148,7 +151,7 @@ func (m *ConfigMethods) handlePatch(ctx context.Context, client *gateway.Client,
|
||||
|
||||
// Save to disk
|
||||
if err := config.Save(m.cfgPath, merged); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to save config: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToSave, "config", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"regexp"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -49,30 +50,31 @@ func (m *CronMethods) handleList(_ context.Context, client *gateway.Client, req
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *CronMethods) handleCreate(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *CronMethods) handleCreate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name"`
|
||||
Schedule store.CronSchedule `json:"schedule"`
|
||||
Message string `json:"message"`
|
||||
Deliver bool `json:"deliver"`
|
||||
Channel string `json:"channel"`
|
||||
To string `json:"to"`
|
||||
AgentID string `json:"agentId"`
|
||||
Message string `json:"message"`
|
||||
Deliver bool `json:"deliver"`
|
||||
Channel string `json:"channel"`
|
||||
To string `json:"to"`
|
||||
AgentID string `json:"agentId"`
|
||||
}
|
||||
if req.Params != nil {
|
||||
json.Unmarshal(req.Params, ¶ms)
|
||||
}
|
||||
|
||||
if params.Name == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "name is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "name")))
|
||||
return
|
||||
}
|
||||
if !cronSlugRe.MatchString(params.Name) {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "name must be a valid slug (lowercase letters, numbers, hyphens only)"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidSlug, "name")))
|
||||
return
|
||||
}
|
||||
if params.Message == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "message is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgMsgRequired)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -87,7 +89,8 @@ func (m *CronMethods) handleCreate(_ context.Context, client *gateway.Client, re
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *CronMethods) handleDelete(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *CronMethods) handleDelete(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
JobID string `json:"jobId"`
|
||||
}
|
||||
@@ -96,7 +99,7 @@ func (m *CronMethods) handleDelete(_ context.Context, client *gateway.Client, re
|
||||
}
|
||||
|
||||
if params.JobID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "jobId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "jobId")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -110,7 +113,8 @@ func (m *CronMethods) handleDelete(_ context.Context, client *gateway.Client, re
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *CronMethods) handleToggle(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *CronMethods) handleToggle(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
JobID string `json:"jobId"`
|
||||
Enabled bool `json:"enabled"`
|
||||
@@ -120,7 +124,7 @@ func (m *CronMethods) handleToggle(_ context.Context, client *gateway.Client, re
|
||||
}
|
||||
|
||||
if params.JobID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "jobId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "jobId")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -139,10 +143,11 @@ func (m *CronMethods) handleStatus(_ context.Context, client *gateway.Client, re
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, m.service.Status()))
|
||||
}
|
||||
|
||||
func (m *CronMethods) handleUpdate(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *CronMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
JobID string `json:"jobId"`
|
||||
ID string `json:"id"` // alias (matching TS)
|
||||
JobID string `json:"jobId"`
|
||||
ID string `json:"id"` // alias (matching TS)
|
||||
Patch store.CronJobPatch `json:"patch"`
|
||||
}
|
||||
if req.Params != nil {
|
||||
@@ -154,7 +159,7 @@ func (m *CronMethods) handleUpdate(_ context.Context, client *gateway.Client, re
|
||||
jobID = params.ID
|
||||
}
|
||||
if jobID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "jobId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "jobId")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -169,7 +174,8 @@ func (m *CronMethods) handleUpdate(_ context.Context, client *gateway.Client, re
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *CronMethods) handleRun(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *CronMethods) handleRun(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
JobID string `json:"jobId"`
|
||||
ID string `json:"id"`
|
||||
@@ -184,7 +190,7 @@ func (m *CronMethods) handleRun(_ context.Context, client *gateway.Client, req *
|
||||
jobID = params.ID
|
||||
}
|
||||
if jobID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "jobId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "jobId")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -193,7 +199,7 @@ func (m *CronMethods) handleRun(_ context.Context, client *gateway.Client, req *
|
||||
// Validate job exists before responding
|
||||
_, ok := m.service.GetJob(jobID)
|
||||
if !ok {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "job not found"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgJobNotFound)))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -25,9 +26,10 @@ func (m *DelegationsMethods) Register(router *gateway.MethodRouter) {
|
||||
router.Register(protocol.MethodDelegationsGet, m.handleGet)
|
||||
}
|
||||
|
||||
func (m *DelegationsMethods) handleList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *DelegationsMethods) handleList(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.teamStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "delegations not available"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgDelegationsUnavailable)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -67,7 +69,6 @@ func (m *DelegationsMethods) handleList(_ context.Context, client *gateway.Clien
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
records, total, err := m.teamStore.ListDelegationHistory(ctx, opts)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, err.Error()))
|
||||
@@ -92,9 +93,10 @@ func (m *DelegationsMethods) handleList(_ context.Context, client *gateway.Clien
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *DelegationsMethods) handleGet(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *DelegationsMethods) handleGet(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.teamStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "delegations not available"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgDelegationsUnavailable)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -105,17 +107,16 @@ func (m *DelegationsMethods) handleGet(_ context.Context, client *gateway.Client
|
||||
_ = json.Unmarshal(req.Params, ¶ms)
|
||||
}
|
||||
if params.ID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "id is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "id")))
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(params.ID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid id"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "id")))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
record, err := m.teamStore.GetDelegationHistory(ctx, id)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, err.Error()))
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -55,22 +57,23 @@ func (m *ExecApprovalMethods) handleList(_ context.Context, client *gateway.Clie
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *ExecApprovalMethods) handleApprove(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *ExecApprovalMethods) handleApprove(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.manager == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "exec approval is not enabled"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgExecApprovalDisabled)))
|
||||
return
|
||||
}
|
||||
|
||||
var params struct {
|
||||
ID string `json:"id"`
|
||||
Always bool `json:"always"` // true = allow-always, false = allow-once
|
||||
ID string `json:"id"`
|
||||
Always bool `json:"always"` // true = allow-always, false = allow-once
|
||||
}
|
||||
if req.Params != nil {
|
||||
json.Unmarshal(req.Params, ¶ms)
|
||||
}
|
||||
|
||||
if params.ID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "id is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "id")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,9 +93,10 @@ func (m *ExecApprovalMethods) handleApprove(_ context.Context, client *gateway.C
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *ExecApprovalMethods) handleDeny(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *ExecApprovalMethods) handleDeny(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.manager == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "exec approval is not enabled"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgExecApprovalDisabled)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -104,7 +108,7 @@ func (m *ExecApprovalMethods) handleDeny(_ context.Context, client *gateway.Clie
|
||||
}
|
||||
|
||||
if params.ID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "id is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "id")))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
|
||||
@@ -22,7 +24,8 @@ func (m *LogsMethods) Register(router *gateway.MethodRouter) {
|
||||
router.Register(protocol.MethodLogsTail, m.handleTail)
|
||||
}
|
||||
|
||||
func (m *LogsMethods) handleTail(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *LogsMethods) handleTail(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
Action string `json:"action"`
|
||||
Level string `json:"level"` // "debug", "info", "warn", "error" (default: "info")
|
||||
@@ -48,7 +51,7 @@ func (m *LogsMethods) handleTail(_ context.Context, client *gateway.Client, req
|
||||
client.SendResponse(protocol.NewErrorResponse(
|
||||
req.ID,
|
||||
protocol.ErrInvalidRequest,
|
||||
"action must be 'start' or 'stop'",
|
||||
i18n.T(locale, i18n.MsgInvalidLogAction),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -46,7 +47,8 @@ func (m *PairingMethods) Register(router *gateway.MethodRouter) {
|
||||
router.Register(protocol.MethodBrowserPairingStatus, m.handleBrowserPairingStatus)
|
||||
}
|
||||
|
||||
func (m *PairingMethods) handleRequest(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *PairingMethods) handleRequest(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
SenderID string `json:"senderId"`
|
||||
Channel string `json:"channel"`
|
||||
@@ -58,7 +60,7 @@ func (m *PairingMethods) handleRequest(_ context.Context, client *gateway.Client
|
||||
}
|
||||
|
||||
if params.SenderID == "" || params.Channel == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "senderId and channel are required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgSenderChannelRequired)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -78,6 +80,7 @@ func (m *PairingMethods) handleRequest(_ context.Context, client *gateway.Client
|
||||
}
|
||||
|
||||
func (m *PairingMethods) handleApprove(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
Code string `json:"code"`
|
||||
ApprovedBy string `json:"approvedBy"`
|
||||
@@ -87,7 +90,7 @@ func (m *PairingMethods) handleApprove(ctx context.Context, client *gateway.Clie
|
||||
}
|
||||
|
||||
if params.Code == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "code is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgCodeRequired)))
|
||||
return
|
||||
}
|
||||
if params.ApprovedBy == "" {
|
||||
@@ -115,7 +118,8 @@ func (m *PairingMethods) handleApprove(ctx context.Context, client *gateway.Clie
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *PairingMethods) handleDeny(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *PairingMethods) handleDeny(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
@@ -124,7 +128,7 @@ func (m *PairingMethods) handleDeny(_ context.Context, client *gateway.Client, r
|
||||
}
|
||||
|
||||
if params.Code == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "code is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgCodeRequired)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -152,7 +156,8 @@ func (m *PairingMethods) handleList(_ context.Context, client *gateway.Client, r
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *PairingMethods) handleRevoke(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *PairingMethods) handleRevoke(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
SenderID string `json:"senderId"`
|
||||
Channel string `json:"channel"`
|
||||
@@ -162,7 +167,7 @@ func (m *PairingMethods) handleRevoke(_ context.Context, client *gateway.Client,
|
||||
}
|
||||
|
||||
if params.SenderID == "" || params.Channel == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "senderId and channel are required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgSenderChannelRequired)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -193,7 +198,8 @@ func (m *PairingMethods) handleRevoke(_ context.Context, client *gateway.Client,
|
||||
|
||||
// handleBrowserPairingStatus lets a pending browser client check if its pairing code has been approved.
|
||||
// Called by unauthenticated clients during the browser pairing flow.
|
||||
func (m *PairingMethods) handleBrowserPairingStatus(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *PairingMethods) handleBrowserPairingStatus(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
SenderID string `json:"sender_id"`
|
||||
}
|
||||
@@ -202,7 +208,7 @@ func (m *PairingMethods) handleBrowserPairingStatus(_ context.Context, client *g
|
||||
}
|
||||
|
||||
if params.SenderID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "sender_id is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgSenderIDRequired)))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
|
||||
@@ -23,7 +25,8 @@ func (m *SendMethods) Register(router *gateway.MethodRouter) {
|
||||
router.Register(protocol.MethodSend, m.handleSend)
|
||||
}
|
||||
|
||||
func (m *SendMethods) handleSend(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *SendMethods) handleSend(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
Channel string `json:"channel"`
|
||||
To string `json:"to"`
|
||||
@@ -34,15 +37,15 @@ func (m *SendMethods) handleSend(_ context.Context, client *gateway.Client, req
|
||||
}
|
||||
|
||||
if params.Channel == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "channel is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "channel")))
|
||||
return
|
||||
}
|
||||
if params.To == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "to is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "to")))
|
||||
return
|
||||
}
|
||||
if params.Message == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "message is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgMsgRequired)))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -59,10 +60,11 @@ type sessionKeyParams struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
func (m *SessionsMethods) handlePreview(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *SessionsMethods) handlePreview(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params sessionKeyParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -78,7 +80,8 @@ func (m *SessionsMethods) handlePreview(_ context.Context, client *gateway.Clien
|
||||
|
||||
// handlePatch updates session metadata fields.
|
||||
// Matching TS sessions.patch (src/gateway/server-methods/sessions.ts:237-287).
|
||||
func (m *SessionsMethods) handlePatch(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *SessionsMethods) handlePatch(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
Key string `json:"key"`
|
||||
Label *string `json:"label,omitempty"`
|
||||
@@ -86,12 +89,12 @@ func (m *SessionsMethods) handlePatch(_ context.Context, client *gateway.Client,
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
if params.Key == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "key is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "key")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -119,10 +122,11 @@ func (m *SessionsMethods) handlePatch(_ context.Context, client *gateway.Client,
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *SessionsMethods) handleDelete(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *SessionsMethods) handleDelete(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params sessionKeyParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -136,10 +140,11 @@ func (m *SessionsMethods) handleDelete(_ context.Context, client *gateway.Client
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *SessionsMethods) handleReset(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *SessionsMethods) handleReset(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params sessionKeyParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -55,7 +56,8 @@ func (m *SkillsMethods) handleList(_ context.Context, client *gateway.Client, re
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *SkillsMethods) handleGet(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *SkillsMethods) handleGet(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
@@ -63,13 +65,13 @@ func (m *SkillsMethods) handleGet(_ context.Context, client *gateway.Client, req
|
||||
json.Unmarshal(req.Params, ¶ms)
|
||||
}
|
||||
if params.Name == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "name is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "name")))
|
||||
return
|
||||
}
|
||||
|
||||
info, ok := m.store.GetSkill(params.Name)
|
||||
if !ok {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "skill not found: "+params.Name))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgNotFound, "skill", params.Name)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,7 +102,8 @@ type skillUpdater interface {
|
||||
UpdateSkill(id uuid.UUID, updates map[string]interface{}) error
|
||||
}
|
||||
|
||||
func (m *SkillsMethods) handleUpdate(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *SkillsMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
var params struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
@@ -110,14 +113,14 @@ func (m *SkillsMethods) handleUpdate(_ context.Context, client *gateway.Client,
|
||||
json.Unmarshal(req.Params, ¶ms)
|
||||
}
|
||||
if params.Name == "" && params.ID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "name or id is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "name or id")))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the store supports updates (PGSkillStore does, FileSkillStore doesn't)
|
||||
updater, ok := m.store.(skillUpdater)
|
||||
if !ok {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "skills.update not supported for file-based skills"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgSkillsUpdateNotSupported)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -126,7 +129,7 @@ func (m *SkillsMethods) handleUpdate(_ context.Context, client *gateway.Client,
|
||||
if params.ID != "" {
|
||||
parsed, err := uuid.Parse(params.ID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid skill ID"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "skill")))
|
||||
return
|
||||
}
|
||||
skillID = parsed
|
||||
@@ -135,20 +138,20 @@ func (m *SkillsMethods) handleUpdate(_ context.Context, client *gateway.Client,
|
||||
// For PGSkillStore, the name is the slug
|
||||
info, exists := m.store.GetSkill(params.Name)
|
||||
if !exists {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "skill not found: "+params.Name))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, i18n.T(locale, i18n.MsgNotFound, "skill", params.Name)))
|
||||
return
|
||||
}
|
||||
// Try to parse Path as UUID (PGSkillStore stores DB ID in Path field for managed skills)
|
||||
parsed, err := uuid.Parse(info.Path)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "cannot resolve skill ID for file-based skill"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgCannotResolveSkillID)))
|
||||
return
|
||||
}
|
||||
skillID = parsed
|
||||
}
|
||||
|
||||
if params.Updates == nil || len(params.Updates) == 0 {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "updates is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "updates")))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/agent"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -50,13 +51,13 @@ func (m *TeamsMethods) Register(router *gateway.MethodRouter) {
|
||||
|
||||
// --- List ---
|
||||
|
||||
func (m *TeamsMethods) handleList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *TeamsMethods) handleList(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.teamStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "teams not configured"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgTeamsNotConfigured)))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
teams, err := m.teamStore.ListTeams(ctx)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, err.Error()))
|
||||
@@ -79,24 +80,25 @@ type teamsCreateParams struct {
|
||||
Settings json.RawMessage `json:"settings"`
|
||||
}
|
||||
|
||||
func (m *TeamsMethods) handleCreate(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *TeamsMethods) handleCreate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.teamStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "teams not configured"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgTeamsNotConfigured)))
|
||||
return
|
||||
}
|
||||
|
||||
var params teamsCreateParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
if params.Name == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "name is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "name")))
|
||||
return
|
||||
}
|
||||
if params.Lead == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "lead is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "lead")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -118,8 +120,6 @@ func (m *TeamsMethods) handleCreate(_ context.Context, client *gateway.Client, r
|
||||
memberAgents = append(memberAgents, ag)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create team
|
||||
team := &store.TeamData{
|
||||
Name: params.Name,
|
||||
@@ -130,13 +130,13 @@ func (m *TeamsMethods) handleCreate(_ context.Context, client *gateway.Client, r
|
||||
CreatedBy: client.UserID(),
|
||||
}
|
||||
if err := m.teamStore.CreateTeam(ctx, team); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to create team: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToCreate, "team", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
// Add lead as member with lead role
|
||||
if err := m.teamStore.AddMember(ctx, team.ID, leadAgent.ID, store.TeamRoleLead); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to add lead as member: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToCreate, "team lead membership", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -18,30 +19,30 @@ type teamsGetParams struct {
|
||||
TeamID string `json:"teamId"`
|
||||
}
|
||||
|
||||
func (m *TeamsMethods) handleGet(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *TeamsMethods) handleGet(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.teamStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "teams not configured"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgTeamsNotConfigured)))
|
||||
return
|
||||
}
|
||||
|
||||
var params teamsGetParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
if params.TeamID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "teamId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "teamId")))
|
||||
return
|
||||
}
|
||||
|
||||
teamID, err := uuid.Parse(params.TeamID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid teamId"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "teamId")))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
team, err := m.teamStore.GetTeam(ctx, teamID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, err.Error()))
|
||||
@@ -66,37 +67,36 @@ type teamsDeleteParams struct {
|
||||
TeamID string `json:"teamId"`
|
||||
}
|
||||
|
||||
func (m *TeamsMethods) handleDelete(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *TeamsMethods) handleDelete(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.teamStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "teams not configured"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgTeamsNotConfigured)))
|
||||
return
|
||||
}
|
||||
|
||||
var params teamsDeleteParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
if params.TeamID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "teamId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "teamId")))
|
||||
return
|
||||
}
|
||||
|
||||
teamID, err := uuid.Parse(params.TeamID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid teamId"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "teamId")))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Fetch team and members before deleting for event + cache invalidation
|
||||
team, _ := m.teamStore.GetTeam(ctx, teamID)
|
||||
members, _ := m.teamStore.ListMembers(ctx, teamID)
|
||||
|
||||
if err := m.teamStore.DeleteTeam(ctx, teamID); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to delete team: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToDelete, "team", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,30 +127,30 @@ type teamsTaskListParams struct {
|
||||
TeamID string `json:"teamId"`
|
||||
}
|
||||
|
||||
func (m *TeamsMethods) handleTaskList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *TeamsMethods) handleTaskList(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.teamStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "teams not configured"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgTeamsNotConfigured)))
|
||||
return
|
||||
}
|
||||
|
||||
var params teamsTaskListParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
if params.TeamID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "teamId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "teamId")))
|
||||
return
|
||||
}
|
||||
|
||||
teamID, err := uuid.Parse(params.TeamID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid teamId"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "teamId")))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
tasks, err := m.teamStore.ListTasks(ctx, teamID, "newest", store.TeamTaskFilterAll, "")
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, err.Error()))
|
||||
@@ -170,35 +170,34 @@ type teamsUpdateParams struct {
|
||||
Settings map[string]interface{} `json:"settings"`
|
||||
}
|
||||
|
||||
func (m *TeamsMethods) handleUpdate(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *TeamsMethods) handleUpdate(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.teamStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "teams not configured"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgTeamsNotConfigured)))
|
||||
return
|
||||
}
|
||||
|
||||
var params teamsUpdateParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
if params.TeamID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "teamId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "teamId")))
|
||||
return
|
||||
}
|
||||
|
||||
teamID, err := uuid.Parse(params.TeamID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid teamId"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "teamId")))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Validate team exists
|
||||
team, err := m.teamStore.GetTeam(ctx, teamID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "team not found: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgNotFound, "team", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -213,14 +212,14 @@ func (m *TeamsMethods) handleUpdate(_ context.Context, client *gateway.Client, r
|
||||
raw, _ := json.Marshal(params.Settings)
|
||||
var access teamAccessSettings
|
||||
if err := json.Unmarshal(raw, &access); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid settings: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidRequest, err.Error())))
|
||||
return
|
||||
}
|
||||
cleaned, _ := json.Marshal(access)
|
||||
|
||||
updates := map[string]any{"settings": json.RawMessage(cleaned)}
|
||||
if err := m.teamStore.UpdateTeam(ctx, teamID, updates); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to update team: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToUpdate, "team", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -251,30 +250,30 @@ type teamsKnownUsersParams struct {
|
||||
TeamID string `json:"teamId"`
|
||||
}
|
||||
|
||||
func (m *TeamsMethods) handleKnownUsers(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *TeamsMethods) handleKnownUsers(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.teamStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "teams not configured"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgTeamsNotConfigured)))
|
||||
return
|
||||
}
|
||||
|
||||
var params teamsKnownUsersParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
|
||||
if params.TeamID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "teamId is required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "teamId")))
|
||||
return
|
||||
}
|
||||
|
||||
teamID, err := uuid.Parse(params.TeamID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid teamId"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "teamId")))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
users, err := m.teamStore.KnownUserIDs(ctx, teamID, 100)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, err.Error()))
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -20,34 +21,33 @@ type teamsAddMemberParams struct {
|
||||
Agent string `json:"agent"` // agent key or UUID
|
||||
}
|
||||
|
||||
func (m *TeamsMethods) handleAddMember(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *TeamsMethods) handleAddMember(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.teamStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "teams not configured"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgTeamsNotConfigured)))
|
||||
return
|
||||
}
|
||||
|
||||
var params teamsAddMemberParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
if params.TeamID == "" || params.Agent == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "teamId and agent are required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "teamId and agent")))
|
||||
return
|
||||
}
|
||||
|
||||
teamID, err := uuid.Parse(params.TeamID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid teamId"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "teamId")))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Validate team exists
|
||||
team, err := m.teamStore.GetTeam(ctx, teamID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "team not found: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgNotFound, "team", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -60,13 +60,13 @@ func (m *TeamsMethods) handleAddMember(_ context.Context, client *gateway.Client
|
||||
|
||||
// Prevent adding lead again
|
||||
if ag.ID == team.LeadAgentID {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "agent is already the team lead"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgAgentIsTeamLead)))
|
||||
return
|
||||
}
|
||||
|
||||
// Add member
|
||||
if err := m.teamStore.AddMember(ctx, teamID, ag.ID, store.TeamRoleMember); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to add member: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToCreate, "member", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -106,43 +106,42 @@ type teamsRemoveMemberParams struct {
|
||||
AgentID string `json:"agentId"` // agent UUID
|
||||
}
|
||||
|
||||
func (m *TeamsMethods) handleRemoveMember(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
func (m *TeamsMethods) handleRemoveMember(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
locale := store.LocaleFromContext(ctx)
|
||||
if m.teamStore == nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "teams not configured"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgTeamsNotConfigured)))
|
||||
return
|
||||
}
|
||||
|
||||
var params teamsRemoveMemberParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid params"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
|
||||
return
|
||||
}
|
||||
if params.TeamID == "" || params.AgentID == "" {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "teamId and agentId are required"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "teamId and agentId")))
|
||||
return
|
||||
}
|
||||
|
||||
teamID, err := uuid.Parse(params.TeamID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid teamId"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "teamId")))
|
||||
return
|
||||
}
|
||||
agentID, err := uuid.Parse(params.AgentID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid agentId"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidID, "agentId")))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Validate team exists and prevent removing the lead
|
||||
team, err := m.teamStore.GetTeam(ctx, teamID)
|
||||
if err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "team not found: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgNotFound, "team", err.Error())))
|
||||
return
|
||||
}
|
||||
if agentID == team.LeadAgentID {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "cannot remove the team lead"))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgCannotRemoveTeamLead)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -151,7 +150,7 @@ func (m *TeamsMethods) handleRemoveMember(_ context.Context, client *gateway.Cli
|
||||
|
||||
// Remove member
|
||||
if err := m.teamStore.RemoveMember(ctx, teamID, agentID); err != nil {
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, "failed to remove member: "+err.Error()))
|
||||
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToDelete, "member", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/permissions"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
|
||||
@@ -38,10 +40,11 @@ func (r *MethodRouter) Handle(ctx context.Context, client *Client, req *protocol
|
||||
handler, ok := r.handlers[req.Method]
|
||||
if !ok {
|
||||
slog.Warn("unknown method", "method", req.Method, "client", client.id)
|
||||
locale := i18n.Normalize(client.locale)
|
||||
client.SendResponse(protocol.NewErrorResponse(
|
||||
req.ID,
|
||||
protocol.ErrInvalidRequest,
|
||||
"unknown method: "+req.Method,
|
||||
i18n.T(locale, i18n.MsgUnknownMethod, req.Method),
|
||||
))
|
||||
return
|
||||
}
|
||||
@@ -51,16 +54,20 @@ func (r *MethodRouter) Handle(ctx context.Context, client *Client, req *protocol
|
||||
if pe := r.server.policyEngine; pe != nil {
|
||||
if !pe.CanAccess(client.role, req.Method) {
|
||||
slog.Warn("permission denied", "method", req.Method, "role", client.role, "client", client.id)
|
||||
locale := i18n.Normalize(client.locale)
|
||||
client.SendResponse(protocol.NewErrorResponse(
|
||||
req.ID,
|
||||
protocol.ErrUnauthorized,
|
||||
"permission denied: insufficient role for "+req.Method,
|
||||
i18n.T(locale, i18n.MsgPermissionDenied, req.Method),
|
||||
))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inject locale into context for i18n support
|
||||
ctx = store.WithLocale(ctx, i18n.Normalize(client.locale))
|
||||
|
||||
slog.Debug("handling method", "method", req.Method, "client", client.id, "req_id", req.ID)
|
||||
handler(ctx, client, req)
|
||||
}
|
||||
@@ -81,11 +88,15 @@ func (r *MethodRouter) handleConnect(ctx context.Context, client *Client, req *p
|
||||
Token string `json:"token"`
|
||||
UserID string `json:"user_id"`
|
||||
SenderID string `json:"sender_id"` // browser pairing: stored sender ID for reconnect
|
||||
Locale string `json:"locale"` // user's preferred locale (en, vi, zh)
|
||||
}
|
||||
if req.Params != nil {
|
||||
json.Unmarshal(req.Params, ¶ms)
|
||||
}
|
||||
|
||||
// Set locale on client (persists across all requests for this connection)
|
||||
client.locale = i18n.Normalize(params.Locale)
|
||||
|
||||
configToken := r.server.cfg.Gateway.Token
|
||||
|
||||
// Path 1: Valid token → admin
|
||||
|
||||
+29
-21
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bootstrap"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -68,16 +69,18 @@ func (h *AgentsHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if h.token != "" {
|
||||
if extractBearerToken(r) != h.token {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
// Inject user_id into context
|
||||
// Inject user_id and locale into context
|
||||
userID := extractUserID(r)
|
||||
ctx := store.WithLocale(r.Context(), extractLocale(r))
|
||||
if userID != "" {
|
||||
ctx := store.WithUserID(r.Context(), userID)
|
||||
r = r.WithContext(ctx)
|
||||
ctx = store.WithUserID(ctx, userID)
|
||||
}
|
||||
r = r.WithContext(ctx)
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
@@ -85,7 +88,8 @@ func (h *AgentsHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
func (h *AgentsHandler) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
userID := store.UserIDFromContext(r.Context())
|
||||
if userID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "X-GoClaw-User-Id header required"})
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgUserIDHeader)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -106,25 +110,26 @@ func (h *AgentsHandler) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *AgentsHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
|
||||
userID := store.UserIDFromContext(r.Context())
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
if userID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "X-GoClaw-User-Id header required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgUserIDHeader)})
|
||||
return
|
||||
}
|
||||
|
||||
var req store.AgentData
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
if !isValidSlug(req.AgentKey) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "agent_key must be a valid slug (lowercase letters, numbers, hyphens only)"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidSlug, "agent_key")})
|
||||
return
|
||||
}
|
||||
|
||||
// Check for duplicate agent_key before creating
|
||||
if existing, _ := h.agents.GetByKey(r.Context(), req.AgentKey); existing != nil {
|
||||
writeJSON(w, http.StatusConflict, map[string]string{"error": "An agent with key \"" + req.AgentKey + "\" already exists"})
|
||||
writeJSON(w, http.StatusConflict, map[string]string{"error": i18n.T(locale, i18n.MsgAlreadyExists, "agent", req.AgentKey)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -161,7 +166,7 @@ func (h *AgentsHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := h.agents.Create(r.Context(), &req); err != nil {
|
||||
if strings.Contains(err.Error(), "duplicate key") || strings.Contains(err.Error(), "23505") {
|
||||
writeJSON(w, http.StatusConflict, map[string]string{"error": "An agent with this key already exists"})
|
||||
writeJSON(w, http.StatusConflict, map[string]string{"error": i18n.T(locale, i18n.MsgAlreadyExists, "agent", req.AgentKey)})
|
||||
} else {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
@@ -184,6 +189,7 @@ func (h *AgentsHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *AgentsHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
userID := store.UserIDFromContext(r.Context())
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
isOwner := h.isOwnerUser(userID)
|
||||
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
@@ -191,12 +197,12 @@ func (h *AgentsHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
// Try by agent_key
|
||||
ag, err2 := h.agents.GetByKey(r.Context(), r.PathValue("id"))
|
||||
if err2 != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "agent", r.PathValue("id"))})
|
||||
return
|
||||
}
|
||||
if userID != "" && !isOwner {
|
||||
if ok, _, _ := h.agents.CanAccess(r.Context(), ag.ID, userID); !ok {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "no access to this agent"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgNoAccess, "agent")})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -206,13 +212,13 @@ func (h *AgentsHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ag, err := h.agents.GetByID(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "agent", id.String())})
|
||||
return
|
||||
}
|
||||
|
||||
if userID != "" && !isOwner {
|
||||
if ok, _, _ := h.agents.CanAccess(r.Context(), id, userID); !ok {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "no access to this agent"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgNoAccess, "agent")})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -222,26 +228,27 @@ func (h *AgentsHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *AgentsHandler) handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
userID := store.UserIDFromContext(r.Context())
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
// Only owner can update
|
||||
ag, err := h.agents.GetByID(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "agent", id.String())})
|
||||
return
|
||||
}
|
||||
if userID != "" && ag.OwnerID != userID && !h.isOwnerUser(userID) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "only owner can update agent"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgOwnerOnly, "update agent")})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -277,20 +284,21 @@ func (h *AgentsHandler) handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *AgentsHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
userID := store.UserIDFromContext(r.Context())
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
// Only owner can delete
|
||||
ag, err := h.agents.GetByID(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "agent", id.String())})
|
||||
return
|
||||
}
|
||||
if userID != "" && ag.OwnerID != userID && !h.isOwnerUser(userID) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "only owner can delete agent"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgOwnerOnly, "delete agent")})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,25 +8,27 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
// handleListInstances returns all user instances for a predefined agent.
|
||||
func (h *AgentsHandler) handleListInstances(w http.ResponseWriter, r *http.Request) {
|
||||
userID := store.UserIDFromContext(r.Context())
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
ag, err := h.agents.GetByID(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "agent", id.String())})
|
||||
return
|
||||
}
|
||||
if userID != "" && ag.OwnerID != userID && !h.isOwnerUser(userID) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "only owner can view instances"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgOwnerOnly, "view instances")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -42,24 +44,25 @@ func (h *AgentsHandler) handleListInstances(w http.ResponseWriter, r *http.Reque
|
||||
// handleGetInstanceFiles returns user context files for a specific instance.
|
||||
func (h *AgentsHandler) handleGetInstanceFiles(w http.ResponseWriter, r *http.Request) {
|
||||
callerID := store.UserIDFromContext(r.Context())
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
instanceUserID := r.PathValue("userID")
|
||||
if instanceUserID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing userID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "userID")})
|
||||
return
|
||||
}
|
||||
|
||||
ag, err := h.agents.GetByID(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "agent", id.String())})
|
||||
return
|
||||
}
|
||||
if callerID != "" && ag.OwnerID != callerID && !h.isOwnerUser(callerID) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "only owner can view instance files"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgOwnerOnly, "view instance files")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -75,44 +78,45 @@ func (h *AgentsHandler) handleGetInstanceFiles(w http.ResponseWriter, r *http.Re
|
||||
// handleSetInstanceFile updates a user context file for a specific instance.
|
||||
func (h *AgentsHandler) handleSetInstanceFile(w http.ResponseWriter, r *http.Request) {
|
||||
callerID := store.UserIDFromContext(r.Context())
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
instanceUserID := r.PathValue("userID")
|
||||
fileName := r.PathValue("fileName")
|
||||
if instanceUserID == "" || fileName == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing userID or fileName"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "userID and fileName")})
|
||||
return
|
||||
}
|
||||
|
||||
// Only USER.md can be edited via this endpoint — other files are managed by the agent itself.
|
||||
if fileName != "USER.md" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "only USER.md can be edited via this endpoint"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, "only USER.md can be edited via this endpoint")})
|
||||
return
|
||||
}
|
||||
|
||||
ag, err := h.agents.GetByID(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "agent", id.String())})
|
||||
return
|
||||
}
|
||||
if callerID != "" && ag.OwnerID != callerID && !h.isOwnerUser(callerID) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "only owner can edit instance files"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgOwnerOnly, "edit instance files")})
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1MB limit
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to read body"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, err.Error())})
|
||||
return
|
||||
}
|
||||
var payload struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -130,41 +134,42 @@ func (h *AgentsHandler) handleSetInstanceFile(w http.ResponseWriter, r *http.Req
|
||||
// handleUpdateInstanceMetadata updates metadata for a user instance.
|
||||
func (h *AgentsHandler) handleUpdateInstanceMetadata(w http.ResponseWriter, r *http.Request) {
|
||||
callerID := store.UserIDFromContext(r.Context())
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
instanceUserID := r.PathValue("userID")
|
||||
if instanceUserID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing userID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "userID")})
|
||||
return
|
||||
}
|
||||
|
||||
ag, err := h.agents.GetByID(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "agent", id.String())})
|
||||
return
|
||||
}
|
||||
if callerID != "" && ag.OwnerID != callerID && !h.isOwnerUser(callerID) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "only owner can edit instance metadata"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgOwnerOnly, "edit instance metadata")})
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to read body"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, err.Error())})
|
||||
return
|
||||
}
|
||||
var payload struct {
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
if len(payload.Metadata) == 0 {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "metadata is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "metadata")})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -6,25 +6,27 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
func (h *AgentsHandler) handleListShares(w http.ResponseWriter, r *http.Request) {
|
||||
userID := store.UserIDFromContext(r.Context())
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
// Only owner can list shares
|
||||
ag, err := h.agents.GetByID(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "agent", id.String())})
|
||||
return
|
||||
}
|
||||
if userID != "" && ag.OwnerID != userID && !h.isOwnerUser(userID) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "only owner can view shares"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgOwnerOnly, "view shares")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -39,20 +41,21 @@ func (h *AgentsHandler) handleListShares(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
func (h *AgentsHandler) handleShare(w http.ResponseWriter, r *http.Request) {
|
||||
userID := store.UserIDFromContext(r.Context())
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
// Only owner can share
|
||||
ag, err := h.agents.GetByID(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "agent", id.String())})
|
||||
return
|
||||
}
|
||||
if userID != "" && ag.OwnerID != userID && !h.isOwnerUser(userID) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "only owner can share agent"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgOwnerOnly, "share agent")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -61,11 +64,11 @@ func (h *AgentsHandler) handleShare(w http.ResponseWriter, r *http.Request) {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, err.Error())})
|
||||
return
|
||||
}
|
||||
if req.UserID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "user_id is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "user_id")})
|
||||
return
|
||||
}
|
||||
if err := store.ValidateUserID(req.UserID); err != nil {
|
||||
@@ -86,20 +89,21 @@ func (h *AgentsHandler) handleShare(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *AgentsHandler) handleRevokeShare(w http.ResponseWriter, r *http.Request) {
|
||||
userID := store.UserIDFromContext(r.Context())
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
// Only owner can revoke shares
|
||||
ag, err := h.agents.GetByID(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "agent", id.String())})
|
||||
return
|
||||
}
|
||||
if userID != "" && ag.OwnerID != userID && !h.isOwnerUser(userID) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "only owner can revoke shares"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgOwnerOnly, "revoke shares")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -118,28 +122,29 @@ func (h *AgentsHandler) handleRevokeShare(w http.ResponseWriter, r *http.Request
|
||||
|
||||
func (h *AgentsHandler) handleRegenerate(w http.ResponseWriter, r *http.Request) {
|
||||
userID := store.UserIDFromContext(r.Context())
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
// Only owner can regenerate
|
||||
ag, err := h.agents.GetByID(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "agent", id.String())})
|
||||
return
|
||||
}
|
||||
if userID != "" && ag.OwnerID != userID && !h.isOwnerUser(userID) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "only owner can regenerate agent"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgOwnerOnly, "regenerate agent")})
|
||||
return
|
||||
}
|
||||
if ag.Status == store.AgentStatusSummoning {
|
||||
writeJSON(w, http.StatusConflict, map[string]string{"error": "agent is already being summoned"})
|
||||
writeJSON(w, http.StatusConflict, map[string]string{"error": i18n.T(locale, i18n.MsgAlreadySummoning)})
|
||||
return
|
||||
}
|
||||
if h.summoner == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "summoning not available"})
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": i18n.T(locale, i18n.MsgSummoningUnavailable)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -147,11 +152,11 @@ func (h *AgentsHandler) handleRegenerate(w http.ResponseWriter, r *http.Request)
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, err.Error())})
|
||||
return
|
||||
}
|
||||
if req.Prompt == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "prompt is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "prompt")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -170,33 +175,34 @@ func (h *AgentsHandler) handleRegenerate(w http.ResponseWriter, r *http.Request)
|
||||
// Used when initial summoning failed (e.g. wrong model) and user wants to retry.
|
||||
func (h *AgentsHandler) handleResummon(w http.ResponseWriter, r *http.Request) {
|
||||
userID := store.UserIDFromContext(r.Context())
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
ag, err := h.agents.GetByID(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "agent not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "agent", id.String())})
|
||||
return
|
||||
}
|
||||
if userID != "" && ag.OwnerID != userID && !h.isOwnerUser(userID) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "only owner can resummon agent"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgOwnerOnly, "resummon agent")})
|
||||
return
|
||||
}
|
||||
if ag.Status == store.AgentStatusSummoning {
|
||||
writeJSON(w, http.StatusConflict, map[string]string{"error": "agent is already being summoned"})
|
||||
writeJSON(w, http.StatusConflict, map[string]string{"error": i18n.T(locale, i18n.MsgAlreadySummoning)})
|
||||
return
|
||||
}
|
||||
if h.summoner == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "summoning not available"})
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": i18n.T(locale, i18n.MsgSummoningUnavailable)})
|
||||
return
|
||||
}
|
||||
|
||||
description := extractDescription(ag.OtherConfig)
|
||||
if description == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "agent has no description to resummon from"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgNoDescription)})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
@@ -66,3 +67,21 @@ func extractAgentID(r *http.Request, model string) string {
|
||||
|
||||
return "default"
|
||||
}
|
||||
|
||||
// extractLocale parses the Accept-Language header and returns a supported locale.
|
||||
// Falls back to "en" if no supported language is found.
|
||||
func extractLocale(r *http.Request) string {
|
||||
accept := r.Header.Get("Accept-Language")
|
||||
if accept == "" {
|
||||
return i18n.DefaultLocale
|
||||
}
|
||||
// Simple parser: take the first language tag before comma or semicolon
|
||||
for _, part := range strings.Split(accept, ",") {
|
||||
tag := strings.TrimSpace(strings.SplitN(part, ";", 2)[0])
|
||||
locale := i18n.Normalize(tag)
|
||||
if locale != i18n.DefaultLocale || strings.HasPrefix(tag, "en") {
|
||||
return locale
|
||||
}
|
||||
}
|
||||
return i18n.DefaultLocale
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -34,7 +35,8 @@ func (h *BuiltinToolsHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if h.token != "" {
|
||||
if extractBearerToken(r) != h.token {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -56,22 +58,24 @@ func (h *BuiltinToolsHandler) handleList(w http.ResponseWriter, r *http.Request)
|
||||
result, err := h.store.List(r.Context())
|
||||
if err != nil {
|
||||
slog.Error("builtin_tools.list", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list tools"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToList, "tools")})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"tools": result})
|
||||
}
|
||||
|
||||
func (h *BuiltinToolsHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
name := r.PathValue("name")
|
||||
if name == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "name")})
|
||||
return
|
||||
}
|
||||
|
||||
def, err := h.store.Get(r.Context(), name)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "tool not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "tool", name)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -79,15 +83,16 @@ func (h *BuiltinToolsHandler) handleGet(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (h *BuiltinToolsHandler) handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
name := r.PathValue("name")
|
||||
if name == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "name")})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&updates); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -101,7 +106,7 @@ func (h *BuiltinToolsHandler) handleUpdate(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
if len(allowed) == 0 {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "no valid fields to update (only enabled and settings are editable)"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidUpdates)})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -47,15 +48,17 @@ func (h *ChannelInstancesHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if h.token != "" {
|
||||
if extractBearerToken(r) != h.token {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
userID := extractUserID(r)
|
||||
ctx := store.WithLocale(r.Context(), extractLocale(r))
|
||||
if userID != "" {
|
||||
ctx := store.WithUserID(r.Context(), userID)
|
||||
r = r.WithContext(ctx)
|
||||
ctx = store.WithUserID(ctx, userID)
|
||||
}
|
||||
r = r.WithContext(ctx)
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
@@ -93,7 +96,8 @@ func (h *ChannelInstancesHandler) handleList(w http.ResponseWriter, r *http.Requ
|
||||
instances, err := h.store.ListPaged(r.Context(), opts)
|
||||
if err != nil {
|
||||
slog.Error("channel_instances.list", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list instances"})
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToList, "instances")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -113,6 +117,7 @@ func (h *ChannelInstancesHandler) handleList(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
func (h *ChannelInstancesHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
@@ -123,23 +128,23 @@ func (h *ChannelInstancesHandler) handleCreate(w http.ResponseWriter, r *http.Re
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
if body.Name == "" || body.ChannelType == "" || body.AgentID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name, channel_type, and agent_id are required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "name, channel_type, and agent_id")})
|
||||
return
|
||||
}
|
||||
|
||||
if !isValidChannelType(body.ChannelType) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid channel_type"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidChannelType)})
|
||||
return
|
||||
}
|
||||
|
||||
agentID, err := uuid.Parse(body.AgentID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent_id"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -172,15 +177,16 @@ func (h *ChannelInstancesHandler) handleCreate(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
func (h *ChannelInstancesHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid instance ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "instance")})
|
||||
return
|
||||
}
|
||||
|
||||
inst, err := h.store.Get(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "instance not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgInstanceNotFound)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -188,15 +194,16 @@ func (h *ChannelInstancesHandler) handleGet(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
func (h *ChannelInstancesHandler) handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid instance ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "instance")})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&updates); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -211,20 +218,21 @@ func (h *ChannelInstancesHandler) handleUpdate(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
func (h *ChannelInstancesHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid instance ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "instance")})
|
||||
return
|
||||
}
|
||||
|
||||
// Look up instance to check if it's a default (seeded) instance.
|
||||
inst, err := h.store.Get(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "instance not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgInstanceNotFound)})
|
||||
return
|
||||
}
|
||||
if store.IsDefaultChannelInstance(inst.Name) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "cannot delete default channel instance"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgCannotDeleteDefaultInst)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -241,18 +249,18 @@ func (h *ChannelInstancesHandler) handleDelete(w http.ResponseWriter, r *http.Re
|
||||
// maskInstanceHTTP returns a map with credentials masked for HTTP responses.
|
||||
func maskInstanceHTTP(inst store.ChannelInstanceData) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"id": inst.ID,
|
||||
"name": inst.Name,
|
||||
"display_name": inst.DisplayName,
|
||||
"channel_type": inst.ChannelType,
|
||||
"agent_id": inst.AgentID,
|
||||
"config": inst.Config,
|
||||
"enabled": inst.Enabled,
|
||||
"is_default": store.IsDefaultChannelInstance(inst.Name),
|
||||
"has_credentials": len(inst.Credentials) > 0,
|
||||
"created_by": inst.CreatedBy,
|
||||
"created_at": inst.CreatedAt,
|
||||
"updated_at": inst.UpdatedAt,
|
||||
"id": inst.ID,
|
||||
"name": inst.Name,
|
||||
"display_name": inst.DisplayName,
|
||||
"channel_type": inst.ChannelType,
|
||||
"agent_id": inst.AgentID,
|
||||
"config": inst.Config,
|
||||
"enabled": inst.Enabled,
|
||||
"is_default": store.IsDefaultChannelInstance(inst.Name),
|
||||
"has_credentials": len(inst.Credentials) > 0,
|
||||
"created_by": inst.CreatedBy,
|
||||
"created_at": inst.CreatedAt,
|
||||
"updated_at": inst.UpdatedAt,
|
||||
}
|
||||
|
||||
if len(inst.Credentials) > 0 {
|
||||
@@ -277,14 +285,15 @@ func maskInstanceHTTP(inst store.ChannelInstanceData) map[string]interface{} {
|
||||
|
||||
// resolveAgentID looks up the channel instance and returns its agent_id.
|
||||
func (h *ChannelInstancesHandler) resolveAgentID(w http.ResponseWriter, r *http.Request) (uuid.UUID, bool) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
instID, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid instance ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "instance")})
|
||||
return uuid.Nil, false
|
||||
}
|
||||
inst, err := h.store.Get(r.Context(), instID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "instance not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgInstanceNotFound)})
|
||||
return uuid.Nil, false
|
||||
}
|
||||
return inst.AgentID, true
|
||||
@@ -298,7 +307,8 @@ func (h *ChannelInstancesHandler) handleWriterGroups(w http.ResponseWriter, r *h
|
||||
groups, err := h.agentStore.ListGroupFileWriterGroups(r.Context(), agentID)
|
||||
if err != nil {
|
||||
slog.Error("channel_instances.writer_groups", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list writer groups"})
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToList, "writer groups")})
|
||||
return
|
||||
}
|
||||
if groups == nil {
|
||||
@@ -312,15 +322,16 @@ func (h *ChannelInstancesHandler) handleListWriters(w http.ResponseWriter, r *ht
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
groupID := r.URL.Query().Get("group_id")
|
||||
if groupID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "group_id query parameter is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "group_id")})
|
||||
return
|
||||
}
|
||||
writers, err := h.agentStore.ListGroupFileWriters(r.Context(), agentID, groupID)
|
||||
if err != nil {
|
||||
slog.Error("channel_instances.list_writers", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list writers"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToList, "writers")})
|
||||
return
|
||||
}
|
||||
if writers == nil {
|
||||
@@ -334,6 +345,7 @@ func (h *ChannelInstancesHandler) handleAddWriter(w http.ResponseWriter, r *http
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
var body struct {
|
||||
GroupID string `json:"group_id"`
|
||||
UserID string `json:"user_id"`
|
||||
@@ -341,16 +353,16 @@ func (h *ChannelInstancesHandler) handleAddWriter(w http.ResponseWriter, r *http
|
||||
Username string `json:"username"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<16)).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
if body.GroupID == "" || body.UserID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "group_id and user_id are required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "group_id and user_id")})
|
||||
return
|
||||
}
|
||||
if err := h.agentStore.AddGroupFileWriter(r.Context(), agentID, body.GroupID, body.UserID, body.DisplayName, body.Username); err != nil {
|
||||
slog.Error("channel_instances.add_writer", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to add writer"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToCreate, "writer", err.Error())})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"status": "added"})
|
||||
@@ -361,15 +373,16 @@ func (h *ChannelInstancesHandler) handleRemoveWriter(w http.ResponseWriter, r *h
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
userID := r.PathValue("userId")
|
||||
groupID := r.URL.Query().Get("group_id")
|
||||
if groupID == "" || userID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "group_id query parameter and userId path parameter are required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "group_id and userId")})
|
||||
return
|
||||
}
|
||||
if err := h.agentStore.RemoveGroupFileWriter(r.Context(), agentID, groupID, userID); err != nil {
|
||||
slog.Error("channel_instances.remove_writer", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to remove writer"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToDelete, "writer", err.Error())})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "removed"})
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/agent"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/sessions"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
@@ -74,14 +75,16 @@ type chatUsage struct {
|
||||
}
|
||||
|
||||
func (h *ChatCompletionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
http.Error(w, i18n.T(locale, i18n.MsgMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Auth check (timing-safe comparison)
|
||||
if !tokenMatch(extractBearerToken(r), h.token) {
|
||||
http.Error(w, `{"error":{"message":"Invalid authentication","type":"invalid_request_error"}}`, http.StatusUnauthorized)
|
||||
http.Error(w, fmt.Sprintf(`{"error":{"message":"%s","type":"invalid_request_error"}}`, i18n.T(locale, i18n.MsgInvalidAuth)), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -93,7 +96,7 @@ func (h *ChatCompletionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
if !h.rateLimiter(key) {
|
||||
w.Header().Set("Retry-After", "60")
|
||||
http.Error(w, `{"error":{"message":"Rate limit exceeded","type":"rate_limit_error"}}`, http.StatusTooManyRequests)
|
||||
http.Error(w, fmt.Sprintf(`{"error":{"message":"%s","type":"rate_limit_error"}}`, i18n.T(locale, i18n.MsgRateLimitExceeded)), http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -104,25 +107,25 @@ func (h *ChatCompletionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
var req chatCompletionsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, fmt.Sprintf(`{"error":{"message":"Invalid JSON: %s"}}`, err), http.StatusBadRequest)
|
||||
http.Error(w, fmt.Sprintf(`{"error":{"message":"%s"}}`, i18n.T(locale, i18n.MsgInvalidRequest, err.Error())), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Messages) == 0 {
|
||||
http.Error(w, `{"error":{"message":"messages is required"}}`, http.StatusBadRequest)
|
||||
http.Error(w, fmt.Sprintf(`{"error":{"message":"%s"}}`, i18n.T(locale, i18n.MsgMsgsRequired)), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
agentID := extractAgentID(r, req.Model)
|
||||
userID := extractUserID(r)
|
||||
if h.isManaged && userID == "" {
|
||||
http.Error(w, `{"error":{"message":"X-GoClaw-User-Id header is required in managed mode"}}`, http.StatusBadRequest)
|
||||
http.Error(w, fmt.Sprintf(`{"error":{"message":"%s"}}`, i18n.T(locale, i18n.MsgUserIDHeader)), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
loop, err := h.agents.Get(agentID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf(`{"error":{"message":"Agent not found: %s"}}`, agentID), http.StatusNotFound)
|
||||
http.Error(w, fmt.Sprintf(`{"error":{"message":"%s"}}`, i18n.T(locale, i18n.MsgNotFound, "agent", agentID)), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -135,12 +138,12 @@ func (h *ChatCompletionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
}
|
||||
if lastMessage == "" {
|
||||
http.Error(w, `{"error":{"message":"No user message found"}}`, http.StatusBadRequest)
|
||||
http.Error(w, fmt.Sprintf(`{"error":{"message":"%s"}}`, i18n.T(locale, i18n.MsgNoUserMessage)), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Inject user_id into context for downstream stores/tools
|
||||
ctx := r.Context()
|
||||
// Inject user_id and locale into context for downstream stores/tools
|
||||
ctx := store.WithLocale(r.Context(), extractLocale(r))
|
||||
if userID != "" {
|
||||
ctx = store.WithUserID(ctx, userID)
|
||||
}
|
||||
@@ -174,7 +177,8 @@ func (h *ChatCompletionsHandler) handleNonStream(w http.ResponseWriter, r *http.
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf(`{"error":{"message":"Agent error: %s"}}`, err), http.StatusInternalServerError)
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
http.Error(w, fmt.Sprintf(`{"error":{"message":"%s"}}`, i18n.T(locale, i18n.MsgInternalError, err.Error())), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -205,7 +209,8 @@ func (h *ChatCompletionsHandler) handleNonStream(w http.ResponseWriter, r *http.
|
||||
func (h *ChatCompletionsHandler) handleStream(w http.ResponseWriter, r *http.Request, loop agent.Agent, runID, sessionKey, message, model, userID string) {
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "streaming not supported", http.StatusInternalServerError)
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
http.Error(w, i18n.T(locale, i18n.MsgStreamingNotSupported), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
@@ -40,15 +41,17 @@ func (h *CustomToolsHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if h.token != "" {
|
||||
if extractBearerToken(r) != h.token {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
userID := extractUserID(r)
|
||||
ctx := store.WithLocale(r.Context(), extractLocale(r))
|
||||
if userID != "" {
|
||||
ctx := store.WithUserID(r.Context(), userID)
|
||||
r = r.WithContext(ctx)
|
||||
ctx = store.WithUserID(ctx, userID)
|
||||
}
|
||||
r = r.WithContext(ctx)
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
@@ -64,6 +67,7 @@ func (h *CustomToolsHandler) emitCacheInvalidate(key string) {
|
||||
}
|
||||
|
||||
func (h *CustomToolsHandler) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
opts := store.CustomToolListOpts{
|
||||
Limit: 50,
|
||||
Offset: 0,
|
||||
@@ -72,7 +76,7 @@ func (h *CustomToolsHandler) handleList(w http.ResponseWriter, r *http.Request)
|
||||
if v := r.URL.Query().Get("agent_id"); v != "" {
|
||||
id, err := uuid.Parse(v)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent_id"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
opts.AgentID = &id
|
||||
@@ -94,7 +98,7 @@ func (h *CustomToolsHandler) handleList(w http.ResponseWriter, r *http.Request)
|
||||
result, err := h.store.ListPaged(r.Context(), opts)
|
||||
if err != nil {
|
||||
slog.Error("custom_tools.list", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list tools"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToList, "tools")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -109,25 +113,26 @@ func (h *CustomToolsHandler) handleList(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (h *CustomToolsHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
var def store.CustomToolDef
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&def); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
if def.Name == "" || def.Command == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name and command are required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "name and command")})
|
||||
return
|
||||
}
|
||||
if !isValidSlug(def.Name) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name must be a valid slug (lowercase letters, numbers, hyphens only)"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidSlug, "name")})
|
||||
return
|
||||
}
|
||||
|
||||
// Check name collision with built-in/MCP tools
|
||||
if h.toolsReg != nil {
|
||||
if _, exists := h.toolsReg.Get(def.Name); exists {
|
||||
writeJSON(w, http.StatusConflict, map[string]string{"error": "tool name conflicts with existing built-in or MCP tool"})
|
||||
writeJSON(w, http.StatusConflict, map[string]string{"error": i18n.T(locale, i18n.MsgAlreadyExists, "tool name", def.Name)})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -152,15 +157,16 @@ func (h *CustomToolsHandler) handleCreate(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
func (h *CustomToolsHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid tool ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "tool")})
|
||||
return
|
||||
}
|
||||
|
||||
def, err := h.store.Get(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "tool not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "tool", id.String())})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -168,21 +174,22 @@ func (h *CustomToolsHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *CustomToolsHandler) handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid tool ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "tool")})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&updates); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
if name, ok := updates["name"]; ok {
|
||||
if s, _ := name.(string); !isValidSlug(s) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name must be a valid slug (lowercase letters, numbers, hyphens only)"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidSlug, "name")})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -198,9 +205,10 @@ func (h *CustomToolsHandler) handleUpdate(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
func (h *CustomToolsHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid tool ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "tool")})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
@@ -28,10 +29,14 @@ func (h *DelegationsHandler) authMiddleware(next http.HandlerFunc) http.HandlerF
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if h.token != "" {
|
||||
if extractBearerToken(r) != h.token {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
locale := extractLocale(r)
|
||||
ctx := store.WithLocale(r.Context(), locale)
|
||||
r = r.WithContext(ctx)
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
@@ -87,10 +92,11 @@ func (h *DelegationsHandler) handleList(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (h *DelegationsHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
idStr := r.PathValue("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "delegation")})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
)
|
||||
|
||||
// FilesHandler serves files over HTTP with Bearer token auth.
|
||||
@@ -34,7 +36,8 @@ func (h *FilesHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
provided = r.URL.Query().Get("token")
|
||||
}
|
||||
if provided != h.token {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -43,16 +46,17 @@ func (h *FilesHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
|
||||
func (h *FilesHandler) handleServe(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
urlPath := r.PathValue("path")
|
||||
if urlPath == "" {
|
||||
http.Error(w, "path is required", http.StatusBadRequest)
|
||||
http.Error(w, i18n.T(locale, i18n.MsgRequired, "path"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent path traversal
|
||||
if strings.Contains(urlPath, "..") {
|
||||
slog.Warn("security.files_traversal", "path", urlPath)
|
||||
http.Error(w, "invalid path", http.StatusBadRequest)
|
||||
http.Error(w, i18n.T(locale, i18n.MsgInvalidPath), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
@@ -53,13 +54,14 @@ func (h *KnowledgeGraphHandler) handleListEntities(w http.ResponseWriter, r *htt
|
||||
}
|
||||
|
||||
func (h *KnowledgeGraphHandler) handleGetEntity(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
agentID := r.PathValue("agentID")
|
||||
entityID := r.PathValue("entityID")
|
||||
userID := r.URL.Query().Get("user_id")
|
||||
|
||||
entity, err := h.store.GetEntity(r.Context(), agentID, userID, entityID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "entity not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "entity", entityID)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -78,17 +80,18 @@ func (h *KnowledgeGraphHandler) handleGetEntity(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
|
||||
func (h *KnowledgeGraphHandler) handleUpsertEntity(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
agentID := r.PathValue("agentID")
|
||||
|
||||
var entity store.Entity
|
||||
if err := json.NewDecoder(r.Body).Decode(&entity); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
entity.AgentID = agentID
|
||||
|
||||
if entity.ExternalID == "" || entity.Name == "" || entity.EntityType == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "external_id, name, and entity_type are required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgEntityFieldsRequired)})
|
||||
return
|
||||
}
|
||||
if entity.Confidence <= 0 {
|
||||
@@ -117,6 +120,7 @@ func (h *KnowledgeGraphHandler) handleDeleteEntity(w http.ResponseWriter, r *htt
|
||||
}
|
||||
|
||||
func (h *KnowledgeGraphHandler) handleTraverse(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
agentID := r.PathValue("agentID")
|
||||
|
||||
var body struct {
|
||||
@@ -125,11 +129,11 @@ func (h *KnowledgeGraphHandler) handleTraverse(w http.ResponseWriter, r *http.Re
|
||||
MaxDepth int `json:"max_depth"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
if body.EntityID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "entity_id is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgEntityIDRequired)})
|
||||
return
|
||||
}
|
||||
if body.MaxDepth <= 0 {
|
||||
@@ -152,6 +156,7 @@ func (h *KnowledgeGraphHandler) handleTraverse(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
func (h *KnowledgeGraphHandler) handleExtract(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
agentID := r.PathValue("agentID")
|
||||
|
||||
var body struct {
|
||||
@@ -162,21 +167,21 @@ func (h *KnowledgeGraphHandler) handleExtract(w http.ResponseWriter, r *http.Req
|
||||
MinConf float64 `json:"min_confidence"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
if body.Text == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "text is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgTextRequired)})
|
||||
return
|
||||
}
|
||||
if body.Provider == "" || body.Model == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "provider and model are required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgProviderModelRequired)})
|
||||
return
|
||||
}
|
||||
|
||||
extractor := h.NewExtractor(body.Provider, body.Model, body.MinConf)
|
||||
if extractor == nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid provider or model"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidProviderOrModel)})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+21
-14
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
@@ -75,15 +76,17 @@ func (h *MCPHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if h.token != "" {
|
||||
if extractBearerToken(r) != h.token {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
userID := extractUserID(r)
|
||||
ctx := store.WithLocale(r.Context(), extractLocale(r))
|
||||
if userID != "" {
|
||||
ctx := store.WithUserID(r.Context(), userID)
|
||||
r = r.WithContext(ctx)
|
||||
ctx = store.WithUserID(ctx, userID)
|
||||
}
|
||||
r = r.WithContext(ctx)
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
@@ -94,25 +97,27 @@ func (h *MCPHandler) handleListServers(w http.ResponseWriter, r *http.Request) {
|
||||
servers, err := h.store.ListServers(r.Context())
|
||||
if err != nil {
|
||||
slog.Error("mcp.list_servers", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list servers"})
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToList, "servers")})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"servers": servers})
|
||||
}
|
||||
|
||||
func (h *MCPHandler) handleCreateServer(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
var srv store.MCPServerData
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&srv); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
if srv.Name == "" || srv.Transport == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name and transport are required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "name and transport")})
|
||||
return
|
||||
}
|
||||
if !isValidSlug(srv.Name) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name must be a valid slug (lowercase letters, numbers, hyphens only)"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidSlug, "name")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -132,15 +137,16 @@ func (h *MCPHandler) handleCreateServer(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (h *MCPHandler) handleGetServer(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid server ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "server")})
|
||||
return
|
||||
}
|
||||
|
||||
srv, err := h.store.GetServer(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "server not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "server", id.String())})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -148,21 +154,22 @@ func (h *MCPHandler) handleGetServer(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *MCPHandler) handleUpdateServer(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid server ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "server")})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&updates); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
if name, ok := updates["name"]; ok {
|
||||
if s, _ := name.(string); !isValidSlug(s) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name must be a valid slug (lowercase letters, numbers, hyphens only)"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidSlug, "name")})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -178,9 +185,10 @@ func (h *MCPHandler) handleUpdateServer(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (h *MCPHandler) handleDeleteServer(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid server ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "server")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -193,4 +201,3 @@ func (h *MCPHandler) handleDeleteServer(w http.ResponseWriter, r *http.Request)
|
||||
h.emitCacheInvalidate()
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
+19
-12
@@ -7,13 +7,15 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
func (h *MCPHandler) handleGrantAgent(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
serverID, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid server ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "server")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -23,13 +25,13 @@ func (h *MCPHandler) handleGrantAgent(w http.ResponseWriter, r *http.Request) {
|
||||
ToolDeny json.RawMessage `json:"tool_deny,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
agentID, err := uuid.Parse(req.AgentID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent_id"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -53,15 +55,16 @@ func (h *MCPHandler) handleGrantAgent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *MCPHandler) handleRevokeAgent(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
serverID, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid server ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "server")})
|
||||
return
|
||||
}
|
||||
|
||||
agentID, err := uuid.Parse(r.PathValue("agentID"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -76,9 +79,10 @@ func (h *MCPHandler) handleRevokeAgent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *MCPHandler) handleListAgentGrants(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
agentID, err := uuid.Parse(r.PathValue("agentID"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -93,16 +97,17 @@ func (h *MCPHandler) handleListAgentGrants(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
func (h *MCPHandler) handleListServerGrants(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
serverID, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid server ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "server")})
|
||||
return
|
||||
}
|
||||
|
||||
grants, err := h.store.ListServerGrants(r.Context(), serverID)
|
||||
if err != nil {
|
||||
slog.Error("mcp.list_server_grants", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list grants"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToList, "grants")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -110,9 +115,10 @@ func (h *MCPHandler) handleListServerGrants(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
func (h *MCPHandler) handleGrantUser(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
serverID, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid server ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "server")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -122,12 +128,12 @@ func (h *MCPHandler) handleGrantUser(w http.ResponseWriter, r *http.Request) {
|
||||
ToolDeny json.RawMessage `json:"tool_deny,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
if req.UserID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "user_id is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "user_id")})
|
||||
return
|
||||
}
|
||||
if err := store.ValidateUserID(req.UserID); err != nil {
|
||||
@@ -155,9 +161,10 @@ func (h *MCPHandler) handleGrantUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *MCPHandler) handleRevokeUser(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
serverID, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid server ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "server")})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,22 +7,24 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
func (h *MCPHandler) handleCreateRequest(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
var req store.MCPAccessRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
if req.ServerID == uuid.Nil || req.Scope == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "server_id and scope are required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "server_id and scope")})
|
||||
return
|
||||
}
|
||||
if req.Scope != "agent" && req.Scope != "user" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "scope must be 'agent' or 'user'"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, "scope must be 'agent' or 'user'")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -50,9 +52,10 @@ func (h *MCPHandler) handleListPendingRequests(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
func (h *MCPHandler) handleReviewRequest(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
requestID, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "request")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -61,7 +64,7 @@ func (h *MCPHandler) handleReviewRequest(w http.ResponseWriter, r *http.Request)
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,14 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
mcpbridge "github.com/nextlevelbuilder/goclaw/internal/mcp"
|
||||
)
|
||||
|
||||
// handleTestConnection tests an MCP server connection without saving it.
|
||||
func (h *MCPHandler) handleTestConnection(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
var req struct {
|
||||
Transport string `json:"transport"`
|
||||
Command string `json:"command"`
|
||||
@@ -21,11 +24,11 @@ func (h *MCPHandler) handleTestConnection(w http.ResponseWriter, r *http.Request
|
||||
Env map[string]string `json:"env"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
if req.Transport == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "transport is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "transport")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,15 +49,16 @@ func (h *MCPHandler) handleTestConnection(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// handleListServerTools lists tools for a specific MCP server.
|
||||
func (h *MCPHandler) handleListServerTools(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid server ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "server")})
|
||||
return
|
||||
}
|
||||
|
||||
srv, err := h.store.GetServer(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "server not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "server", id.String())})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/media"
|
||||
)
|
||||
|
||||
@@ -34,7 +35,8 @@ func (h *MediaServeHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
provided = r.URL.Query().Get("token")
|
||||
}
|
||||
if provided != h.token {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -43,9 +45,10 @@ func (h *MediaServeHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
|
||||
func (h *MediaServeHandler) handleServe(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
id := r.PathValue("id")
|
||||
if id == "" || strings.Contains(id, "..") || strings.Contains(id, "/") {
|
||||
http.Error(w, "invalid media id", http.StatusBadRequest)
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, "invalid media id")})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/channels/media"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -37,7 +38,8 @@ func (h *MediaUploadHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
if h.token != "" {
|
||||
provided := extractBearerToken(r)
|
||||
if provided != h.token {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -46,16 +48,17 @@ func (h *MediaUploadHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
|
||||
func (h *MediaUploadHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
|
||||
|
||||
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "file too large or invalid multipart form"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgFileTooLarge)})
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing 'file' field"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgMissingFileField)})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
@@ -63,7 +66,7 @@ func (h *MediaUploadHandler) handleUpload(w http.ResponseWriter, r *http.Request
|
||||
// Sanitize filename: strip path, prevent traversal.
|
||||
origName := filepath.Base(header.Filename)
|
||||
if origName == "." || origName == "/" || strings.Contains(origName, "..") {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid filename"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidFilename)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -78,14 +81,14 @@ func (h *MediaUploadHandler) handleUpload(w http.ResponseWriter, r *http.Request
|
||||
|
||||
out, err := os.Create(tmpPath)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to create temp file"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgInternalError, "failed to create temp file")})
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if _, err := io.Copy(out, file); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save file"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgInternalError, "failed to save file")})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package http
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
@@ -34,7 +35,8 @@ func (h *MemoryHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if h.token != "" {
|
||||
if extractBearerToken(r) != h.token {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
@@ -44,6 +45,7 @@ func (h *MemoryHandler) handleListDocuments(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
func (h *MemoryHandler) handleGetDocument(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
agentID := r.PathValue("agentID")
|
||||
path := r.PathValue("path")
|
||||
userID := r.URL.Query().Get("user_id")
|
||||
@@ -51,13 +53,14 @@ func (h *MemoryHandler) handleGetDocument(w http.ResponseWriter, r *http.Request
|
||||
detail, err := h.store.GetDocumentDetail(r.Context(), agentID, userID, path)
|
||||
if err != nil {
|
||||
slog.Warn("memory.get_document failed", "error", err, "path", path)
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "document not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "document", path)})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, detail)
|
||||
}
|
||||
|
||||
func (h *MemoryHandler) handlePutDocument(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
agentID := r.PathValue("agentID")
|
||||
path := r.PathValue("path")
|
||||
|
||||
@@ -66,7 +69,7 @@ func (h *MemoryHandler) handlePutDocument(w http.ResponseWriter, r *http.Request
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -92,12 +95,13 @@ func (h *MemoryHandler) handleDeleteDocument(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
func (h *MemoryHandler) handleListChunks(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
agentID := r.PathValue("agentID")
|
||||
path := r.URL.Query().Get("path")
|
||||
userID := r.URL.Query().Get("user_id")
|
||||
|
||||
if path == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "path query parameter required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "path")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -114,6 +118,7 @@ func (h *MemoryHandler) handleListChunks(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (h *MemoryHandler) handleIndexDocument(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
agentID := r.PathValue("agentID")
|
||||
|
||||
var body struct {
|
||||
@@ -121,11 +126,11 @@ func (h *MemoryHandler) handleIndexDocument(w http.ResponseWriter, r *http.Reque
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
if body.Path == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "path is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "path")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -154,6 +159,7 @@ func (h *MemoryHandler) handleIndexAll(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *MemoryHandler) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
agentID := r.PathValue("agentID")
|
||||
|
||||
var body struct {
|
||||
@@ -163,11 +169,11 @@ func (h *MemoryHandler) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
MinScore float64 `json:"min_score"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
if body.Query == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "query is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "query")})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/oauth"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
@@ -47,7 +48,8 @@ func (h *OAuthHandler) RegisterRoutes(mux *http.ServeMux) {
|
||||
func (h *OAuthHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !tokenMatch(extractBearerToken(r), h.token) {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
@@ -80,6 +82,7 @@ func (h *OAuthHandler) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *OAuthHandler) handleStart(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
@@ -102,7 +105,7 @@ func (h *OAuthHandler) handleStart(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
slog.Error("oauth.start", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
||||
"error": "failed to start OAuth flow (is port 1455 available?)",
|
||||
"error": i18n.T(locale, i18n.MsgInternalError, "failed to start OAuth flow (is port 1455 available?)"),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -142,11 +145,12 @@ func (h *OAuthHandler) waitForCallback(pending *oauth.PendingLogin) {
|
||||
}
|
||||
|
||||
func (h *OAuthHandler) handleManualCallback(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
var body struct {
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.RedirectURL == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "redirect_url is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "redirect_url")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -155,7 +159,7 @@ func (h *OAuthHandler) handleManualCallback(w http.ResponseWriter, r *http.Reque
|
||||
h.mu.Unlock()
|
||||
|
||||
if pending == nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "no pending OAuth flow"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgNoPendingOAuth)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -177,7 +181,7 @@ func (h *OAuthHandler) handleManualCallback(w http.ResponseWriter, r *http.Reque
|
||||
providerID, err := h.saveAndRegister(r.Context(), tokenResp)
|
||||
if err != nil {
|
||||
slog.Error("oauth.save_token", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save token"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToSaveToken)})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
@@ -28,10 +29,14 @@ func (h *PendingMessagesHandler) authMiddleware(next http.HandlerFunc) http.Hand
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if h.token != "" {
|
||||
if extractBearerToken(r) != h.token {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
locale := extractLocale(r)
|
||||
ctx := store.WithLocale(r.Context(), locale)
|
||||
r = r.WithContext(ctx)
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
@@ -58,10 +63,11 @@ func (h *PendingMessagesHandler) handleListGroups(w http.ResponseWriter, r *http
|
||||
|
||||
// GET /v1/pending-messages/messages?channel=X&key=Y — list messages for a group
|
||||
func (h *PendingMessagesHandler) handleListMessages(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
channel := r.URL.Query().Get("channel")
|
||||
key := r.URL.Query().Get("key")
|
||||
if channel == "" || key == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "channel and key are required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgChannelKeyReq)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -75,10 +81,11 @@ func (h *PendingMessagesHandler) handleListMessages(w http.ResponseWriter, r *ht
|
||||
|
||||
// DELETE /v1/pending-messages?channel=X&key=Y — clear a group
|
||||
func (h *PendingMessagesHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
channel := r.URL.Query().Get("channel")
|
||||
key := r.URL.Query().Get("key")
|
||||
if channel == "" || key == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "channel and key are required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgChannelKeyReq)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -96,13 +103,14 @@ type compactRequest struct {
|
||||
|
||||
// POST /v1/pending-messages/compact — MVP: clear group and return success
|
||||
func (h *PendingMessagesHandler) handleCompact(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
var req compactRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
if req.ChannelName == "" || req.HistoryKey == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "channel_name and history_key are required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "channel_name and history_key")})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
@@ -25,15 +26,16 @@ type ModelInfo struct {
|
||||
//
|
||||
// GET /v1/providers/{id}/models
|
||||
func (h *ProvidersHandler) handleListProviderModels(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid provider ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "provider")})
|
||||
return
|
||||
}
|
||||
|
||||
p, err := h.store.GetProvider(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "provider not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "provider", id.String())})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -44,7 +46,7 @@ func (h *ProvidersHandler) handleListProviderModels(w http.ResponseWriter, r *ht
|
||||
}
|
||||
|
||||
if p.APIKey == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "provider has no API key configured"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "API key")})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
)
|
||||
|
||||
@@ -17,9 +18,10 @@ import (
|
||||
// Body: {"model": "anthropic/claude-sonnet-4"}
|
||||
// Response: {"valid": true} or {"valid": false, "error": "..."}
|
||||
func (h *ProvidersHandler) handleVerifyProvider(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid provider ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "provider")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -27,18 +29,18 @@ func (h *ProvidersHandler) handleVerifyProvider(w http.ResponseWriter, r *http.R
|
||||
Model string `json:"model"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<16)).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
if req.Model == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "model is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "model")})
|
||||
return
|
||||
}
|
||||
|
||||
// Look up provider record from DB to get the provider name
|
||||
p, err := h.store.GetProvider(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "provider not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "provider", id.String())})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+21
-14
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/oauth"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
@@ -58,7 +59,8 @@ func (h *ProvidersHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if h.token != "" {
|
||||
if extractBearerToken(r) != h.token {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -128,7 +130,8 @@ func (h *ProvidersHandler) handleListProviders(w http.ResponseWriter, r *http.Re
|
||||
providers, err := h.store.ListProviders(r.Context())
|
||||
if err != nil {
|
||||
slog.Error("providers.list", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list providers"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToList, "providers")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -140,22 +143,23 @@ func (h *ProvidersHandler) handleListProviders(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
func (h *ProvidersHandler) handleCreateProvider(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
var p store.LLMProviderData
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&p); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
if p.Name == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "name")})
|
||||
return
|
||||
}
|
||||
if !isValidSlug(p.Name) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name must be a valid slug (lowercase letters, numbers, hyphens only)"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidSlug, "name")})
|
||||
return
|
||||
}
|
||||
if !store.ValidProviderTypes[p.ProviderType] {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "unsupported provider_type"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, "unsupported provider_type")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -169,7 +173,7 @@ func (h *ProvidersHandler) handleCreateProvider(w http.ResponseWriter, r *http.R
|
||||
for _, ep := range existing {
|
||||
if ep.ProviderType == store.ProviderClaudeCLI {
|
||||
writeJSON(w, http.StatusConflict, map[string]string{
|
||||
"error": "A Claude CLI provider already exists. Only one is allowed per instance.",
|
||||
"error": i18n.T(locale, i18n.MsgAlreadyExists, "Claude CLI provider", "only one is allowed per instance"),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -190,15 +194,16 @@ func (h *ProvidersHandler) handleCreateProvider(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
|
||||
func (h *ProvidersHandler) handleGetProvider(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid provider ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "provider")})
|
||||
return
|
||||
}
|
||||
|
||||
p, err := h.store.GetProvider(r.Context(), id)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "provider not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "provider", id.String())})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -207,22 +212,23 @@ func (h *ProvidersHandler) handleGetProvider(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
func (h *ProvidersHandler) handleUpdateProvider(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid provider ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "provider")})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&updates); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate name if being updated
|
||||
if name, ok := updates["name"]; ok {
|
||||
if s, _ := name.(string); !isValidSlug(s) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name must be a valid slug"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidSlug, "name")})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -233,7 +239,7 @@ func (h *ProvidersHandler) handleUpdateProvider(w http.ResponseWriter, r *http.R
|
||||
// silently deleting it would hide the error from the end user.
|
||||
if pt, ok := updates["provider_type"]; ok {
|
||||
if s, _ := pt.(string); !store.ValidProviderTypes[s] {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "unsupported provider_type"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, "unsupported provider_type")})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -282,9 +288,10 @@ func (h *ProvidersHandler) handleUpdateProvider(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
|
||||
func (h *ProvidersHandler) handleDeleteProvider(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid provider ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "provider")})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+13
-8
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
@@ -62,15 +63,17 @@ func (h *SkillsHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if h.token != "" {
|
||||
if extractBearerToken(r) != h.token {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
userID := extractUserID(r)
|
||||
ctx := store.WithLocale(r.Context(), extractLocale(r))
|
||||
if userID != "" {
|
||||
ctx := store.WithUserID(r.Context(), userID)
|
||||
r = r.WithContext(ctx)
|
||||
ctx = store.WithUserID(ctx, userID)
|
||||
}
|
||||
r = r.WithContext(ctx)
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
@@ -81,26 +84,28 @@ func (h *SkillsHandler) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *SkillsHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id := r.PathValue("id")
|
||||
skill, ok := h.skills.GetSkill(id)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "skill not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "skill", id)})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, skill)
|
||||
}
|
||||
|
||||
func (h *SkillsHandler) handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
idStr := r.PathValue("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid skill ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "skill")})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
// Prevent changing sensitive fields
|
||||
@@ -118,10 +123,11 @@ func (h *SkillsHandler) handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *SkillsHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
idStr := r.PathValue("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid skill ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "skill")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -133,4 +139,3 @@ func (h *SkillsHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
h.skills.BumpVersion()
|
||||
writeJSON(w, http.StatusOK, map[string]string{"ok": "true"})
|
||||
}
|
||||
|
||||
@@ -12,21 +12,23 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
func (h *SkillsHandler) handleListAgentSkills(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
agentIDStr := r.PathValue("agentID")
|
||||
agentID, err := uuid.Parse(agentIDStr)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
skills, err := h.skills.ListWithGrantStatus(r.Context(), agentID)
|
||||
if err != nil {
|
||||
slog.Error("failed to list skills with grant status", "agent_id", agentID, "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list skills"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToList, "skills")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -34,11 +36,12 @@ func (h *SkillsHandler) handleListAgentSkills(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
func (h *SkillsHandler) handleGrantAgent(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
userID := store.UserIDFromContext(r.Context())
|
||||
idStr := r.PathValue("id")
|
||||
skillID, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid skill ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "skill")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,13 +50,13 @@ func (h *SkillsHandler) handleGrantAgent(w http.ResponseWriter, r *http.Request)
|
||||
Version int `json:"version"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
agentID, err := uuid.Parse(req.AgentID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent_id"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -72,17 +75,18 @@ func (h *SkillsHandler) handleGrantAgent(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (h *SkillsHandler) handleRevokeAgent(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
idStr := r.PathValue("id")
|
||||
skillID, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid skill ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "skill")})
|
||||
return
|
||||
}
|
||||
|
||||
agentIDStr := r.PathValue("agentID")
|
||||
agentID, err := uuid.Parse(agentIDStr)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid agent ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -97,11 +101,12 @@ func (h *SkillsHandler) handleRevokeAgent(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
func (h *SkillsHandler) handleGrantUser(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
userID := store.UserIDFromContext(r.Context())
|
||||
idStr := r.PathValue("id")
|
||||
skillID, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid skill ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "skill")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -109,11 +114,11 @@ func (h *SkillsHandler) handleGrantUser(w http.ResponseWriter, r *http.Request)
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
if req.UserID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "user_id is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "user_id")})
|
||||
return
|
||||
}
|
||||
if err := store.ValidateUserID(req.UserID); err != nil {
|
||||
@@ -132,10 +137,11 @@ func (h *SkillsHandler) handleGrantUser(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (h *SkillsHandler) handleRevokeUser(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
idStr := r.PathValue("id")
|
||||
skillID, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid skill ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "skill")})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -11,15 +11,17 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
|
||||
)
|
||||
|
||||
// handleUpload processes a ZIP file upload containing a skill (must have SKILL.md at root).
|
||||
func (h *SkillsHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
userID := store.UserIDFromContext(r.Context())
|
||||
if userID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "X-GoClaw-User-Id header required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgUserIDHeader)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -27,7 +29,7 @@ func (h *SkillsHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "file is required: " + err.Error()})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, "file is required: "+err.Error())})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
@@ -35,7 +37,7 @@ func (h *SkillsHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
// Save to temp file for zip processing
|
||||
tmp, err := os.CreateTemp("", "skill-upload-*.zip")
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to create temp file"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgInternalError, "failed to create temp file")})
|
||||
return
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
@@ -44,7 +46,7 @@ func (h *SkillsHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
hasher := sha256.New()
|
||||
size, err := io.Copy(io.MultiWriter(tmp, hasher), file)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save upload"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgInternalError, "failed to save upload")})
|
||||
return
|
||||
}
|
||||
fileHash := fmt.Sprintf("%x", hasher.Sum(nil))
|
||||
@@ -52,7 +54,7 @@ func (h *SkillsHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
// Open as zip
|
||||
zr, err := zip.OpenReader(tmp.Name())
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid ZIP file"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, "invalid ZIP file")})
|
||||
return
|
||||
}
|
||||
defer zr.Close()
|
||||
@@ -77,27 +79,27 @@ func (h *SkillsHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if skillMD == nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ZIP must contain SKILL.md at root (or inside a single top-level directory)"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, "ZIP must contain SKILL.md at root (or inside a single top-level directory)")})
|
||||
return
|
||||
}
|
||||
|
||||
// Read and parse SKILL.md frontmatter
|
||||
skillContent, err := readZipFile(skillMD)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to read SKILL.md"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, "failed to read SKILL.md")})
|
||||
return
|
||||
}
|
||||
|
||||
name, description, slug, frontmatter := parseSkillFrontmatter(skillContent)
|
||||
if name == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "SKILL.md must have a name in frontmatter"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "name in SKILL.md frontmatter")})
|
||||
return
|
||||
}
|
||||
if slug == "" {
|
||||
slug = slugify(name)
|
||||
}
|
||||
if !slugRegexp.MatchString(slug) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "slug must be a valid slug (lowercase letters, numbers, hyphens only)"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidSlug, "slug")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -107,7 +109,7 @@ func (h *SkillsHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract to filesystem: baseDir/slug/version/
|
||||
destDir := filepath.Join(h.baseDir, slug, fmt.Sprintf("%d", version))
|
||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to create skill directory"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgInternalError, "failed to create skill directory")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -167,7 +169,7 @@ func (h *SkillsHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
id, err := h.skills.CreateSkillManaged(r.Context(), skill)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save skill: " + err.Error()})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToCreate, "skill", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -10,19 +10,23 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
// handleListVersions returns all available version numbers for a skill.
|
||||
func (h *SkillsHandler) handleListVersions(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid skill ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "skill")})
|
||||
return
|
||||
}
|
||||
|
||||
_, slug, currentVersion, ok := h.skills.GetSkillFilePath(id)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "skill not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "skill", id.String())})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -60,15 +64,16 @@ func (h *SkillsHandler) handleListVersions(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
// handleListFiles returns all files in a skill version directory.
|
||||
func (h *SkillsHandler) handleListFiles(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid skill ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "skill")})
|
||||
return
|
||||
}
|
||||
|
||||
_, slug, currentVersion, ok := h.skills.GetSkillFilePath(id)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "skill not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "skill", id.String())})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -76,7 +81,7 @@ func (h *SkillsHandler) handleListFiles(w http.ResponseWriter, r *http.Request)
|
||||
if v := r.URL.Query().Get("version"); v != "" {
|
||||
parsed, err := strconv.Atoi(v)
|
||||
if err != nil || parsed < 1 {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid version"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidVersion)})
|
||||
return
|
||||
}
|
||||
version = parsed
|
||||
@@ -84,7 +89,7 @@ func (h *SkillsHandler) handleListFiles(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
versionDir := filepath.Join(h.baseDir, slug, strconv.Itoa(version))
|
||||
if _, err := os.Stat(versionDir); err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "version not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgVersionNotFound)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -137,26 +142,27 @@ func (h *SkillsHandler) handleListFiles(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// handleReadFile reads a single file from a skill version directory.
|
||||
func (h *SkillsHandler) handleReadFile(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
id, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid skill ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "skill")})
|
||||
return
|
||||
}
|
||||
|
||||
relPath := r.PathValue("path")
|
||||
if relPath == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "path is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "path")})
|
||||
return
|
||||
}
|
||||
if strings.Contains(relPath, "..") {
|
||||
slog.Warn("security.skill_files_traversal", "path", relPath)
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidPath)})
|
||||
return
|
||||
}
|
||||
|
||||
_, slug, currentVersion, ok := h.skills.GetSkillFilePath(id)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "skill not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "skill", id.String())})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -164,7 +170,7 @@ func (h *SkillsHandler) handleReadFile(w http.ResponseWriter, r *http.Request) {
|
||||
if v := r.URL.Query().Get("version"); v != "" {
|
||||
parsed, err := strconv.Atoi(v)
|
||||
if err != nil || parsed < 1 {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid version"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidVersion)})
|
||||
return
|
||||
}
|
||||
version = parsed
|
||||
@@ -176,31 +182,31 @@ func (h *SkillsHandler) handleReadFile(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify resolved path is within the version directory
|
||||
if !strings.HasPrefix(absPath, versionDir+string(filepath.Separator)) {
|
||||
slog.Warn("security.skill_files_escape", "resolved", absPath, "root", versionDir)
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidPath)})
|
||||
return
|
||||
}
|
||||
|
||||
// Use Lstat to detect symlinks — reject them to prevent directory escape
|
||||
info, err := os.Lstat(absPath)
|
||||
if err != nil || info.IsDir() {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "file not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgFileNotFound)})
|
||||
return
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
slog.Warn("security.skill_files_symlink", "path", absPath)
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidPath)})
|
||||
return
|
||||
}
|
||||
|
||||
// Skip system artifacts
|
||||
if isSystemArtifact(relPath) {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "file not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgFileNotFound)})
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to read file"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToReadFile)})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+23
-17
@@ -6,6 +6,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
)
|
||||
|
||||
// StorageHandler provides HTTP endpoints for browsing and managing
|
||||
@@ -33,7 +35,8 @@ func (h *StorageHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
if h.token != "" {
|
||||
provided := extractBearerToken(r)
|
||||
if !tokenMatch(provided, h.token) {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -65,10 +68,11 @@ func isProtectedPath(rel string) bool {
|
||||
// handleList lists all files and directories under ~/.goclaw/.
|
||||
// Optional query param ?path= scopes the listing to a subtree.
|
||||
func (h *StorageHandler) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
subPath := r.URL.Query().Get("path")
|
||||
if strings.Contains(subPath, "..") {
|
||||
slog.Warn("security.storage_traversal", "path", subPath)
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidPath)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -77,7 +81,7 @@ func (h *StorageHandler) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
rootDir = filepath.Join(h.baseDir, filepath.Clean(subPath))
|
||||
if !strings.HasPrefix(rootDir, h.baseDir) {
|
||||
slog.Warn("security.storage_escape", "resolved", rootDir, "root", h.baseDir)
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidPath)})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -87,8 +91,8 @@ func (h *StorageHandler) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
Name string `json:"name"`
|
||||
IsDir bool `json:"isDir"`
|
||||
Size int64 `json:"size"`
|
||||
TotalSize int64 `json:"totalSize"` // recursive size for directories
|
||||
Protected bool `json:"protected"` // true if deletion is blocked
|
||||
TotalSize int64 `json:"totalSize"` // recursive size for directories
|
||||
Protected bool `json:"protected"` // true if deletion is blocked
|
||||
}
|
||||
|
||||
// Compute directory sizes via a two-pass approach:
|
||||
@@ -180,38 +184,39 @@ func (h *StorageHandler) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// handleRead reads a single file's content by relative path.
|
||||
func (h *StorageHandler) handleRead(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
relPath := r.PathValue("path")
|
||||
if relPath == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "path is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "path")})
|
||||
return
|
||||
}
|
||||
if strings.Contains(relPath, "..") {
|
||||
slog.Warn("security.storage_traversal", "path", relPath)
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidPath)})
|
||||
return
|
||||
}
|
||||
|
||||
absPath := filepath.Join(h.baseDir, filepath.Clean(relPath))
|
||||
if !strings.HasPrefix(absPath, h.baseDir+string(filepath.Separator)) {
|
||||
slog.Warn("security.storage_escape", "resolved", absPath, "root", h.baseDir)
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidPath)})
|
||||
return
|
||||
}
|
||||
|
||||
info, err := os.Lstat(absPath)
|
||||
if err != nil || info.IsDir() {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "file not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgFileNotFound)})
|
||||
return
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
slog.Warn("security.storage_symlink", "path", absPath)
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidPath)})
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to read file"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToReadFile)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -225,33 +230,34 @@ func (h *StorageHandler) handleRead(w http.ResponseWriter, r *http.Request) {
|
||||
// handleDelete removes a file or directory (recursively).
|
||||
// Rejects deletion of the root dir and any path inside excluded directories.
|
||||
func (h *StorageHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
relPath := r.PathValue("path")
|
||||
if relPath == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "path is required"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "path")})
|
||||
return
|
||||
}
|
||||
if strings.Contains(relPath, "..") {
|
||||
slog.Warn("security.storage_traversal", "path", relPath)
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidPath)})
|
||||
return
|
||||
}
|
||||
|
||||
if isProtectedPath(relPath) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "cannot delete skills directories"})
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgCannotDeleteSkillsDir)})
|
||||
return
|
||||
}
|
||||
|
||||
absPath := filepath.Join(h.baseDir, filepath.Clean(relPath))
|
||||
if !strings.HasPrefix(absPath, h.baseDir+string(filepath.Separator)) {
|
||||
slog.Warn("security.storage_escape", "resolved", absPath, "root", h.baseDir)
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidPath)})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify path exists
|
||||
info, err := os.Lstat(absPath)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "path", relPath)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -266,7 +272,7 @@ func (h *StorageHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err != nil {
|
||||
slog.Error("storage.delete_failed", "path", absPath, "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to delete"})
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToDeleteFile)})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
||||
)
|
||||
@@ -39,26 +39,28 @@ type toolsInvokeRequest struct {
|
||||
}
|
||||
|
||||
func (h *ToolsInvokeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
locale := extractLocale(r)
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": i18n.T(locale, i18n.MsgMethodNotAllowed)})
|
||||
return
|
||||
}
|
||||
|
||||
if h.token != "" {
|
||||
if extractBearerToken(r) != h.token {
|
||||
http.Error(w, `{"error":{"code":"UNAUTHORIZED","message":"Invalid token"}}`, http.StatusUnauthorized)
|
||||
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 {
|
||||
http.Error(w, fmt.Sprintf(`{"error":{"code":"BAD_REQUEST","message":"%s"}}`, err), http.StatusBadRequest)
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Tool == "" {
|
||||
http.Error(w, `{"error":{"code":"BAD_REQUEST","message":"tool is required"}}`, http.StatusBadRequest)
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "tool")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -68,7 +70,7 @@ func (h *ToolsInvokeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Just check if tool exists and return its schema
|
||||
tool, ok := h.registry.Get(req.Tool)
|
||||
if !ok {
|
||||
writeToolError(w, http.StatusNotFound, "NOT_FOUND", fmt.Sprintf("Tool '%s' not found", req.Tool))
|
||||
writeToolError(w, http.StatusNotFound, "NOT_FOUND", i18n.T(locale, i18n.MsgNotFound, "tool", req.Tool))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
@@ -30,10 +31,14 @@ func (h *TracesHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if h.token != "" {
|
||||
if extractBearerToken(r) != h.token {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
locale := extractLocale(r)
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
|
||||
return
|
||||
}
|
||||
}
|
||||
locale := extractLocale(r)
|
||||
ctx := store.WithLocale(r.Context(), locale)
|
||||
r = r.WithContext(ctx)
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
@@ -87,16 +92,17 @@ func (h *TracesHandler) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *TracesHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
locale := store.LocaleFromContext(r.Context())
|
||||
traceIDStr := r.PathValue("traceID")
|
||||
traceID, err := uuid.Parse(traceIDStr)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid trace ID"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "trace")})
|
||||
return
|
||||
}
|
||||
|
||||
trace, err := h.tracing.GetTrace(r.Context(), traceID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "trace not found"})
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "trace", traceIDStr)})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package i18n
|
||||
|
||||
func init() {
|
||||
register(LocaleEN, map[string]string{
|
||||
// Common validation
|
||||
MsgRequired: "%s is required",
|
||||
MsgInvalidID: "invalid %s ID",
|
||||
MsgNotFound: "%s not found: %s",
|
||||
MsgAlreadyExists: "%s already exists: %s",
|
||||
MsgInvalidRequest: "invalid request: %s",
|
||||
MsgInvalidJSON: "invalid JSON",
|
||||
MsgUnauthorized: "unauthorized",
|
||||
MsgPermissionDenied: "permission denied: insufficient role for %s",
|
||||
MsgInternalError: "internal error: %s",
|
||||
MsgInvalidSlug: "%s must be a valid slug (lowercase letters, numbers, hyphens only)",
|
||||
MsgFailedToList: "failed to list %s",
|
||||
MsgFailedToCreate: "failed to create %s: %s",
|
||||
MsgFailedToUpdate: "failed to update %s: %s",
|
||||
MsgFailedToDelete: "failed to delete %s: %s",
|
||||
MsgFailedToSave: "failed to save %s: %s",
|
||||
MsgInvalidUpdates: "invalid updates",
|
||||
|
||||
// Agent
|
||||
MsgAgentNotFound: "agent not found: %s",
|
||||
MsgCannotDeleteDefault: "cannot delete the default agent",
|
||||
MsgUserCtxRequired: "user context required",
|
||||
|
||||
// Chat
|
||||
MsgRateLimitExceeded: "rate limit exceeded — please wait",
|
||||
MsgNoUserMessage: "no user message found",
|
||||
MsgUserIDRequired: "user_id is required",
|
||||
MsgMsgRequired: "message is required",
|
||||
|
||||
// Channel instances
|
||||
MsgInvalidChannelType: "invalid channel_type",
|
||||
MsgInstanceNotFound: "instance not found",
|
||||
|
||||
// Cron
|
||||
MsgJobNotFound: "job not found",
|
||||
MsgInvalidCronExpr: "invalid cron expression: %s",
|
||||
|
||||
// Config
|
||||
MsgConfigHashMismatch: "config has changed (hash mismatch)",
|
||||
|
||||
// Exec approval
|
||||
MsgExecApprovalDisabled: "exec approval is not enabled",
|
||||
|
||||
// Pairing
|
||||
MsgSenderChannelRequired: "senderId and channel are required",
|
||||
MsgCodeRequired: "code is required",
|
||||
MsgSenderIDRequired: "sender_id is required",
|
||||
|
||||
// HTTP API
|
||||
MsgInvalidAuth: "invalid authentication",
|
||||
MsgMsgsRequired: "messages is required",
|
||||
MsgUserIDHeader: "X-GoClaw-User-Id header is required in managed mode",
|
||||
MsgFileTooLarge: "file too large or invalid multipart form",
|
||||
MsgMissingFileField: "missing 'file' field",
|
||||
MsgInvalidFilename: "invalid filename",
|
||||
MsgChannelKeyReq: "channel and key are required",
|
||||
MsgMethodNotAllowed: "method not allowed",
|
||||
MsgStreamingNotSupported: "streaming not supported",
|
||||
MsgOwnerOnly: "only owner can %s",
|
||||
MsgNoAccess: "no access to this %s",
|
||||
MsgAlreadySummoning: "agent is already being summoned",
|
||||
MsgSummoningUnavailable: "summoning not available",
|
||||
MsgNoDescription: "agent has no description to resummon from",
|
||||
MsgInvalidPath: "invalid path",
|
||||
|
||||
// Scheduler
|
||||
MsgQueueFull: "session queue is full",
|
||||
MsgShuttingDown: "gateway is shutting down, please retry shortly",
|
||||
|
||||
// Provider
|
||||
MsgProviderReqFailed: "%s: request failed: %s",
|
||||
|
||||
// Unknown method
|
||||
MsgUnknownMethod: "unknown method: %s",
|
||||
|
||||
// Not implemented
|
||||
MsgNotImplemented: "%s not yet implemented",
|
||||
|
||||
// Agent links
|
||||
MsgLinksNotConfigured: "agent links not configured",
|
||||
MsgInvalidDirection: "direction must be outbound, inbound, or bidirectional",
|
||||
MsgSourceTargetSame: "source and target must be different agents",
|
||||
MsgCannotDelegateOpen: "cannot delegate to open agents — only predefined agents can be delegation targets",
|
||||
MsgNoUpdatesProvided: "no updates provided",
|
||||
MsgInvalidLinkStatus: "status must be active or disabled",
|
||||
|
||||
// Teams
|
||||
MsgTeamsNotConfigured: "teams not configured",
|
||||
MsgAgentIsTeamLead: "agent is already the team lead",
|
||||
MsgCannotRemoveTeamLead: "cannot remove the team lead",
|
||||
|
||||
// Delegations
|
||||
MsgDelegationsUnavailable: "delegations not available",
|
||||
|
||||
// Channels
|
||||
MsgCannotDeleteDefaultInst: "cannot delete default channel instance",
|
||||
|
||||
// Skills
|
||||
MsgSkillsUpdateNotSupported: "skills.update not supported for file-based skills",
|
||||
MsgCannotResolveSkillID: "cannot resolve skill ID for file-based skill",
|
||||
|
||||
// Logs
|
||||
MsgInvalidLogAction: "action must be 'start' or 'stop'",
|
||||
|
||||
// Config
|
||||
MsgRawConfigRequired: "raw config is required",
|
||||
MsgRawPatchRequired: "raw patch is required",
|
||||
|
||||
// Storage / File
|
||||
MsgCannotDeleteSkillsDir: "cannot delete skills directories",
|
||||
MsgFailedToReadFile: "failed to read file",
|
||||
MsgFileNotFound: "file not found",
|
||||
MsgInvalidVersion: "invalid version",
|
||||
MsgVersionNotFound: "version not found",
|
||||
MsgFailedToDeleteFile: "failed to delete",
|
||||
|
||||
// OAuth
|
||||
MsgNoPendingOAuth: "no pending OAuth flow",
|
||||
MsgFailedToSaveToken: "failed to save token",
|
||||
|
||||
// Knowledge Graph
|
||||
MsgEntityIDRequired: "entity_id is required",
|
||||
MsgEntityFieldsRequired: "external_id, name, and entity_type are required",
|
||||
MsgTextRequired: "text is required",
|
||||
MsgProviderModelRequired: "provider and model are required",
|
||||
MsgInvalidProviderOrModel: "invalid provider or model",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package i18n
|
||||
|
||||
func init() {
|
||||
register(LocaleVI, map[string]string{
|
||||
// Common validation
|
||||
MsgRequired: "%s là bắt buộc",
|
||||
MsgInvalidID: "ID %s không hợp lệ",
|
||||
MsgNotFound: "không tìm thấy %s: %s",
|
||||
MsgAlreadyExists: "%s đã tồn tại: %s",
|
||||
MsgInvalidRequest: "yêu cầu không hợp lệ: %s",
|
||||
MsgInvalidJSON: "JSON không hợp lệ",
|
||||
MsgUnauthorized: "chưa xác thực",
|
||||
MsgPermissionDenied: "từ chối quyền truy cập: không đủ quyền cho %s",
|
||||
MsgInternalError: "lỗi nội bộ: %s",
|
||||
MsgInvalidSlug: "%s phải là slug hợp lệ (chữ thường, số, dấu gạch ngang)",
|
||||
MsgFailedToList: "không thể liệt kê %s",
|
||||
MsgFailedToCreate: "không thể tạo %s: %s",
|
||||
MsgFailedToUpdate: "không thể cập nhật %s: %s",
|
||||
MsgFailedToDelete: "không thể xóa %s: %s",
|
||||
MsgFailedToSave: "không thể lưu %s: %s",
|
||||
MsgInvalidUpdates: "cập nhật không hợp lệ",
|
||||
|
||||
// Agent
|
||||
MsgAgentNotFound: "không tìm thấy agent: %s",
|
||||
MsgCannotDeleteDefault: "không thể xóa agent mặc định",
|
||||
MsgUserCtxRequired: "yêu cầu ngữ cảnh người dùng",
|
||||
|
||||
// Chat
|
||||
MsgRateLimitExceeded: "vượt quá giới hạn tốc độ — vui lòng đợi",
|
||||
MsgNoUserMessage: "không tìm thấy tin nhắn người dùng",
|
||||
MsgUserIDRequired: "user_id là bắt buộc",
|
||||
MsgMsgRequired: "tin nhắn là bắt buộc",
|
||||
|
||||
// Channel instances
|
||||
MsgInvalidChannelType: "loại channel không hợp lệ",
|
||||
MsgInstanceNotFound: "không tìm thấy phiên bản",
|
||||
|
||||
// Cron
|
||||
MsgJobNotFound: "không tìm thấy tác vụ",
|
||||
MsgInvalidCronExpr: "biểu thức cron không hợp lệ: %s",
|
||||
|
||||
// Config
|
||||
MsgConfigHashMismatch: "cấu hình đã thay đổi (hash không khớp)",
|
||||
|
||||
// Exec approval
|
||||
MsgExecApprovalDisabled: "phê duyệt thực thi chưa được bật",
|
||||
|
||||
// Pairing
|
||||
MsgSenderChannelRequired: "senderId và channel là bắt buộc",
|
||||
MsgCodeRequired: "mã là bắt buộc",
|
||||
MsgSenderIDRequired: "sender_id là bắt buộc",
|
||||
|
||||
// HTTP API
|
||||
MsgInvalidAuth: "xác thực không hợp lệ",
|
||||
MsgMsgsRequired: "messages là bắt buộc",
|
||||
MsgUserIDHeader: "header X-GoClaw-User-Id là bắt buộc ở chế độ managed",
|
||||
MsgFileTooLarge: "tệp quá lớn hoặc form multipart không hợp lệ",
|
||||
MsgMissingFileField: "thiếu trường 'file'",
|
||||
MsgInvalidFilename: "tên tệp không hợp lệ",
|
||||
MsgChannelKeyReq: "channel và key là bắt buộc",
|
||||
MsgMethodNotAllowed: "phương thức không được phép",
|
||||
MsgStreamingNotSupported: "streaming không được hỗ trợ",
|
||||
MsgOwnerOnly: "chỉ chủ sở hữu mới có thể %s",
|
||||
MsgNoAccess: "không có quyền truy cập %s này",
|
||||
MsgAlreadySummoning: "agent đang được triệu hồi",
|
||||
MsgSummoningUnavailable: "triệu hồi không khả dụng",
|
||||
MsgNoDescription: "agent không có mô tả để triệu hồi lại",
|
||||
MsgInvalidPath: "đường dẫn không hợp lệ",
|
||||
|
||||
// Scheduler
|
||||
MsgQueueFull: "hàng đợi session đã đầy",
|
||||
MsgShuttingDown: "cổng đang tắt, vui lòng thử lại sau",
|
||||
|
||||
// Provider
|
||||
MsgProviderReqFailed: "%s: yêu cầu thất bại: %s",
|
||||
|
||||
// Unknown method
|
||||
MsgUnknownMethod: "phương thức không xác định: %s",
|
||||
|
||||
// Not implemented
|
||||
MsgNotImplemented: "%s chưa được triển khai",
|
||||
|
||||
// Agent links
|
||||
MsgLinksNotConfigured: "liên kết agent chưa được cấu hình",
|
||||
MsgInvalidDirection: "hướng phải là outbound, inbound hoặc bidirectional",
|
||||
MsgSourceTargetSame: "nguồn và đích phải là các agent khác nhau",
|
||||
MsgCannotDelegateOpen: "không thể ủy quyền cho agent mở — chỉ agent định sẵn mới có thể là đích ủy quyền",
|
||||
MsgNoUpdatesProvided: "không có cập nhật nào được cung cấp",
|
||||
MsgInvalidLinkStatus: "trạng thái phải là active hoặc disabled",
|
||||
|
||||
// Teams
|
||||
MsgTeamsNotConfigured: "nhóm chưa được cấu hình",
|
||||
MsgAgentIsTeamLead: "agent đã là trưởng nhóm",
|
||||
MsgCannotRemoveTeamLead: "không thể xóa trưởng nhóm",
|
||||
|
||||
// Delegations
|
||||
MsgDelegationsUnavailable: "ủy quyền không khả dụng",
|
||||
|
||||
// Channels
|
||||
MsgCannotDeleteDefaultInst: "không thể xóa phiên bản channel mặc định",
|
||||
|
||||
// Skills
|
||||
MsgSkillsUpdateNotSupported: "skills.update không được hỗ trợ với skill dựa trên tệp",
|
||||
MsgCannotResolveSkillID: "không thể xác định ID skill dựa trên tệp",
|
||||
|
||||
// Logs
|
||||
MsgInvalidLogAction: "action phải là 'start' hoặc 'stop'",
|
||||
|
||||
// Config
|
||||
MsgRawConfigRequired: "cấu hình raw là bắt buộc",
|
||||
MsgRawPatchRequired: "patch raw là bắt buộc",
|
||||
|
||||
// Storage / File
|
||||
MsgCannotDeleteSkillsDir: "không thể xóa thư mục skill",
|
||||
MsgFailedToReadFile: "không thể đọc tệp",
|
||||
MsgFileNotFound: "không tìm thấy tệp",
|
||||
MsgInvalidVersion: "phiên bản không hợp lệ",
|
||||
MsgVersionNotFound: "không tìm thấy phiên bản",
|
||||
MsgFailedToDeleteFile: "không thể xóa",
|
||||
|
||||
// OAuth
|
||||
MsgNoPendingOAuth: "không có luồng OAuth đang chờ",
|
||||
MsgFailedToSaveToken: "không thể lưu token",
|
||||
|
||||
// Knowledge Graph
|
||||
MsgEntityIDRequired: "entity_id là bắt buộc",
|
||||
MsgEntityFieldsRequired: "external_id, name và entity_type là bắt buộc",
|
||||
MsgTextRequired: "text là bắt buộc",
|
||||
MsgProviderModelRequired: "provider và model là bắt buộc",
|
||||
MsgInvalidProviderOrModel: "provider hoặc model không hợp lệ",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package i18n
|
||||
|
||||
func init() {
|
||||
register(LocaleZH, map[string]string{
|
||||
// Common validation
|
||||
MsgRequired: "%s 是必填项",
|
||||
MsgInvalidID: "无效的 %s ID",
|
||||
MsgNotFound: "未找到 %s:%s",
|
||||
MsgAlreadyExists: "%s 已存在:%s",
|
||||
MsgInvalidRequest: "无效请求:%s",
|
||||
MsgInvalidJSON: "无效的 JSON",
|
||||
MsgUnauthorized: "未授权",
|
||||
MsgPermissionDenied: "权限不足:无法访问 %s",
|
||||
MsgInternalError: "内部错误:%s",
|
||||
MsgInvalidSlug: "%s 必须是有效的 slug(小写字母、数字、连字符)",
|
||||
MsgFailedToList: "获取 %s 列表失败",
|
||||
MsgFailedToCreate: "创建 %s 失败:%s",
|
||||
MsgFailedToUpdate: "更新 %s 失败:%s",
|
||||
MsgFailedToDelete: "删除 %s 失败:%s",
|
||||
MsgFailedToSave: "保存 %s 失败:%s",
|
||||
MsgInvalidUpdates: "更新内容无效",
|
||||
|
||||
// Agent
|
||||
MsgAgentNotFound: "未找到Agent:%s",
|
||||
MsgCannotDeleteDefault: "无法删除默认Agent",
|
||||
MsgUserCtxRequired: "需要用户上下文",
|
||||
|
||||
// Chat
|
||||
MsgRateLimitExceeded: "请求频率超限 — 请稍候",
|
||||
MsgNoUserMessage: "未找到用户消息",
|
||||
MsgUserIDRequired: "user_id 是必填项",
|
||||
MsgMsgRequired: "消息是必填项",
|
||||
|
||||
// Channel instances
|
||||
MsgInvalidChannelType: "Channel类型无效",
|
||||
MsgInstanceNotFound: "未找到实例",
|
||||
|
||||
// Cron
|
||||
MsgJobNotFound: "未找到任务",
|
||||
MsgInvalidCronExpr: "无效的 cron 表达式:%s",
|
||||
|
||||
// Config
|
||||
MsgConfigHashMismatch: "配置已更改(hash 不匹配)",
|
||||
|
||||
// Exec approval
|
||||
MsgExecApprovalDisabled: "执行审批未启用",
|
||||
|
||||
// Pairing
|
||||
MsgSenderChannelRequired: "senderId 和 channel 是必填项",
|
||||
MsgCodeRequired: "代码是必填项",
|
||||
MsgSenderIDRequired: "sender_id 是必填项",
|
||||
|
||||
// HTTP API
|
||||
MsgInvalidAuth: "身份验证无效",
|
||||
MsgMsgsRequired: "messages 是必填项",
|
||||
MsgUserIDHeader: "托管模式下需要 X-GoClaw-User-Id 请求头",
|
||||
MsgFileTooLarge: "文件过大或 multipart 表单无效",
|
||||
MsgMissingFileField: "缺少 'file' 字段",
|
||||
MsgInvalidFilename: "文件名无效",
|
||||
MsgChannelKeyReq: "channel 和 key 是必填项",
|
||||
MsgMethodNotAllowed: "不允许的请求方法",
|
||||
MsgStreamingNotSupported: "不支持流式传输",
|
||||
MsgOwnerOnly: "只有所有者才能%s",
|
||||
MsgNoAccess: "无权访问此%s",
|
||||
MsgAlreadySummoning: "Agent正在被召唤中",
|
||||
MsgSummoningUnavailable: "召唤功能不可用",
|
||||
MsgNoDescription: "Agent没有可供重新召唤的描述",
|
||||
MsgInvalidPath: "路径无效",
|
||||
|
||||
// Scheduler
|
||||
MsgQueueFull: "Session队列已满",
|
||||
MsgShuttingDown: "网关正在关闭,请稍后重试",
|
||||
|
||||
// Provider
|
||||
MsgProviderReqFailed: "%s:请求失败:%s",
|
||||
|
||||
// Unknown method
|
||||
MsgUnknownMethod: "未知方法:%s",
|
||||
|
||||
// Not implemented
|
||||
MsgNotImplemented: "%s 尚未实现",
|
||||
|
||||
// Agent links
|
||||
MsgLinksNotConfigured: "Agent链接未配置",
|
||||
MsgInvalidDirection: "方向必须是 outbound、inbound 或 bidirectional",
|
||||
MsgSourceTargetSame: "源和目标必须是不同的Agent",
|
||||
MsgCannotDelegateOpen: "无法委派给开放型Agent — 只有预定义Agent才能作为委派目标",
|
||||
MsgNoUpdatesProvided: "未提供更新内容",
|
||||
MsgInvalidLinkStatus: "状态必须是 active 或 disabled",
|
||||
|
||||
// Teams
|
||||
MsgTeamsNotConfigured: "团队未配置",
|
||||
MsgAgentIsTeamLead: "该Agent已是团队负责人",
|
||||
MsgCannotRemoveTeamLead: "无法移除团队负责人",
|
||||
|
||||
// Delegations
|
||||
MsgDelegationsUnavailable: "委派功能不可用",
|
||||
|
||||
// Channels
|
||||
MsgCannotDeleteDefaultInst: "无法删除默认Channel实例",
|
||||
|
||||
// Skills
|
||||
MsgSkillsUpdateNotSupported: "基于文件的Skill不支持 skills.update",
|
||||
MsgCannotResolveSkillID: "无法解析基于文件的Skill ID",
|
||||
|
||||
// Logs
|
||||
MsgInvalidLogAction: "action 必须是 'start' 或 'stop'",
|
||||
|
||||
// Config
|
||||
MsgRawConfigRequired: "raw 配置是必填项",
|
||||
MsgRawPatchRequired: "raw 补丁是必填项",
|
||||
|
||||
// Storage / File
|
||||
MsgCannotDeleteSkillsDir: "无法删除Skill目录",
|
||||
MsgFailedToReadFile: "读取文件失败",
|
||||
MsgFileNotFound: "文件未找到",
|
||||
MsgInvalidVersion: "版本无效",
|
||||
MsgVersionNotFound: "未找到该版本",
|
||||
MsgFailedToDeleteFile: "删除失败",
|
||||
|
||||
// OAuth
|
||||
MsgNoPendingOAuth: "没有待处理的 OAuth 流程",
|
||||
MsgFailedToSaveToken: "保存令牌失败",
|
||||
|
||||
// Knowledge Graph
|
||||
MsgEntityIDRequired: "entity_id 是必填项",
|
||||
MsgEntityFieldsRequired: "external_id、name 和 entity_type 是必填项",
|
||||
MsgTextRequired: "text 是必填项",
|
||||
MsgProviderModelRequired: "provider 和 model 是必填项",
|
||||
MsgInvalidProviderOrModel: "provider 或 model 无效",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Package i18n provides a lightweight message catalog for localizing
|
||||
// user-facing error messages. Messages use fmt.Sprintf-style templates.
|
||||
package i18n
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Supported locales.
|
||||
const (
|
||||
LocaleEN = "en"
|
||||
LocaleVI = "vi"
|
||||
LocaleZH = "zh"
|
||||
|
||||
DefaultLocale = LocaleEN
|
||||
)
|
||||
|
||||
// catalogs maps locale → key → message template.
|
||||
var catalogs = map[string]map[string]string{}
|
||||
|
||||
// register adds a set of messages for a locale. Called from catalog_*.go init().
|
||||
func register(locale string, msgs map[string]string) {
|
||||
if catalogs[locale] == nil {
|
||||
catalogs[locale] = make(map[string]string, len(msgs))
|
||||
}
|
||||
for k, v := range msgs {
|
||||
catalogs[locale][k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// T returns a localized message for the given key.
|
||||
// If the key is not found in the requested locale, it falls back to English.
|
||||
// If the key is not found in English either, the key itself is returned.
|
||||
// Optional args are applied via fmt.Sprintf if the template contains verbs.
|
||||
func T(locale, key string, args ...any) string {
|
||||
msg := lookup(locale, key)
|
||||
if len(args) > 0 {
|
||||
return fmt.Sprintf(msg, args...)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func lookup(locale, key string) string {
|
||||
if cat, ok := catalogs[locale]; ok {
|
||||
if msg, ok := cat[key]; ok {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
// Fallback to English
|
||||
if locale != LocaleEN {
|
||||
if cat, ok := catalogs[LocaleEN]; ok {
|
||||
if msg, ok := cat[key]; ok {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// IsSupported returns true if the locale is a known language.
|
||||
func IsSupported(locale string) bool {
|
||||
switch locale {
|
||||
case LocaleEN, LocaleVI, LocaleZH:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Normalize returns a supported locale or the default.
|
||||
func Normalize(locale string) string {
|
||||
if IsSupported(locale) {
|
||||
return locale
|
||||
}
|
||||
// Handle common prefixes: "en-US" → "en", "vi-VN" → "vi", "zh-CN" → "zh"
|
||||
if len(locale) >= 2 {
|
||||
prefix := locale[:2]
|
||||
if IsSupported(prefix) {
|
||||
return prefix
|
||||
}
|
||||
}
|
||||
return DefaultLocale
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package i18n
|
||||
|
||||
// Message keys for gateway/HTTP error messages.
|
||||
// Grouped by domain for easier maintenance.
|
||||
const (
|
||||
// --- Common validation ---
|
||||
MsgRequired = "error.required" // "%s is required"
|
||||
MsgInvalidID = "error.invalid_id" // "invalid %s ID"
|
||||
MsgNotFound = "error.not_found" // "%s not found: %s"
|
||||
MsgAlreadyExists = "error.already_exists" // "%s already exists: %s"
|
||||
MsgInvalidRequest = "error.invalid_request" // "invalid request: %s"
|
||||
MsgInvalidJSON = "error.invalid_json" // "invalid JSON"
|
||||
MsgUnauthorized = "error.unauthorized" // "unauthorized"
|
||||
MsgPermissionDenied = "error.permission_denied" // "permission denied: insufficient role for %s"
|
||||
MsgInternalError = "error.internal" // "internal error: %s"
|
||||
MsgInvalidSlug = "error.invalid_slug" // "%s must be a valid slug (lowercase letters, numbers, hyphens only)"
|
||||
MsgFailedToList = "error.failed_to_list" // "failed to list %s"
|
||||
MsgFailedToCreate = "error.failed_to_create" // "failed to create %s: %s"
|
||||
MsgFailedToUpdate = "error.failed_to_update" // "failed to update %s: %s"
|
||||
MsgFailedToDelete = "error.failed_to_delete" // "failed to delete %s: %s"
|
||||
MsgFailedToSave = "error.failed_to_save" // "failed to save %s: %s"
|
||||
MsgInvalidUpdates = "error.invalid_updates" // "invalid updates"
|
||||
|
||||
// --- Agent ---
|
||||
MsgAgentNotFound = "error.agent_not_found" // "agent not found: %s"
|
||||
MsgCannotDeleteDefault = "error.cannot_delete_default" // "cannot delete the default agent"
|
||||
MsgUserCtxRequired = "error.user_ctx_required" // "user context required"
|
||||
|
||||
// --- Chat ---
|
||||
MsgRateLimitExceeded = "error.rate_limit" // "rate limit exceeded — please wait"
|
||||
MsgNoUserMessage = "error.no_user_message" // "no user message found"
|
||||
MsgUserIDRequired = "error.user_id_required" // "user_id is required"
|
||||
MsgMsgRequired = "error.message_required" // "message is required"
|
||||
|
||||
// --- Channel instances ---
|
||||
MsgInvalidChannelType = "error.invalid_channel_type" // "invalid channel_type"
|
||||
MsgInstanceNotFound = "error.instance_not_found" // "instance not found"
|
||||
|
||||
// --- Cron ---
|
||||
MsgJobNotFound = "error.job_not_found" // "job not found"
|
||||
MsgInvalidCronExpr = "error.invalid_cron_expr" // "invalid cron expression: %s"
|
||||
|
||||
// --- Config ---
|
||||
MsgConfigHashMismatch = "error.config_hash_mismatch" // "config has changed (hash mismatch)"
|
||||
|
||||
// --- Exec approval ---
|
||||
MsgExecApprovalDisabled = "error.exec_approval_disabled" // "exec approval is not enabled"
|
||||
|
||||
// --- Pairing ---
|
||||
MsgSenderChannelRequired = "error.sender_channel_required" // "senderId and channel are required"
|
||||
MsgCodeRequired = "error.code_required" // "code is required"
|
||||
MsgSenderIDRequired = "error.sender_id_required" // "sender_id is required"
|
||||
|
||||
// --- HTTP API ---
|
||||
MsgInvalidAuth = "error.invalid_auth" // "invalid authentication"
|
||||
MsgMsgsRequired = "error.messages_required" // "messages is required"
|
||||
MsgUserIDHeader = "error.user_id_header" // "X-GoClaw-User-Id header is required"
|
||||
MsgFileTooLarge = "error.file_too_large" // "file too large or invalid multipart form"
|
||||
MsgMissingFileField = "error.missing_file_field" // "missing 'file' field"
|
||||
MsgInvalidFilename = "error.invalid_filename" // "invalid filename"
|
||||
MsgChannelKeyReq = "error.channel_key_required" // "channel and key are required"
|
||||
MsgMethodNotAllowed = "error.method_not_allowed" // "method not allowed"
|
||||
MsgStreamingNotSupported = "error.streaming_not_supported" // "streaming not supported"
|
||||
MsgOwnerOnly = "error.owner_only" // "only owner can %s"
|
||||
MsgNoAccess = "error.no_access" // "no access to this %s"
|
||||
MsgAlreadySummoning = "error.already_summoning" // "agent is already being summoned"
|
||||
MsgSummoningUnavailable = "error.summoning_unavailable" // "summoning not available"
|
||||
MsgNoDescription = "error.no_description" // "agent has no description to resummon from"
|
||||
MsgInvalidPath = "error.invalid_path" // "invalid path"
|
||||
|
||||
// --- Scheduler ---
|
||||
MsgQueueFull = "error.queue_full" // "session queue is full"
|
||||
MsgShuttingDown = "error.shutting_down" // "gateway is shutting down, please retry shortly"
|
||||
|
||||
// --- Provider ---
|
||||
MsgProviderReqFailed = "error.provider_request_failed" // "%s: request failed: %s"
|
||||
|
||||
// --- Unknown method ---
|
||||
MsgUnknownMethod = "error.unknown_method" // "unknown method: %s"
|
||||
|
||||
// --- Not implemented ---
|
||||
MsgNotImplemented = "error.not_implemented" // "%s not yet implemented"
|
||||
|
||||
// --- Agent links ---
|
||||
MsgLinksNotConfigured = "error.links_not_configured" // "agent links not configured"
|
||||
MsgInvalidDirection = "error.invalid_direction" // "direction must be outbound, inbound, or bidirectional"
|
||||
MsgSourceTargetSame = "error.source_target_same" // "source and target must be different agents"
|
||||
MsgCannotDelegateOpen = "error.cannot_delegate_open" // "cannot delegate to open agents — only predefined agents can be delegation targets"
|
||||
MsgNoUpdatesProvided = "error.no_updates_provided" // "no updates provided"
|
||||
MsgInvalidLinkStatus = "error.invalid_link_status" // "status must be active or disabled"
|
||||
|
||||
// --- Teams ---
|
||||
MsgTeamsNotConfigured = "error.teams_not_configured" // "teams not configured"
|
||||
MsgAgentIsTeamLead = "error.agent_is_team_lead" // "agent is already the team lead"
|
||||
MsgCannotRemoveTeamLead = "error.cannot_remove_team_lead" // "cannot remove the team lead"
|
||||
|
||||
// --- Delegations ---
|
||||
MsgDelegationsUnavailable = "error.delegations_not_available" // "delegations not available"
|
||||
|
||||
// --- Channels ---
|
||||
MsgCannotDeleteDefaultInst = "error.cannot_delete_default_inst" // "cannot delete default channel instance"
|
||||
|
||||
// --- Skills ---
|
||||
MsgSkillsUpdateNotSupported = "error.skills_update_not_supported" // "skills.update not supported for file-based skills"
|
||||
MsgCannotResolveSkillID = "error.cannot_resolve_skill_id" // "cannot resolve skill ID for file-based skill"
|
||||
|
||||
// --- Logs ---
|
||||
MsgInvalidLogAction = "error.invalid_log_action" // "action must be 'start' or 'stop'"
|
||||
|
||||
// --- Config ---
|
||||
MsgRawConfigRequired = "error.raw_config_required" // "raw config is required"
|
||||
MsgRawPatchRequired = "error.raw_patch_required" // "raw patch is required"
|
||||
|
||||
// --- Storage / File ---
|
||||
MsgCannotDeleteSkillsDir = "error.cannot_delete_skills_dir" // "cannot delete skills directories"
|
||||
MsgFailedToReadFile = "error.failed_to_read_file" // "failed to read file"
|
||||
MsgFileNotFound = "error.file_not_found" // "file not found"
|
||||
MsgInvalidVersion = "error.invalid_version" // "invalid version"
|
||||
MsgVersionNotFound = "error.version_not_found" // "version not found"
|
||||
MsgFailedToDeleteFile = "error.failed_to_delete_file" // "failed to delete"
|
||||
|
||||
// --- OAuth ---
|
||||
MsgNoPendingOAuth = "error.no_pending_oauth" // "no pending OAuth flow"
|
||||
MsgFailedToSaveToken = "error.failed_to_save_token" // "failed to save token"
|
||||
|
||||
// --- Knowledge Graph ---
|
||||
MsgEntityIDRequired = "error.entity_id_required" // "entity_id is required"
|
||||
MsgEntityFieldsRequired = "error.entity_fields_required" // "external_id, name, and entity_type are required"
|
||||
MsgTextRequired = "error.text_required" // "text is required"
|
||||
MsgProviderModelRequired = "error.provider_model_required" // "provider and model are required"
|
||||
MsgInvalidProviderOrModel = "error.invalid_provider_or_model" // "invalid provider or model"
|
||||
)
|
||||
@@ -21,6 +21,8 @@ const (
|
||||
SenderIDKey contextKey = "goclaw_sender_id"
|
||||
// SelfEvolveKey indicates whether a predefined agent can update its SOUL.md.
|
||||
SelfEvolveKey contextKey = "goclaw_self_evolve"
|
||||
// LocaleKey is the context key for the user's preferred locale (e.g. "en", "vi", "zh").
|
||||
LocaleKey contextKey = "goclaw_locale"
|
||||
)
|
||||
|
||||
// WithUserID returns a new context with the given user ID.
|
||||
@@ -87,3 +89,16 @@ func SelfEvolveFromContext(ctx context.Context) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WithLocale returns a new context with the given locale.
|
||||
func WithLocale(ctx context.Context, locale string) context.Context {
|
||||
return context.WithValue(ctx, LocaleKey, locale)
|
||||
}
|
||||
|
||||
// LocaleFromContext extracts the locale from context. Returns "en" if not set.
|
||||
func LocaleFromContext(ctx context.Context) string {
|
||||
if v, ok := ctx.Value(LocaleKey).(string); ok && v != "" {
|
||||
return v
|
||||
}
|
||||
return "en"
|
||||
}
|
||||
@@ -22,10 +22,12 @@
|
||||
"d3-force": "^3.0.0",
|
||||
"framer-motion": "^12.34.3",
|
||||
"highlight.js": "^11.11.1",
|
||||
"i18next": "^25.8.15",
|
||||
"lucide-react": "^0.468.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-i18next": "^16.5.6",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.1.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
|
||||
Generated
+66
@@ -44,6 +44,9 @@ importers:
|
||||
highlight.js:
|
||||
specifier: ^11.11.1
|
||||
version: 11.11.1
|
||||
i18next:
|
||||
specifier: ^25.8.15
|
||||
version: 25.8.15(typescript@5.7.3)
|
||||
lucide-react:
|
||||
specifier: ^0.468.0
|
||||
version: 0.468.0(react@19.2.4)
|
||||
@@ -56,6 +59,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.4(react@19.2.4)
|
||||
react-i18next:
|
||||
specifier: ^16.5.6
|
||||
version: 16.5.6(i18next@25.8.15(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
|
||||
react-markdown:
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0(@types/react@19.2.14)(react@19.2.4)
|
||||
@@ -179,6 +185,10 @@ packages:
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/runtime@7.28.6':
|
||||
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.28.6':
|
||||
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1654,9 +1664,20 @@ packages:
|
||||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||
|
||||
html-url-attributes@3.0.1:
|
||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||
|
||||
i18next@25.8.15:
|
||||
resolution: {integrity: sha512-DW+vfwYtt+8lqpknhPGBfPL2P4SvtUv+KGDZAjfPrbnKAJkzVWGNRYw/rNkzxSzCBvnpiUnF5AgX7kL8hCg6uw==}
|
||||
peerDependencies:
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
inline-style-parser@0.2.7:
|
||||
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
|
||||
|
||||
@@ -1972,6 +1993,22 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^19.2.4
|
||||
|
||||
react-i18next@16.5.6:
|
||||
resolution: {integrity: sha512-Ua7V2/efA88ido7KyK51fb8Ki8M/sRfW8LR/rZ/9ZKr2luhuTI7kwYZN5agT1rWG7aYm5G0RYE/6JR8KJoCMDw==}
|
||||
peerDependencies:
|
||||
i18next: '>= 25.6.2'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
react-markdown@10.1.0:
|
||||
resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
|
||||
peerDependencies:
|
||||
@@ -2204,6 +2241,10 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
void-elements@3.1.0:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ws@8.19.0:
|
||||
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -2346,6 +2387,8 @@ snapshots:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/runtime@7.28.6': {}
|
||||
|
||||
'@babel/template@7.28.6':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
@@ -3752,8 +3795,18 @@ snapshots:
|
||||
|
||||
highlight.js@11.11.1: {}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
|
||||
html-url-attributes@3.0.1: {}
|
||||
|
||||
i18next@25.8.15(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
optionalDependencies:
|
||||
typescript: 5.7.3
|
||||
|
||||
inline-style-parser@0.2.7: {}
|
||||
|
||||
is-alphabetical@2.0.1: {}
|
||||
@@ -4299,6 +4352,17 @@ snapshots:
|
||||
react: 19.2.4
|
||||
scheduler: 0.27.0
|
||||
|
||||
react-i18next@16.5.6(i18next@25.8.15(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 25.8.15(typescript@5.7.3)
|
||||
react: 19.2.4
|
||||
use-sync-external-store: 1.6.0(react@19.2.4)
|
||||
optionalDependencies:
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
typescript: 5.7.3
|
||||
|
||||
react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4):
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
@@ -4562,6 +4626,8 @@ snapshots:
|
||||
jiti: 2.6.1
|
||||
lightningcss: 1.31.1
|
||||
|
||||
void-elements@3.1.0: {}
|
||||
|
||||
ws@8.19.0: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
@@ -197,6 +197,7 @@ export class WsClient {
|
||||
token: this.getToken(),
|
||||
user_id: this.getUserId(),
|
||||
sender_id: this.getSenderID(),
|
||||
locale: localStorage.getItem("goclaw:language") || "en",
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
});
|
||||
if (this.connectGeneration !== generation) return;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Bot, ChevronDown } from "lucide-react";
|
||||
import { useHttp } from "@/hooks/use-ws";
|
||||
import { useAuthStore } from "@/stores/use-auth-store";
|
||||
@@ -10,6 +11,7 @@ interface AgentSelectorProps {
|
||||
}
|
||||
|
||||
export function AgentSelector({ value, onChange }: AgentSelectorProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const http = useHttp();
|
||||
const connected = useAuthStore((s) => s.connected);
|
||||
const [agents, setAgents] = useState<AgentData[]>([]);
|
||||
@@ -37,7 +39,7 @@ export function AgentSelector({ value, onChange }: AgentSelectorProps) {
|
||||
>
|
||||
<Bot className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span className="flex-1 truncate text-left">
|
||||
{selected?.display_name ?? selected?.agent_key ?? (value || "Select agent")}
|
||||
{selected?.display_name ?? selected?.agent_key ?? (value || t("selectAgent"))}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
@@ -48,7 +50,7 @@ export function AgentSelector({ value, onChange }: AgentSelectorProps) {
|
||||
<div className="absolute left-0 right-0 top-full z-50 mt-1 rounded-lg border bg-popover p-1 shadow-md">
|
||||
{agents.length === 0 && (
|
||||
<div className="px-3 py-2 text-sm text-muted-foreground">
|
||||
No agents available
|
||||
{t("noAgentsAvailable")}
|
||||
</div>
|
||||
)}
|
||||
{agents.map((agent) => (
|
||||
@@ -68,7 +70,7 @@ export function AgentSelector({ value, onChange }: AgentSelectorProps) {
|
||||
{agent.display_name || agent.agent_key}
|
||||
</span>
|
||||
{agent.is_default && (
|
||||
<span className="text-xs text-muted-foreground">default</span>
|
||||
<span className="text-xs text-muted-foreground">{t("default")}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef, useCallback, type KeyboardEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Send, Square } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
@@ -10,6 +11,7 @@ interface ChatInputProps {
|
||||
}
|
||||
|
||||
export function ChatInput({ onSend, onAbort, isRunning, disabled }: ChatInputProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const [value, setValue] = useState("");
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
@@ -49,7 +51,7 @@ export function ChatInput({ onSend, onAbort, isRunning, disabled }: ChatInputPro
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onInput={handleInput}
|
||||
placeholder="Send a message... (Shift+Enter for new line)"
|
||||
placeholder={t("sendMessage")}
|
||||
disabled={disabled}
|
||||
rows={1}
|
||||
className="flex-1 resize-none rounded-lg border bg-background px-4 py-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:opacity-50"
|
||||
@@ -59,7 +61,7 @@ export function ChatInput({ onSend, onAbort, isRunning, disabled }: ChatInputPro
|
||||
variant="destructive"
|
||||
size="icon-lg"
|
||||
onClick={onAbort}
|
||||
title="Stop generation"
|
||||
title={t("stopGeneration")}
|
||||
>
|
||||
<Square className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -68,7 +70,7 @@ export function ChatInput({ onSend, onAbort, isRunning, disabled }: ChatInputPro
|
||||
size="icon-lg"
|
||||
onClick={handleSend}
|
||||
disabled={!value.trim() || disabled}
|
||||
title="Send message"
|
||||
title={t("sendMessageTitle")}
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Image,
|
||||
Video,
|
||||
@@ -126,33 +127,26 @@ const mediaIcons: Record<string, typeof Image> = {
|
||||
animation: Video,
|
||||
};
|
||||
|
||||
const mediaLabels: Record<string, string> = {
|
||||
image: "Image",
|
||||
video: "Video",
|
||||
audio: "Audio",
|
||||
voice: "Voice message",
|
||||
document: "Document",
|
||||
animation: "Animation",
|
||||
};
|
||||
|
||||
function MediaBadge({ mediaType }: { mediaType: string }) {
|
||||
const { t } = useTranslation("chat");
|
||||
const Icon = mediaIcons[mediaType] ?? FileText;
|
||||
const label = mediaLabels[mediaType] ?? mediaType;
|
||||
const label = t(`media.${mediaType}`, { defaultValue: mediaType });
|
||||
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-md border border-blue-200 bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-300">
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{label} attached
|
||||
{label} {t("media.attached")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardBadge({ from, date }: { from: string; date: string }) {
|
||||
const { t } = useTranslation("chat");
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs text-amber-700 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-300">
|
||||
<Forward className="h-3.5 w-3.5" />
|
||||
<span>
|
||||
Forwarded from <span className="font-medium">{from}</span>
|
||||
{t("forwardedFrom")} <span className="font-medium">{from}</span>
|
||||
{date && <span className="text-amber-600 dark:text-amber-400"> · {date}</span>}
|
||||
</span>
|
||||
</div>
|
||||
@@ -160,11 +154,12 @@ function ForwardBadge({ from, date }: { from: string; date: string }) {
|
||||
}
|
||||
|
||||
function ReplyQuote({ sender, body }: { sender: string; body: string }) {
|
||||
const { t } = useTranslation("chat");
|
||||
return (
|
||||
<div className="rounded-md border-l-2 border-muted-foreground/40 bg-muted/50 px-3 py-2">
|
||||
<div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
||||
<Reply className="h-3 w-3" />
|
||||
Replying to {sender}
|
||||
{t("replyingTo")} {sender}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground/80 line-clamp-3">{body}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MessageSquare } from "lucide-react";
|
||||
import { formatRelativeTime } from "@/lib/format";
|
||||
import { parseSessionKey } from "@/lib/session-key";
|
||||
@@ -16,6 +17,7 @@ export function SessionSwitcher({
|
||||
onSelect,
|
||||
loading,
|
||||
}: SessionSwitcherProps) {
|
||||
const { t } = useTranslation("common");
|
||||
if (sessions.length === 0 && loading) {
|
||||
return (
|
||||
<div className="space-y-2 p-2">
|
||||
@@ -29,7 +31,7 @@ export function SessionSwitcher({
|
||||
if (sessions.length === 0) {
|
||||
return (
|
||||
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
No sessions yet. Start a chat!
|
||||
{t("noSessions")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -56,7 +58,7 @@ export function SessionSwitcher({
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium">{label}</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>{session.messageCount} messages</span>
|
||||
<span>{session.messageCount} {t("messages")}</span>
|
||||
<span>{formatRelativeTime(session.updated)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronDown, ChevronRight, Brain } from "lucide-react";
|
||||
|
||||
interface ThinkingBlockProps {
|
||||
@@ -7,6 +8,7 @@ interface ThinkingBlockProps {
|
||||
}
|
||||
|
||||
export function ThinkingBlock({ text, isStreaming = false }: ThinkingBlockProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const [expanded, setExpanded] = useState(isStreaming);
|
||||
|
||||
return (
|
||||
@@ -18,7 +20,7 @@ export function ThinkingBlock({ text, isStreaming = false }: ThinkingBlockProps)
|
||||
>
|
||||
<Brain className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="text-xs font-medium">
|
||||
Thinking{isStreaming ? "..." : ""}
|
||||
{isStreaming ? t("thinkingStreaming") : t("thinking")}
|
||||
</span>
|
||||
{expanded ? (
|
||||
<ChevronDown className="ml-auto h-3.5 w-3.5" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Wrench, Check, AlertTriangle, Loader2, ChevronDown, ChevronRight, Zap } from "lucide-react";
|
||||
import type { ToolStreamEntry } from "@/types/chat";
|
||||
|
||||
@@ -18,6 +19,7 @@ interface ToolCallCardProps {
|
||||
}
|
||||
|
||||
export function ToolCallCard({ entry }: ToolCallCardProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const hasDetails = entry.arguments || entry.result;
|
||||
const hasError = entry.phase === "error" && !!entry.errorContent;
|
||||
const canExpand = hasDetails || hasError;
|
||||
@@ -57,7 +59,7 @@ export function ToolCallCard({ entry }: ToolCallCardProps) {
|
||||
)}
|
||||
{entry.arguments && Object.keys(entry.arguments).length > 0 && (
|
||||
<div>
|
||||
<div className="text-[10px] font-semibold uppercase text-muted-foreground mb-1">Arguments</div>
|
||||
<div className="text-[10px] font-semibold uppercase text-muted-foreground mb-1">{t("toolArguments")}</div>
|
||||
<pre className="whitespace-pre-wrap text-xs font-mono bg-background rounded p-2 max-h-48 overflow-y-auto">
|
||||
{JSON.stringify(entry.arguments, null, 2)}
|
||||
</pre>
|
||||
@@ -65,7 +67,7 @@ export function ToolCallCard({ entry }: ToolCallCardProps) {
|
||||
)}
|
||||
{entry.result && (
|
||||
<div>
|
||||
<div className="text-[10px] font-semibold uppercase text-muted-foreground mb-1">Result</div>
|
||||
<div className="text-[10px] font-semibold uppercase text-muted-foreground mb-1">{t("toolResult")}</div>
|
||||
<pre className="whitespace-pre-wrap text-xs font-mono bg-background rounded p-2 max-h-48 overflow-y-auto">
|
||||
{entry.result}
|
||||
</pre>
|
||||
@@ -103,9 +105,10 @@ function ToolIcon({ phase, isSkill }: { phase: ToolStreamEntry["phase"]; isSkill
|
||||
}
|
||||
|
||||
function PhaseLabel({ phase, isSkill }: { phase: ToolStreamEntry["phase"]; isSkill?: boolean }) {
|
||||
const { t } = useTranslation("common");
|
||||
const labels: Record<string, string> = isSkill
|
||||
? { calling: "Activating...", completed: "Activated", error: "Failed" }
|
||||
: { calling: "Running...", completed: "Done", error: "Failed" };
|
||||
? { calling: t("skillActivating"), completed: t("skillActivated"), error: t("toolFailed") }
|
||||
: { calling: t("toolRunning"), completed: t("toolDone"), error: t("toolFailed") };
|
||||
const colors: Record<string, string> = {
|
||||
calling: "text-blue-500",
|
||||
completed: "text-green-500",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Outlet } from "react-router";
|
||||
import { WifiOff } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { Topbar } from "./topbar";
|
||||
import { useUiStore } from "@/stores/use-ui-store";
|
||||
@@ -8,6 +9,7 @@ import { useIsMobile } from "@/hooks/use-media-query";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function AppLayout() {
|
||||
const { t } = useTranslation("common");
|
||||
const sidebarCollapsed = useUiStore((s) => s.sidebarCollapsed);
|
||||
const mobileSidebarOpen = useUiStore((s) => s.mobileSidebarOpen);
|
||||
const setMobileSidebarOpen = useUiStore((s) => s.setMobileSidebarOpen);
|
||||
@@ -43,7 +45,7 @@ export function AppLayout() {
|
||||
{!connected && (
|
||||
<div className="flex items-center gap-2 border-b border-destructive/30 bg-destructive/10 px-4 py-2 text-sm text-destructive">
|
||||
<WifiOff className="h-4 w-4 shrink-0" />
|
||||
<span>Disconnected from gateway. Attempting to reconnect...</span>
|
||||
<span>{t("disconnectedGateway")}</span>
|
||||
</div>
|
||||
)}
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAuthStore } from "@/stores/use-auth-store";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function ConnectionStatus() {
|
||||
const { t } = useTranslation("common");
|
||||
const connected = useAuthStore((s) => s.connected);
|
||||
|
||||
return (
|
||||
@@ -12,7 +14,7 @@ export function ConnectionStatus() {
|
||||
connected ? "bg-green-500" : "bg-red-500",
|
||||
)}
|
||||
/>
|
||||
<span>{connected ? "Connected" : "Disconnected"}</span>
|
||||
<span>{connected ? t("connected") : t("disconnected")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
Inbox,
|
||||
Brain,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SidebarGroup } from "./sidebar-group";
|
||||
import { SidebarItem } from "./sidebar-item";
|
||||
import { ConnectionStatus } from "./connection-status";
|
||||
@@ -37,6 +38,7 @@ interface SidebarProps {
|
||||
}
|
||||
|
||||
export function Sidebar({ collapsed, onNavItemClick }: SidebarProps) {
|
||||
const { t } = useTranslation("sidebar");
|
||||
const { pendingCount } = usePendingPairingsCount();
|
||||
|
||||
return (
|
||||
@@ -66,40 +68,40 @@ export function Sidebar({ collapsed, onNavItemClick }: SidebarProps) {
|
||||
|
||||
{/* Nav items */}
|
||||
<nav className="flex-1 space-y-4 overflow-y-auto px-2 py-4">
|
||||
<SidebarGroup label="Core" collapsed={collapsed}>
|
||||
<SidebarItem to={ROUTES.OVERVIEW} icon={LayoutDashboard} label="Overview" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.CHAT} icon={MessageSquare} label="Chat" collapsed={collapsed} />
|
||||
<SidebarGroup label={t("groups.core")} collapsed={collapsed}>
|
||||
<SidebarItem to={ROUTES.OVERVIEW} icon={LayoutDashboard} label={t("nav.overview")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.CHAT} icon={MessageSquare} label={t("nav.chat")} collapsed={collapsed} />
|
||||
</SidebarGroup>
|
||||
|
||||
<SidebarGroup label="Management" collapsed={collapsed}>
|
||||
<SidebarItem to={ROUTES.AGENTS} icon={Bot} label="Agents" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.TEAMS} icon={Users} label="Agent Teams" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.SESSIONS} icon={History} label="Sessions" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.PENDING_MESSAGES} icon={Inbox} label="Pending Messages" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.CHANNELS} icon={Radio} label="Channels" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.SKILLS} icon={Zap} label="Skills" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.CRON} icon={Clock} label="Cron" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.CUSTOM_TOOLS} icon={Wrench} label="Custom Tools" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.BUILTIN_TOOLS} icon={Package} label="Built-in Tools" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.MCP} icon={Plug} label="MCP Servers" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.MEMORY} icon={Brain} label="Memory" collapsed={collapsed} />
|
||||
<SidebarGroup label={t("groups.management")} collapsed={collapsed}>
|
||||
<SidebarItem to={ROUTES.AGENTS} icon={Bot} label={t("nav.agents")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.TEAMS} icon={Users} label={t("nav.agentTeams")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.SESSIONS} icon={History} label={t("nav.sessions")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.PENDING_MESSAGES} icon={Inbox} label={t("nav.pendingMessages")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.CHANNELS} icon={Radio} label={t("nav.channels")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.SKILLS} icon={Zap} label={t("nav.skills")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.CRON} icon={Clock} label={t("nav.cron")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.CUSTOM_TOOLS} icon={Wrench} label={t("nav.customTools")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.BUILTIN_TOOLS} icon={Package} label={t("nav.builtinTools")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.MCP} icon={Plug} label={t("nav.mcpServers")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.MEMORY} icon={Brain} label={t("nav.memory")} collapsed={collapsed} />
|
||||
</SidebarGroup>
|
||||
|
||||
<SidebarGroup label="Monitoring" collapsed={collapsed}>
|
||||
<SidebarItem to={ROUTES.TRACES} icon={Activity} label="Traces" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.EVENTS} icon={Radar} label="Realtime Events" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.DELEGATIONS} icon={ArrowRightLeft} label="Delegations" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.USAGE} icon={BarChart3} label="Usage" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.LOGS} icon={Terminal} label="Logs" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.STORAGE} icon={HardDrive} label="Storage" collapsed={collapsed} />
|
||||
<SidebarGroup label={t("groups.monitoring")} collapsed={collapsed}>
|
||||
<SidebarItem to={ROUTES.TRACES} icon={Activity} label={t("nav.traces")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.EVENTS} icon={Radar} label={t("nav.realtimeEvents")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.DELEGATIONS} icon={ArrowRightLeft} label={t("nav.delegations")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.USAGE} icon={BarChart3} label={t("nav.usage")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.LOGS} icon={Terminal} label={t("nav.logs")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.STORAGE} icon={HardDrive} label={t("nav.storage")} collapsed={collapsed} />
|
||||
</SidebarGroup>
|
||||
|
||||
<SidebarGroup label="System" collapsed={collapsed}>
|
||||
<SidebarItem to={ROUTES.PROVIDERS} icon={Cpu} label="Providers" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.CONFIG} icon={Settings} label="Config" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.APPROVALS} icon={ShieldCheck} label="Approvals" collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.NODES} icon={Link} label="Nodes" collapsed={collapsed} badge={pendingCount} />
|
||||
<SidebarItem to={ROUTES.TTS} icon={Volume2} label="TTS" collapsed={collapsed} />
|
||||
<SidebarGroup label={t("groups.system")} collapsed={collapsed}>
|
||||
<SidebarItem to={ROUTES.PROVIDERS} icon={Cpu} label={t("nav.providers")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.CONFIG} icon={Settings} label={t("nav.config")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.APPROVALS} icon={ShieldCheck} label={t("nav.approvals")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.NODES} icon={Link} label={t("nav.nodes")} collapsed={collapsed} badge={pendingCount} />
|
||||
<SidebarItem to={ROUTES.TTS} icon={Volume2} label={t("nav.tts")} collapsed={collapsed} />
|
||||
</SidebarGroup>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { Moon, Sun, PanelLeftClose, PanelLeftOpen, Menu, LogOut, Bell } from "lucide-react";
|
||||
import { Moon, Sun, PanelLeftClose, PanelLeftOpen, Menu, LogOut, Bell, Globe } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useUiStore } from "@/stores/use-ui-store";
|
||||
import { useAuthStore } from "@/stores/use-auth-store";
|
||||
import { useIsMobile } from "@/hooks/use-media-query";
|
||||
import { usePendingPairingsCount } from "@/hooks/use-pending-pairings-count";
|
||||
import { ROUTES } from "@/lib/constants";
|
||||
import { ROUTES, SUPPORTED_LANGUAGES, LANGUAGE_LABELS, type Language } from "@/lib/constants";
|
||||
|
||||
export function Topbar() {
|
||||
const { t } = useTranslation("topbar");
|
||||
const theme = useUiStore((s) => s.theme);
|
||||
const setTheme = useUiStore((s) => s.setTheme);
|
||||
const language = useUiStore((s) => s.language);
|
||||
const setLanguage = useUiStore((s) => s.setLanguage);
|
||||
const sidebarCollapsed = useUiStore((s) => s.sidebarCollapsed);
|
||||
const toggleSidebar = useUiStore((s) => s.toggleSidebar);
|
||||
const setMobileSidebarOpen = useUiStore((s) => s.setMobileSidebarOpen);
|
||||
@@ -24,13 +28,19 @@ export function Topbar() {
|
||||
? () => setMobileSidebarOpen(true)
|
||||
: toggleSidebar;
|
||||
|
||||
const cycleLanguage = () => {
|
||||
const idx = SUPPORTED_LANGUAGES.indexOf(language);
|
||||
const next = SUPPORTED_LANGUAGES[(idx + 1) % SUPPORTED_LANGUAGES.length] as Language;
|
||||
setLanguage(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="flex h-14 items-center justify-between border-b bg-background px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleSidebarToggle}
|
||||
className="cursor-pointer rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
title={isMobile ? "Open menu" : sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||
title={isMobile ? t("openMenu") : sidebarCollapsed ? t("expandSidebar") : t("collapseSidebar")}
|
||||
>
|
||||
{isMobile ? (
|
||||
<Menu className="h-4 w-4" />
|
||||
@@ -50,7 +60,7 @@ export function Topbar() {
|
||||
<button
|
||||
onClick={() => navigate(ROUTES.NODES)}
|
||||
className="relative cursor-pointer rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
title={pendingCount > 0 ? `${pendingCount} pending pairing request${pendingCount > 1 ? "s" : ""}` : "Pairing requests"}
|
||||
title={pendingCount > 0 ? t("pendingPairing", { count: pendingCount }) : t("pairingRequests")}
|
||||
>
|
||||
<Bell className="h-4 w-4" />
|
||||
{pendingCount > 0 && (
|
||||
@@ -58,10 +68,19 @@ export function Topbar() {
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={cycleLanguage}
|
||||
className="cursor-pointer rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground flex items-center gap-1"
|
||||
title={t("language")}
|
||||
>
|
||||
<Globe className="h-4 w-4" />
|
||||
<span className="text-xs">{LANGUAGE_LABELS[language]}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||
className="cursor-pointer rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
title="Toggle theme"
|
||||
title={t("toggleTheme")}
|
||||
>
|
||||
{isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</button>
|
||||
@@ -69,7 +88,7 @@ export function Topbar() {
|
||||
<button
|
||||
onClick={logout}
|
||||
className="cursor-pointer rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
title="Logout"
|
||||
title={t("logout")}
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -27,10 +28,11 @@ export function ConfirmDeleteDialog({
|
||||
title,
|
||||
description,
|
||||
confirmValue,
|
||||
confirmLabel = "Delete",
|
||||
confirmLabel,
|
||||
onConfirm,
|
||||
loading,
|
||||
}: ConfirmDeleteDialogProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -48,7 +50,7 @@ export function ConfirmDeleteDialog({
|
||||
</DialogHeader>
|
||||
<div className="py-2">
|
||||
<p className="mb-2 text-sm text-muted-foreground">
|
||||
Type <span className="font-semibold text-foreground">{confirmValue}</span> to confirm.
|
||||
{t("typeToConfirmPrefix")} <span className="font-semibold text-foreground">{confirmValue}</span> {t("typeToConfirmSuffix")}
|
||||
</p>
|
||||
<Input
|
||||
value={inputValue}
|
||||
@@ -59,14 +61,14 @@ export function ConfirmDeleteDialog({
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={onConfirm}
|
||||
disabled={!isMatch || loading}
|
||||
>
|
||||
{loading ? "..." : confirmLabel}
|
||||
{loading ? "..." : (confirmLabel ?? t("delete"))}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -24,11 +25,12 @@ export function ConfirmDialog({
|
||||
onOpenChange,
|
||||
title,
|
||||
description,
|
||||
confirmLabel = "Confirm",
|
||||
confirmLabel,
|
||||
variant = "default",
|
||||
onConfirm,
|
||||
loading,
|
||||
}: ConfirmDialogProps) {
|
||||
const { t } = useTranslation("common");
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
@@ -38,10 +40,10 @@ export function ConfirmDialog({
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button variant={variant} onClick={onConfirm} disabled={loading}>
|
||||
{loading ? "..." : confirmLabel}
|
||||
{loading ? "..." : (confirmLabel ?? t("confirm"))}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatSize, type TreeNode } from "@/lib/file-helpers";
|
||||
import { FileTreePanel } from "@/components/shared/file-tree";
|
||||
import { FileContentPanel } from "@/components/shared/file-viewers";
|
||||
@@ -33,6 +34,7 @@ export function FileBrowser({
|
||||
showSize?: boolean;
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
const { t } = useTranslation("common");
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [treeWidth, setTreeWidth] = useState(220);
|
||||
const [mobileShowTree, setMobileShowTree] = useState(true);
|
||||
@@ -82,7 +84,7 @@ export function FileBrowser({
|
||||
onClick={() => setMobileShowTree(true)}
|
||||
className="text-primary hover:underline cursor-pointer shrink-0"
|
||||
>
|
||||
← Files
|
||||
← {t("filesBack")}
|
||||
</button>
|
||||
{fileContent && (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Folder,
|
||||
FolderOpen,
|
||||
@@ -119,6 +120,7 @@ export function FileTreePanel({
|
||||
onDelete?: (path: string, isDir: boolean) => void;
|
||||
showSize?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation("common");
|
||||
if (filesLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
@@ -127,7 +129,7 @@ export function FileTreePanel({
|
||||
);
|
||||
}
|
||||
if (tree.length === 0) {
|
||||
return <p className="px-3 py-4 text-sm text-muted-foreground">No files found.</p>;
|
||||
return <p className="px-3 py-4 text-sm text-muted-foreground">{t("noFiles")}</p>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Check, Copy, Loader2 } from "lucide-react";
|
||||
import { MarkdownRenderer } from "@/components/shared/markdown-renderer";
|
||||
import { useClipboard } from "@/hooks/use-clipboard";
|
||||
@@ -162,6 +163,7 @@ export function FileContentPanel({
|
||||
fileContent: { content: string; path: string; size: number } | null;
|
||||
contentLoading: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation("common");
|
||||
if (contentLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
@@ -174,7 +176,7 @@ export function FileContentPanel({
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8 text-sm text-muted-foreground">
|
||||
Select a file to view its content
|
||||
{t("selectFileToView")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
@@ -29,16 +30,17 @@ export function Pagination({
|
||||
onPageSizeChange,
|
||||
pageSizes = PAGE_SIZES,
|
||||
}: PaginationProps) {
|
||||
const { t } = useTranslation("common");
|
||||
if (total === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-2 border-t px-4 py-3 sm:flex-row sm:justify-between">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{total} {total === 1 ? "item" : "items"}
|
||||
{total} {t("items")}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 sm:gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Rows</span>
|
||||
<span className="text-sm text-muted-foreground">{t("rows")}</span>
|
||||
<Select
|
||||
value={String(pageSize)}
|
||||
onValueChange={(v) => onPageSizeChange(Number(v))}
|
||||
@@ -56,7 +58,7 @@ export function Pagination({
|
||||
</Select>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Page {page} of {totalPages}
|
||||
{t("pageOf", { page, totalPages })}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Combobox } from "@/components/ui/combobox";
|
||||
@@ -40,17 +41,18 @@ export function ProviderModelSelect({
|
||||
onProviderChange,
|
||||
model,
|
||||
onModelChange,
|
||||
providerTip = "LLM provider name. Must match a configured provider.",
|
||||
modelTip = "Model ID to use.",
|
||||
providerLabel = "Provider",
|
||||
modelLabel = "Model",
|
||||
providerPlaceholder = "Select provider",
|
||||
modelPlaceholder = "Enter or select model",
|
||||
providerTip,
|
||||
modelTip,
|
||||
providerLabel,
|
||||
modelLabel,
|
||||
providerPlaceholder,
|
||||
modelPlaceholder,
|
||||
showVerify,
|
||||
savedProvider,
|
||||
savedModel,
|
||||
onSaveBlockedChange,
|
||||
}: ProviderModelSelectProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const { providers } = useProviders();
|
||||
const enabledProviders = providers.filter((p) => p.enabled);
|
||||
|
||||
@@ -94,11 +96,11 @@ export function ProviderModelSelect({
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-1.5">
|
||||
<InfoLabel tip={providerTip}>{providerLabel}</InfoLabel>
|
||||
<InfoLabel tip={providerTip ?? t("providerTip")}>{providerLabel ?? t("provider")}</InfoLabel>
|
||||
{enabledProviders.length > 0 ? (
|
||||
<Select value={provider} onValueChange={handleProviderChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={providerPlaceholder} />
|
||||
<SelectValue placeholder={providerPlaceholder ?? t("selectProvider")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{enabledProviders.map((p) => (
|
||||
@@ -112,19 +114,19 @@ export function ProviderModelSelect({
|
||||
<Input
|
||||
value={provider}
|
||||
onChange={(e) => handleProviderChange(e.target.value)}
|
||||
placeholder="No providers configured"
|
||||
placeholder={t("noProvidersConfigured")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<InfoLabel tip={modelTip}>{modelLabel}</InfoLabel>
|
||||
<InfoLabel tip={modelTip ?? t("modelTip")}>{modelLabel ?? t("model")}</InfoLabel>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<Combobox
|
||||
value={model}
|
||||
onChange={onModelChange}
|
||||
options={models.map((m) => ({ value: m.id, label: m.name }))}
|
||||
placeholder={modelsLoading ? "Loading models..." : modelPlaceholder}
|
||||
placeholder={modelsLoading ? t("loadingModels") : (modelPlaceholder ?? t("enterOrSelectModel"))}
|
||||
/>
|
||||
</div>
|
||||
{shouldShowVerify && (
|
||||
@@ -136,13 +138,13 @@ export function ProviderModelSelect({
|
||||
disabled={!selectedProviderId || !model.trim() || verifying}
|
||||
onClick={handleVerify}
|
||||
>
|
||||
{verifying ? "..." : "Check"}
|
||||
{verifying ? "..." : t("check")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{shouldShowVerify && verifyResult && (
|
||||
<p className={`text-xs ${verifyResult.valid ? "text-success" : "text-destructive"}`}>
|
||||
{verifyResult.valid ? "Model verified" : verifyResult.error || "Verification failed"}
|
||||
{verifyResult.valid ? t("modelVerified") : verifyResult.error || t("verificationFailed")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Search } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useDebounce } from "@/hooks/use-debounce";
|
||||
import { useState, useEffect } from "react";
|
||||
@@ -14,10 +15,11 @@ interface SearchInputProps {
|
||||
export function SearchInput({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "Search...",
|
||||
placeholder,
|
||||
className,
|
||||
delay = 300,
|
||||
}: SearchInputProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const [local, setLocal] = useState(value);
|
||||
const debounced = useDebounce(local, delay);
|
||||
|
||||
@@ -35,7 +37,7 @@ export function SearchInput({
|
||||
<Input
|
||||
value={local}
|
||||
onChange={(e) => setLocal(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
placeholder={placeholder ?? t("searchPlaceholder")}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -12,9 +13,10 @@ interface TagInputProps {
|
||||
export function TagInput({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "Type and press Enter...",
|
||||
placeholder,
|
||||
className,
|
||||
}: TagInputProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const [input, setInput] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -70,7 +72,7 @@ export function TagInput({
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={() => { if (input.trim()) addTag(input); }}
|
||||
placeholder={value.length === 0 ? placeholder : ""}
|
||||
placeholder={value.length === 0 ? (placeholder ?? t("typeAndPressEnter")) : ""}
|
||||
className="placeholder:text-muted-foreground min-w-[80px] flex-1 bg-transparent py-0.5 text-sm outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo, useState, useRef, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { X, ChevronDownIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useBuiltinTools } from "@/pages/builtin-tools/hooks/use-builtin-tools";
|
||||
@@ -20,9 +21,10 @@ interface ToolOption {
|
||||
export function ToolNameSelect({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "Select or type tool names...",
|
||||
placeholder,
|
||||
className,
|
||||
}: ToolNameSelectProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const { tools: builtinTools } = useBuiltinTools();
|
||||
const { tools: customTools } = useCustomTools();
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -127,7 +129,7 @@ export function ToolNameSelect({
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={value.length === 0 ? placeholder : ""}
|
||||
placeholder={value.length === 0 ? (placeholder ?? t("selectOrTypeTools")) : ""}
|
||||
className="placeholder:text-muted-foreground min-w-[80px] flex-1 bg-transparent py-0.5 text-sm outline-none"
|
||||
/>
|
||||
<ChevronDownIcon
|
||||
@@ -140,7 +142,7 @@ export function ToolNameSelect({
|
||||
{grouped.builtin.length > 0 && (
|
||||
<>
|
||||
<div className="text-muted-foreground px-2 py-1 text-[10px] font-semibold uppercase tracking-wider">
|
||||
Built-in
|
||||
{t("builtinTools")}
|
||||
</div>
|
||||
{grouped.builtin.map((t) => (
|
||||
<button
|
||||
@@ -159,7 +161,7 @@ export function ToolNameSelect({
|
||||
{grouped.custom.length > 0 && (
|
||||
<>
|
||||
<div className="text-muted-foreground mt-1 px-2 py-1 text-[10px] font-semibold uppercase tracking-wider">
|
||||
Custom
|
||||
{t("customTools")}
|
||||
</div>
|
||||
{grouped.custom.map((t) => (
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
// --- EN namespaces ---
|
||||
import enCommon from "./locales/en/common.json";
|
||||
import enSidebar from "./locales/en/sidebar.json";
|
||||
import enTopbar from "./locales/en/topbar.json";
|
||||
import enLogin from "./locales/en/login.json";
|
||||
import enOverview from "./locales/en/overview.json";
|
||||
import enChat from "./locales/en/chat.json";
|
||||
import enAgents from "./locales/en/agents.json";
|
||||
import enTeams from "./locales/en/teams.json";
|
||||
import enSessions from "./locales/en/sessions.json";
|
||||
import enSkills from "./locales/en/skills.json";
|
||||
import enCron from "./locales/en/cron.json";
|
||||
import enConfig from "./locales/en/config.json";
|
||||
import enChannels from "./locales/en/channels.json";
|
||||
import enProviders from "./locales/en/providers.json";
|
||||
import enTraces from "./locales/en/traces.json";
|
||||
import enEvents from "./locales/en/events.json";
|
||||
import enDelegations from "./locales/en/delegations.json";
|
||||
import enUsage from "./locales/en/usage.json";
|
||||
import enApprovals from "./locales/en/approvals.json";
|
||||
import enNodes from "./locales/en/nodes.json";
|
||||
import enLogs from "./locales/en/logs.json";
|
||||
import enTools from "./locales/en/tools.json";
|
||||
import enMcp from "./locales/en/mcp.json";
|
||||
import enTts from "./locales/en/tts.json";
|
||||
import enSetup from "./locales/en/setup.json";
|
||||
import enMemory from "./locales/en/memory.json";
|
||||
import enStorage from "./locales/en/storage.json";
|
||||
import enPendingMessages from "./locales/en/pending-messages.json";
|
||||
|
||||
// --- VI namespaces ---
|
||||
import viCommon from "./locales/vi/common.json";
|
||||
import viSidebar from "./locales/vi/sidebar.json";
|
||||
import viTopbar from "./locales/vi/topbar.json";
|
||||
import viLogin from "./locales/vi/login.json";
|
||||
import viOverview from "./locales/vi/overview.json";
|
||||
import viChat from "./locales/vi/chat.json";
|
||||
import viAgents from "./locales/vi/agents.json";
|
||||
import viTeams from "./locales/vi/teams.json";
|
||||
import viSessions from "./locales/vi/sessions.json";
|
||||
import viSkills from "./locales/vi/skills.json";
|
||||
import viCron from "./locales/vi/cron.json";
|
||||
import viConfig from "./locales/vi/config.json";
|
||||
import viChannels from "./locales/vi/channels.json";
|
||||
import viProviders from "./locales/vi/providers.json";
|
||||
import viTraces from "./locales/vi/traces.json";
|
||||
import viEvents from "./locales/vi/events.json";
|
||||
import viDelegations from "./locales/vi/delegations.json";
|
||||
import viUsage from "./locales/vi/usage.json";
|
||||
import viApprovals from "./locales/vi/approvals.json";
|
||||
import viNodes from "./locales/vi/nodes.json";
|
||||
import viLogs from "./locales/vi/logs.json";
|
||||
import viTools from "./locales/vi/tools.json";
|
||||
import viMcp from "./locales/vi/mcp.json";
|
||||
import viTts from "./locales/vi/tts.json";
|
||||
import viSetup from "./locales/vi/setup.json";
|
||||
import viMemory from "./locales/vi/memory.json";
|
||||
import viStorage from "./locales/vi/storage.json";
|
||||
import viPendingMessages from "./locales/vi/pending-messages.json";
|
||||
|
||||
// --- ZH namespaces ---
|
||||
import zhCommon from "./locales/zh/common.json";
|
||||
import zhSidebar from "./locales/zh/sidebar.json";
|
||||
import zhTopbar from "./locales/zh/topbar.json";
|
||||
import zhLogin from "./locales/zh/login.json";
|
||||
import zhOverview from "./locales/zh/overview.json";
|
||||
import zhChat from "./locales/zh/chat.json";
|
||||
import zhAgents from "./locales/zh/agents.json";
|
||||
import zhTeams from "./locales/zh/teams.json";
|
||||
import zhSessions from "./locales/zh/sessions.json";
|
||||
import zhSkills from "./locales/zh/skills.json";
|
||||
import zhCron from "./locales/zh/cron.json";
|
||||
import zhConfig from "./locales/zh/config.json";
|
||||
import zhChannels from "./locales/zh/channels.json";
|
||||
import zhProviders from "./locales/zh/providers.json";
|
||||
import zhTraces from "./locales/zh/traces.json";
|
||||
import zhEvents from "./locales/zh/events.json";
|
||||
import zhDelegations from "./locales/zh/delegations.json";
|
||||
import zhUsage from "./locales/zh/usage.json";
|
||||
import zhApprovals from "./locales/zh/approvals.json";
|
||||
import zhNodes from "./locales/zh/nodes.json";
|
||||
import zhLogs from "./locales/zh/logs.json";
|
||||
import zhTools from "./locales/zh/tools.json";
|
||||
import zhMcp from "./locales/zh/mcp.json";
|
||||
import zhTts from "./locales/zh/tts.json";
|
||||
import zhSetup from "./locales/zh/setup.json";
|
||||
import zhMemory from "./locales/zh/memory.json";
|
||||
import zhStorage from "./locales/zh/storage.json";
|
||||
import zhPendingMessages from "./locales/zh/pending-messages.json";
|
||||
|
||||
const STORAGE_KEY = "goclaw:language";
|
||||
|
||||
function getInitialLanguage(): string {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored === "en" || stored === "vi" || stored === "zh") return stored;
|
||||
const lang = navigator.language.toLowerCase();
|
||||
if (lang.startsWith("vi")) return "vi";
|
||||
if (lang.startsWith("zh")) return "zh";
|
||||
return "en";
|
||||
}
|
||||
|
||||
const ns = [
|
||||
"common", "sidebar", "topbar", "login", "overview", "chat",
|
||||
"agents", "teams", "sessions", "skills", "cron", "config",
|
||||
"channels", "providers", "traces", "events", "delegations",
|
||||
"usage", "approvals", "nodes", "logs", "tools", "mcp", "tts",
|
||||
"setup", "memory", "storage", "pending-messages",
|
||||
] as const;
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
en: {
|
||||
common: enCommon, sidebar: enSidebar, topbar: enTopbar, login: enLogin,
|
||||
overview: enOverview, chat: enChat, agents: enAgents, teams: enTeams,
|
||||
sessions: enSessions, skills: enSkills, cron: enCron, config: enConfig,
|
||||
channels: enChannels, providers: enProviders, traces: enTraces,
|
||||
events: enEvents, delegations: enDelegations, usage: enUsage,
|
||||
approvals: enApprovals, nodes: enNodes, logs: enLogs, tools: enTools,
|
||||
mcp: enMcp, tts: enTts, setup: enSetup, memory: enMemory, storage: enStorage,
|
||||
"pending-messages": enPendingMessages,
|
||||
},
|
||||
vi: {
|
||||
common: viCommon, sidebar: viSidebar, topbar: viTopbar, login: viLogin,
|
||||
overview: viOverview, chat: viChat, agents: viAgents, teams: viTeams,
|
||||
sessions: viSessions, skills: viSkills, cron: viCron, config: viConfig,
|
||||
channels: viChannels, providers: viProviders, traces: viTraces,
|
||||
events: viEvents, delegations: viDelegations, usage: viUsage,
|
||||
approvals: viApprovals, nodes: viNodes, logs: viLogs, tools: viTools,
|
||||
mcp: viMcp, tts: viTts, setup: viSetup, memory: viMemory, storage: viStorage,
|
||||
"pending-messages": viPendingMessages,
|
||||
},
|
||||
zh: {
|
||||
common: zhCommon, sidebar: zhSidebar, topbar: zhTopbar, login: zhLogin,
|
||||
overview: zhOverview, chat: zhChat, agents: zhAgents, teams: zhTeams,
|
||||
sessions: zhSessions, skills: zhSkills, cron: zhCron, config: zhConfig,
|
||||
channels: zhChannels, providers: zhProviders, traces: zhTraces,
|
||||
events: zhEvents, delegations: zhDelegations, usage: zhUsage,
|
||||
approvals: zhApprovals, nodes: zhNodes, logs: zhLogs, tools: zhTools,
|
||||
mcp: zhMcp, tts: zhTts, setup: zhSetup, memory: zhMemory, storage: zhStorage,
|
||||
"pending-messages": zhPendingMessages,
|
||||
},
|
||||
},
|
||||
ns: [...ns],
|
||||
defaultNS: "common",
|
||||
lng: getInitialLanguage(),
|
||||
fallbackLng: "en",
|
||||
interpolation: { escapeValue: false },
|
||||
missingKeyHandler: import.meta.env.DEV
|
||||
? (_lngs, _ns, key) => console.warn(`[i18n] missing: ${key}`)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
i18n.on("languageChanged", (lng) => {
|
||||
localStorage.setItem(STORAGE_KEY, lng);
|
||||
document.documentElement.lang = lng;
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,342 @@
|
||||
{
|
||||
"title": "Agents",
|
||||
"description": "Manage your AI agents",
|
||||
"createAgent": "Create Agent",
|
||||
"searchPlaceholder": "Search agents...",
|
||||
"emptyTitle": "No agents yet",
|
||||
"emptyDescription": "Create your first agent to get started.",
|
||||
"noMatchTitle": "No matching agents",
|
||||
"noMatchDescription": "Try a different search term.",
|
||||
"card": {
|
||||
"unnamedAgent": "Unnamed Agent",
|
||||
"summoning": "Summoning...",
|
||||
"summonFailed": "Failed",
|
||||
"resummon": "Resummon",
|
||||
"delete": "Delete",
|
||||
"predefinedTooltip": "Shared context files (SOUL.md, IDENTITY.md, AGENTS.md) with per-user USER.md instances.",
|
||||
"openTooltip": "Each user gets their own full set of context files.",
|
||||
"evolving": "Evolving",
|
||||
"static": "Static",
|
||||
"evolvingTooltip": "Agent can self-evolve its communication style (SOUL.md). Identity and workflow stay fixed.",
|
||||
"staticTooltip": "Agent style is static. Enable Self-Evolution in settings to allow style adaptation."
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete Agent",
|
||||
"description": "Permanently delete this agent, all its context files, sessions, and configuration. This action cannot be undone.",
|
||||
"detailDescription": "Are you sure you want to delete \"{{name}}\"? All context files, sessions, and configuration will be permanently removed.",
|
||||
"confirmLabel": "Delete",
|
||||
"defaultCannotDelete": "The default agent cannot be deleted."
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Agent",
|
||||
"displayName": "Display Name *",
|
||||
"displayNamePlaceholder": "My Agent",
|
||||
"agentKey": "Agent Key *",
|
||||
"agentKeyPlaceholder": "e.g. my-agent",
|
||||
"agentKeyHint": "Lowercase, numbers, hyphens",
|
||||
"provider": "Provider *",
|
||||
"selectProvider": "Select provider",
|
||||
"model": "Model *",
|
||||
"enterOrSelectModel": "Enter or select model",
|
||||
"loadingModels": "Loading models...",
|
||||
"check": "Check",
|
||||
"checking": "Checking...",
|
||||
"modelVerified": "Model verified",
|
||||
"verificationFailed": "Verification failed",
|
||||
"noModelsHint": "This provider doesn't list models — type the model ID manually.",
|
||||
"agentType": "Agent Type",
|
||||
"open": "Open",
|
||||
"openSubLabel": "Per-user context",
|
||||
"predefined": "Predefined",
|
||||
"predefinedSubLabel": "Agent-level config",
|
||||
"describeAgent": "Describe Your Agent",
|
||||
"descriptionPlaceholder": "Describe your agent's personality, purpose, and behavior...",
|
||||
"descriptionHint": "AI will automatically generate your agent's context files from this description. Leave empty to start with templates.",
|
||||
"selfEvolution": "Self-Evolution",
|
||||
"selfEvolutionHint": "Allow agent to evolve its style and tone over time via SOUL.md",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"creating": "Creating...",
|
||||
"checkAndCreate": "Check & Create",
|
||||
"failedToCreate": "Failed to create agent"
|
||||
},
|
||||
"detail": {
|
||||
"tabs": {
|
||||
"general": "General",
|
||||
"config": "Config",
|
||||
"files": "Files",
|
||||
"shares": "Shares",
|
||||
"links": "Links",
|
||||
"skills": "Skills",
|
||||
"instances": "Instances"
|
||||
},
|
||||
"summonFailed": "Summon Failed",
|
||||
"evolving": "Evolving",
|
||||
"static": "Static",
|
||||
"evolvingTooltipDetail": "Agent can self-evolve its communication style (SOUL.md). Identity and workflow stay fixed.",
|
||||
"staticTooltipDetail": "Agent style is static. Enable Self-Evolution in General tab to allow style adaptation."
|
||||
},
|
||||
"general": {
|
||||
"selfEvolution": "Self-Evolution",
|
||||
"selfEvolutionLabel": "Allow agent to evolve its communication style",
|
||||
"selfEvolutionHint": "When enabled, the agent can update its SOUL.md to refine tone, vocabulary, and response style based on interactions. Identity, name, and operating instructions remain locked.",
|
||||
"selfEvolutionInfo": "Agent will evolve its style over time through SOUL.md updates. Only style and tone are affected — identity and workflow rules stay fixed.",
|
||||
"saveChanges": "Save Changes",
|
||||
"saving": "Saving...",
|
||||
"saved": "Saved",
|
||||
"failedToSave": "Failed to save"
|
||||
},
|
||||
"identity": {
|
||||
"title": "Identity",
|
||||
"agentKey": "Agent Key",
|
||||
"agentKeyHint": "Unique identifier, cannot be changed.",
|
||||
"displayName": "Display Name",
|
||||
"displayNamePlaceholder": "e.g. My Assistant",
|
||||
"displayNameHint": "Friendly name shown in the UI. Leave empty to use the agent key.",
|
||||
"expertiseSummary": "Expertise Summary",
|
||||
"expertiseSummaryPlaceholder": "e.g. Chiêm tinh, bói toán, tử vi, thần số học",
|
||||
"expertiseSummaryHint": "Short description of this agent's expertise. Used for delegation discovery.",
|
||||
"status": "Status",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"summonFailed": "Summon Failed",
|
||||
"defaultAgent": "Default Agent"
|
||||
},
|
||||
"llmConfig": {
|
||||
"title": "LLM Configuration",
|
||||
"contextWindow": "Context Window",
|
||||
"contextWindowHint": "Token limit for the model context.",
|
||||
"maxToolIterations": "Max Tool Iterations",
|
||||
"maxToolIterationsHint": "Max tool calls per request."
|
||||
},
|
||||
"workspace": {
|
||||
"title": "Workspace",
|
||||
"workspacePath": "Workspace Path",
|
||||
"workspacePathHint": "Automatically assigned when the agent is created. Per-user subdirectories are created at runtime.",
|
||||
"noWorkspace": "No workspace configured",
|
||||
"restrictToWorkspace": "Restrict to Workspace",
|
||||
"restrictToWorkspaceHint": "Confine file access strictly within the workspace path."
|
||||
},
|
||||
"config": {
|
||||
"saveConfig": "Save Config",
|
||||
"saving": "Saving...",
|
||||
"saved": "Saved",
|
||||
"failedToSave": "Failed to save",
|
||||
"usingGlobalDefaults": "Using global defaults from config.json"
|
||||
},
|
||||
"files": {
|
||||
"readOnly": "Read-only",
|
||||
"readOnlyDesc": "Only the agent owner can edit predefined context files.",
|
||||
"resummon": "Resummon",
|
||||
"editWithAi": "Edit with AI",
|
||||
"resummonTitle": "Resummon Agent",
|
||||
"resummonDesc": "This will regenerate SOUL.md, IDENTITY.md, and optionally USER_PREDEFINED.md from scratch using the original description. Any manual edits to these files will be overwritten.",
|
||||
"cancel": "Cancel",
|
||||
"resummonConfirm": "Resummon",
|
||||
"summoning": "Summoning...",
|
||||
"selectFileToEdit": "Select a file to edit",
|
||||
"selectFileToView": "Select a file to view",
|
||||
"saving": "Saving...",
|
||||
"save": "Save",
|
||||
"loading": "Loading...",
|
||||
"openAgentTitle": "Open Agent - Per-User Context Files",
|
||||
"openAgentDesc1": "This is an open agent. Context files (AGENTS.md, SOUL.md, TOOLS.md, etc.) are personalized for each user. They are automatically created from templates when a user first chats with this agent.",
|
||||
"openAgentDesc2pre": "Agent-level files shown here are empty because open agents store all context per-user in the ",
|
||||
"openAgentDesc2post": " table.",
|
||||
"contextFiles": "Context Files",
|
||||
"contextFile": "Context file",
|
||||
"perUser": "per-user",
|
||||
"emptyFile": "empty",
|
||||
"estTokens": "Est {{tokens}} tokens"
|
||||
},
|
||||
"fileEditor": {
|
||||
"editWithAi": "Edit with AI",
|
||||
"editAiDescription": "Describe what you want to change. AI will read the current files and update them accordingly.",
|
||||
"editAiPlaceholder": "e.g. Make the agent more formal, add Vietnamese language support, change the name to Luna...",
|
||||
"sending": "Sending...",
|
||||
"regenerate": "Regenerate",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"shares": {
|
||||
"grantAccess": "Grant Access",
|
||||
"userId": "User ID",
|
||||
"userIdPlaceholder": "Enter user ID...",
|
||||
"role": "Role",
|
||||
"share": "Share",
|
||||
"loadingShares": "Loading shares...",
|
||||
"noShares": "No shares yet",
|
||||
"noSharesDesc": "Share this agent with other users by entering their User ID above.",
|
||||
"user": "User",
|
||||
"revokeTitle": "Revoke Share",
|
||||
"revokeDesc": "Revoke access for user \"{{userId}}\"? They will no longer be able to use this agent.",
|
||||
"revoke": "Revoke"
|
||||
},
|
||||
"links": {
|
||||
"teamsHint": "For multi-agent collaboration with task tracking and quality control, consider using Agent Teams instead. Agent Links are best for simple 1-to-1 delegation between two agents.",
|
||||
"createLink": "Create Link",
|
||||
"targetAgent": "Target Agent",
|
||||
"selectAgent": "Select or search agent...",
|
||||
"direction": "Direction",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Optional description of the link...",
|
||||
"maxConcurrent": "Max Concurrent",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"allowedUsers": "Allowed Users",
|
||||
"allowedUsersPlaceholder": "user1, user2, ...",
|
||||
"allowedUsersHint": "If set, only these users can trigger this delegation. Comma-separated.",
|
||||
"deniedUsers": "Denied Users",
|
||||
"deniedUsersPlaceholder": "user3, user4, ...",
|
||||
"deniedUsersHint": "These users are blocked from triggering this delegation. Comma-separated.",
|
||||
"creating": "Creating...",
|
||||
"noLinks": "No agent links yet",
|
||||
"noLinksDesc": "Create links to allow agents to delegate tasks to each other.",
|
||||
"loadingLinks": "Loading links...",
|
||||
"columns": {
|
||||
"target": "Target",
|
||||
"direction": "Direction",
|
||||
"status": "Status",
|
||||
"limit": "Limit"
|
||||
},
|
||||
"deleteTitle": "Delete Link",
|
||||
"deleteDesc": "Remove the delegation link to \"{{name}}\"? Agents will no longer be able to delegate to each other through this link.",
|
||||
"editTitle": "Edit Link",
|
||||
"editDescription": "Update the delegation link settings.",
|
||||
"optionalDescription": "Optional description...",
|
||||
"allowedUsersHintShort": "If set, only these users can trigger this delegation.",
|
||||
"deniedUsersHintShort": "These users are blocked from triggering this delegation."
|
||||
},
|
||||
"skills": {
|
||||
"noSkillsAvailable": "No skills available",
|
||||
"noSkillsDesc": "Upload skills in the Skills page to grant them to agents.",
|
||||
"skillsGranted": "{{granted}} of {{total}} skills granted",
|
||||
"filterSkills": "Filter skills...",
|
||||
"noSkillsMatch": "No skills match your search."
|
||||
},
|
||||
"instances": {
|
||||
"loadingInstances": "Loading instances...",
|
||||
"noInstances": "No user instances yet.",
|
||||
"noInstancesDesc": "Instances are created when users interact with this agent.",
|
||||
"instanceCount": "{{count}} instance",
|
||||
"instanceCount_plural": "{{count}} instances",
|
||||
"selectInstance": "Select an instance to view and edit its USER.md",
|
||||
"loading": "Loading...",
|
||||
"saving": "Saving...",
|
||||
"save": "Save",
|
||||
"saved": "Saved"
|
||||
},
|
||||
"summoning": {
|
||||
"title": "Summoning Your Agent...",
|
||||
"completed": "Summoning Complete!",
|
||||
"failed": "Summoning Failed",
|
||||
"agentReady": "{{name}} is ready!",
|
||||
"weavingSoul": "Weaving the soul of",
|
||||
"wait": "This usually takes few minutes. Please wait...",
|
||||
"continue": "Continue",
|
||||
"retry": "Retry",
|
||||
"retrying": "Retrying...",
|
||||
"done": "done"
|
||||
},
|
||||
"configSections": {
|
||||
"subagents": {
|
||||
"title": "Subagents",
|
||||
"description": "Controls sub-agent spawning limits and behavior",
|
||||
"maxConcurrent": "Max Concurrent",
|
||||
"maxSpawnDepth": "Max Spawn Depth",
|
||||
"maxChildrenPerAgent": "Max Children Per Agent",
|
||||
"archiveAfter": "Archive After (minutes)",
|
||||
"modelOverride": "Model Override",
|
||||
"inheritFromAgent": "(inherit from agent)"
|
||||
},
|
||||
"toolPolicy": {
|
||||
"title": "Tool Policy",
|
||||
"description": "Control which tools this agent can use",
|
||||
"profile": "Profile",
|
||||
"allow": "Allow",
|
||||
"deny": "Deny",
|
||||
"alsoAllow": "Also Allow",
|
||||
"selectToolsAllow": "Select tools to allow...",
|
||||
"selectToolsDeny": "Select tools to deny...",
|
||||
"selectToolsAlsoAllow": "Select additional tools..."
|
||||
},
|
||||
"compaction": {
|
||||
"title": "Compaction",
|
||||
"description": "Context window compaction and memory flush settings",
|
||||
"reserveTokensFloor": "Reserve Tokens Floor",
|
||||
"maxHistoryShare": "Max History Share (0-1)",
|
||||
"minMessages": "Min Messages",
|
||||
"keepLastMessages": "Keep Last Messages",
|
||||
"memoryFlush": "Memory Flush",
|
||||
"softThreshold": "Soft Threshold (tokens)"
|
||||
},
|
||||
"contextPruning": {
|
||||
"title": "Context Pruning",
|
||||
"description": "Trim old tool results to save context window",
|
||||
"mode": "Mode",
|
||||
"keepLastAssistants": "Keep Last Assistants",
|
||||
"softTrimRatio": "Soft Trim Ratio (0-1)",
|
||||
"hardClearRatio": "Hard Clear Ratio (0-1)",
|
||||
"minPrunableToolChars": "Min Prunable Tool Chars",
|
||||
"softTrim": "Soft Trim",
|
||||
"maxChars": "Max Chars",
|
||||
"headChars": "Head Chars",
|
||||
"tailChars": "Tail Chars",
|
||||
"hardClear": "Hard Clear",
|
||||
"enabled": "Enabled",
|
||||
"placeholderText": "Placeholder Text"
|
||||
},
|
||||
"sandbox": {
|
||||
"title": "Sandbox",
|
||||
"description": "Docker sandbox for code execution isolation",
|
||||
"mode": "Mode",
|
||||
"workspaceAccess": "Workspace Access",
|
||||
"image": "Image",
|
||||
"scope": "Scope",
|
||||
"timeout": "Timeout (sec)",
|
||||
"memoryMb": "Memory (MB)",
|
||||
"cpus": "CPUs",
|
||||
"networkEnabled": "Network Enabled"
|
||||
},
|
||||
"memory": {
|
||||
"title": "Memory",
|
||||
"description": "Semantic memory search and embedding configuration",
|
||||
"embeddingProvider": "Embedding Provider",
|
||||
"embeddingModel": "Embedding Model",
|
||||
"maxResults": "Max Results",
|
||||
"maxChunkLen": "Max Chunk Length",
|
||||
"vectorWeight": "Vector Weight",
|
||||
"textWeight": "Text Weight"
|
||||
},
|
||||
"thinking": {
|
||||
"title": "Extended Thinking",
|
||||
"description": "Allow the model to reason before responding. Higher levels use more tokens but produce better results on complex tasks.",
|
||||
"thinkingLevel": "Thinking Level",
|
||||
"off": "Off",
|
||||
"offDesc": "No extended thinking",
|
||||
"low": "Low",
|
||||
"lowDesc": "~4K token budget",
|
||||
"medium": "Medium",
|
||||
"mediumDesc": "~10-16K token budget",
|
||||
"high": "High",
|
||||
"highDesc": "~32K token budget"
|
||||
},
|
||||
"qualityGates": {
|
||||
"title": "Quality Gates",
|
||||
"description": "Validate delegation output before returning to the caller",
|
||||
"noGates": "No quality gates configured. Click \"Add Gate\" to create one.",
|
||||
"addGate": "Add Gate",
|
||||
"gateNumber": "Gate #{{number}}",
|
||||
"type": "Type",
|
||||
"reviewerAgentKey": "Reviewer Agent Key",
|
||||
"command": "Command",
|
||||
"blockOnFailure": "Block on Failure",
|
||||
"maxRetries": "Max Retries",
|
||||
"timeout": "Timeout (seconds)"
|
||||
},
|
||||
"otherConfig": {
|
||||
"title": "Other Config",
|
||||
"description": "Additional JSON config for advanced settings not covered by other sections",
|
||||
"formatJson": "Format JSON",
|
||||
"invalidJson": "Invalid JSON syntax"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"title": "Approvals",
|
||||
"description": "Pending execution approvals",
|
||||
"pending": "{{count}} pending",
|
||||
"emptyTitle": "No pending approvals",
|
||||
"emptyDescription": "All execution requests have been resolved. New requests will appear here in real-time.",
|
||||
"allowOnce": "Allow Once",
|
||||
"allowAlways": "Allow Always",
|
||||
"deny": "Deny",
|
||||
"confirmAllowOnce": {
|
||||
"title": "Allow Once",
|
||||
"description": "Allow execution of \"{{command}}\" from agent \"{{agentId}}\"?"
|
||||
},
|
||||
"confirmAllowAlways": {
|
||||
"title": "Allow Always",
|
||||
"description": "Permanently allow \"{{command}}\" for agent \"{{agentId}}\"? This command will be auto-approved in the future."
|
||||
},
|
||||
"confirmDeny": {
|
||||
"title": "Deny Execution",
|
||||
"description": "Deny execution request \"{{command}}\" from agent \"{{agentId}}\"?",
|
||||
"confirmLabel": "Deny"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"title": "Channels",
|
||||
"description": "Manage channel instances",
|
||||
"statusDescription": "Communication channel status",
|
||||
"addChannel": "Add Channel",
|
||||
"refresh": "Refresh",
|
||||
"searchPlaceholder": "Search channels...",
|
||||
"emptyTitle": "No channels",
|
||||
"emptyDescription": "Add your first channel instance to get started.",
|
||||
"emptyStatusDescription": "No communication channels are configured.",
|
||||
"noMatchTitle": "No matching channels",
|
||||
"noMatchDescription": "Try a different search term.",
|
||||
"columns": {
|
||||
"name": "Name",
|
||||
"type": "Type",
|
||||
"agent": "Agent",
|
||||
"status": "Status",
|
||||
"enabled": "Enabled",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"status": {
|
||||
"running": "Running",
|
||||
"stopped": "Stopped"
|
||||
},
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"actions": {
|
||||
"reauthenticate": "Re-authenticate",
|
||||
"authenticate": "Authenticate to start channel"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete Channel Instance",
|
||||
"description": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
|
||||
"confirmLabel": "Delete"
|
||||
},
|
||||
"form": {
|
||||
"createTitle": "Create Channel Instance",
|
||||
"editTitle": "Edit Channel Instance",
|
||||
"key": "Key *",
|
||||
"keyPlaceholder": "my-telegram-bot",
|
||||
"keyHint": "Unique slug used as channel identifier",
|
||||
"displayName": "Display Name",
|
||||
"displayNamePlaceholder": "Sales Bot",
|
||||
"channelType": "Channel Type *",
|
||||
"agent": "Agent *",
|
||||
"selectAgent": "Select agent",
|
||||
"credentials": "Credentials",
|
||||
"credentialsHint": "(leave blank to keep current)",
|
||||
"credentialsEncrypted": "Encrypted server-side. Never returned in API responses.",
|
||||
"configuration": "Configuration",
|
||||
"enabled": "Enabled",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"update": "Update",
|
||||
"saving": "Saving...",
|
||||
"step": "Step {{current}} of {{total}}",
|
||||
"authenticate": "Authenticate — {{label}}",
|
||||
"configure": "Configure — {{label}}",
|
||||
"skip": "Skip",
|
||||
"done": "Done",
|
||||
"errors": {
|
||||
"keyRequired": "Key is required",
|
||||
"keySlug": "Key must be a valid slug (lowercase letters, numbers, hyphens only)",
|
||||
"agentRequired": "Agent is required",
|
||||
"requiredFields": "Required: {{fields}}",
|
||||
"failedSave": "Failed to save",
|
||||
"failedSaveConfig": "Failed to save configuration"
|
||||
},
|
||||
"authStatus": {
|
||||
"authenticated": "Authenticated",
|
||||
"notAuthenticated": "Not authenticated",
|
||||
"useQrHint": "— Use the QR login button from the channels table"
|
||||
}
|
||||
},
|
||||
"detail": {
|
||||
"agent": "Agent: {{name}}",
|
||||
"tabs": {
|
||||
"general": "General",
|
||||
"config": "Config",
|
||||
"credentials": "Credentials",
|
||||
"groups": "Groups",
|
||||
"writers": "Writers"
|
||||
},
|
||||
"general": {
|
||||
"name": "Name",
|
||||
"nameHint": "Unique slug (read-only)",
|
||||
"channelType": "Channel Type",
|
||||
"displayName": "Display Name",
|
||||
"displayNamePlaceholder": "Friendly name",
|
||||
"agent": "Agent",
|
||||
"selectAgent": "Select agent",
|
||||
"enabled": "Enabled",
|
||||
"saved": "Saved",
|
||||
"saveChanges": "Save Changes",
|
||||
"saving": "Saving..."
|
||||
},
|
||||
"credentials": {
|
||||
"hint": "Leave fields blank to keep current values. Credentials are encrypted server-side and never returned in API responses.",
|
||||
"noSchema": "No credentials schema for this channel type.",
|
||||
"noCredentials": "No credentials to update",
|
||||
"failedSave": "Failed to save",
|
||||
"saved": "Saved",
|
||||
"updateCredentials": "Update Credentials",
|
||||
"saving": "Saving..."
|
||||
},
|
||||
"config": {
|
||||
"noSchema": "No configuration schema for this channel type.",
|
||||
"saved": "Saved",
|
||||
"saveConfig": "Save Config",
|
||||
"saving": "Saving..."
|
||||
},
|
||||
"groups": {
|
||||
"saved": "Saved",
|
||||
"saveGroups": "Save Groups",
|
||||
"saving": "Saving..."
|
||||
},
|
||||
"writers": {
|
||||
"description": "Manage which users are allowed to write/edit files in group chats.",
|
||||
"groups": "Groups",
|
||||
"noWriterGroups": "No writer groups",
|
||||
"noWriterGroupsHint": "Use the form below to add writers to a group.",
|
||||
"loadingWriters": "Loading writers...",
|
||||
"noWriters": "No writers in this group.",
|
||||
"writersCount": "{{count}} writer",
|
||||
"writersCountPlural": "{{count}} writers",
|
||||
"columns": {
|
||||
"userId": "User ID",
|
||||
"name": "Name",
|
||||
"username": "Username"
|
||||
},
|
||||
"addForm": {
|
||||
"title": "Add Writer to New Group",
|
||||
"hint": "Add a writer to a group that doesn't appear in the list above.",
|
||||
"groupId": "Group ID *",
|
||||
"groupIdPlaceholder": "e.g. group:telegram:-100123456",
|
||||
"userId": "User ID *",
|
||||
"userIdPlaceholder": "e.g. 12345678",
|
||||
"displayName": "Display Name",
|
||||
"username": "Username",
|
||||
"optional": "Optional",
|
||||
"usernameWithout": "Optional (without @)",
|
||||
"adding": "Adding...",
|
||||
"addWriter": "Add Writer",
|
||||
"add": "Add",
|
||||
"errors": {
|
||||
"groupUserRequired": "Group ID and User ID are required",
|
||||
"failedAdd": "Failed to add writer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"groupOverrides": {
|
||||
"title": "Group & Topic Overrides",
|
||||
"hint": "Override channel defaults per group chat or forum topic. Use \"*\" as group ID for wildcard defaults.",
|
||||
"groupLabel": "Group: {{id}}",
|
||||
"groupWildcard": "* (wildcard)",
|
||||
"addGroupPlaceholder": "Chat ID (e.g. -100123456) or *",
|
||||
"addGroup": "Add Group",
|
||||
"topicOverrides": "Topic Overrides",
|
||||
"topicLabel": "Topic: {{id}}",
|
||||
"addTopicPlaceholder": "Topic thread ID",
|
||||
"addTopic": "Add Topic",
|
||||
"fields": {
|
||||
"groupPolicy": "Group Policy",
|
||||
"requireMention": "Require @mention",
|
||||
"enabled": "Enabled",
|
||||
"allowedUsers": "Allowed Users",
|
||||
"allowedUsersPlaceholder": "One user ID per line",
|
||||
"skillsFilter": "Skills Filter",
|
||||
"skillsPlaceholder": "One skill name per line (empty = inherit)",
|
||||
"toolAllow": "Tool Allow",
|
||||
"toolAllowPlaceholder": "Select allowed tools (empty = inherit all)...",
|
||||
"toolAllowHint": "Restrict which tools are available in this group. Supports group names like \"group:web\", \"group:fs\". Empty = inherit all tools.",
|
||||
"systemPrompt": "System Prompt",
|
||||
"systemPromptPlaceholder": "Additional system prompt for this group/topic",
|
||||
"inherit": "Inherit (channel default)",
|
||||
"yes": "Yes",
|
||||
"no": "No"
|
||||
}
|
||||
},
|
||||
"toast": {
|
||||
"created": "Channel created",
|
||||
"createdDesc": "{{name}} has been added",
|
||||
"updated": "Channel updated",
|
||||
"deleted": "Channel deleted",
|
||||
"failedCreate": "Failed to create channel",
|
||||
"failedUpdate": "Failed to update channel",
|
||||
"failedDelete": "Failed to delete channel"
|
||||
},
|
||||
"zalo": {
|
||||
"allowedUsers": "Allowed Users",
|
||||
"loadContacts": "Load Contacts",
|
||||
"loading": "Loading...",
|
||||
"searchContacts": "Search contacts...",
|
||||
"friends": "Friends",
|
||||
"groups": "Groups",
|
||||
"membersCount": "{{count}} members",
|
||||
"noContacts": "No contacts found",
|
||||
"noContactsMatch": "No contacts match \"{{search}}\"",
|
||||
"addManualPlaceholder": "Add ID manually",
|
||||
"add": "Add",
|
||||
"zaloIdsHint": "Zalo user IDs or group IDs",
|
||||
"completeQrLogin": "Complete QR login to load contacts",
|
||||
"loginQr": "Login with QR — {{name}}",
|
||||
"loginSuccessful": "Login successful! Channel starting...",
|
||||
"generatingQr": "Generating QR code...",
|
||||
"scanHint": "Scan with your Zalo app (expires in ~100s)",
|
||||
"loginSuccessLoading": "Login successful! Loading contacts...",
|
||||
"retry": "Retry",
|
||||
"close": "Close",
|
||||
"skip": "Skip"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"newChat": "New Chat",
|
||||
"openSessions": "Open sessions",
|
||||
"readOnly": "Read-only — this session belongs to another user",
|
||||
"empty": {
|
||||
"title": "Start a conversation",
|
||||
"description": "Send a message to begin chatting with the agent."
|
||||
},
|
||||
"media": {
|
||||
"image": "Image",
|
||||
"video": "Video",
|
||||
"audio": "Audio",
|
||||
"voice": "Voice message",
|
||||
"document": "Document",
|
||||
"animation": "Animation",
|
||||
"attached": "attached"
|
||||
},
|
||||
"forwardedFrom": "Forwarded from",
|
||||
"replyingTo": "Replying to"
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"refresh": "Refresh",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"delete": "Delete",
|
||||
"save": "Save",
|
||||
"edit": "Edit",
|
||||
"create": "Create",
|
||||
"add": "Add",
|
||||
"view": "View",
|
||||
"filter": "Filter",
|
||||
"search": "Search",
|
||||
"retry": "Retry",
|
||||
"manage": "Manage",
|
||||
"upload": "Upload",
|
||||
"close": "Close",
|
||||
"back": "Back",
|
||||
"update": "Update",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"loading": "Loading...",
|
||||
"saving": "Saving...",
|
||||
"creating": "Creating...",
|
||||
"connecting": "Connecting...",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"pending": "Pending",
|
||||
"completed": "Completed",
|
||||
"failed": "Failed",
|
||||
"cancelled": "Cancelled",
|
||||
"unknown": "Unknown",
|
||||
"active": "active",
|
||||
"items": "items",
|
||||
"rows": "Rows",
|
||||
"pageOf": "Page {{page}} of {{totalPages}}",
|
||||
"messages": "messages",
|
||||
"cached": "cached",
|
||||
"live": "Live",
|
||||
"noDescription": "No description",
|
||||
"unsavedChanges": "Unsaved changes",
|
||||
"tryDifferentSearch": "Try a different search term.",
|
||||
"notSet": "Not set",
|
||||
"default": "default",
|
||||
"optional": "optional",
|
||||
"required": "required",
|
||||
"actions": "Actions",
|
||||
"status": "Status",
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"type": "Type",
|
||||
"none": "none",
|
||||
"all": "All",
|
||||
"global": "Global",
|
||||
"agent": "Agent",
|
||||
"saved": "Saved",
|
||||
"saveChanges": "Save Changes",
|
||||
"saveConfig": "Save Config",
|
||||
"checkAndCreate": "Check & Create",
|
||||
"checking": "Checking...",
|
||||
"format": "Format JSON",
|
||||
"invalidJson": "Invalid JSON syntax",
|
||||
"usingGlobalDefaults": "Using global defaults from config.json",
|
||||
"disconnectedGateway": "Disconnected from gateway. Attempting to reconnect...",
|
||||
"noFiles": "No files found.",
|
||||
"selectFileToView": "Select a file to view its content",
|
||||
"typeToConfirmPrefix": "Type",
|
||||
"typeToConfirmSuffix": "to confirm.",
|
||||
"searchPlaceholder": "Search...",
|
||||
"noSessions": "No sessions yet. Start a chat!",
|
||||
"noAgentsAvailable": "No agents available",
|
||||
"selectAgent": "Select agent",
|
||||
"sendMessage": "Send a message... (Shift+Enter for new line)",
|
||||
"stopGeneration": "Stop generation",
|
||||
"sendMessageTitle": "Send message",
|
||||
"thinking": "Thinking",
|
||||
"thinkingStreaming": "Thinking...",
|
||||
"toolArguments": "Arguments",
|
||||
"toolResult": "Result",
|
||||
"skillActivating": "Activating...",
|
||||
"skillActivated": "Activated",
|
||||
"toolRunning": "Running...",
|
||||
"toolDone": "Done",
|
||||
"toolFailed": "Failed",
|
||||
"filesBack": "Files",
|
||||
"noProvidersConfigured": "No providers configured",
|
||||
"loadingModels": "Loading models...",
|
||||
"modelVerified": "Model verified",
|
||||
"verificationFailed": "Verification failed",
|
||||
"provider": "Provider",
|
||||
"model": "Model",
|
||||
"selectProvider": "Select provider",
|
||||
"enterOrSelectModel": "Enter or select model",
|
||||
"providerTip": "LLM provider name. Must match a configured provider.",
|
||||
"modelTip": "Model ID to use.",
|
||||
"check": "Check",
|
||||
"builtinTools": "Built-in",
|
||||
"customTools": "Custom",
|
||||
"selectOrTypeTools": "Select or type tool names...",
|
||||
"typeAndPressEnter": "Type and press Enter..."
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"title": "Cron",
|
||||
"description": "Schedule recurring agent tasks",
|
||||
"newJob": "New Job",
|
||||
"emptyTitle": "No cron jobs",
|
||||
"emptyDescription": "Create a cron job to schedule recurring agent tasks.",
|
||||
"columns": {
|
||||
"enabled": "Enabled",
|
||||
"name": "Name",
|
||||
"schedule": "Schedule",
|
||||
"message": "Message",
|
||||
"agent": "Agent",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"runNow": "Run now",
|
||||
"running": "Running...",
|
||||
"runHistory": "Run history",
|
||||
"defaultAgent": "default",
|
||||
"create": {
|
||||
"title": "Create Cron Job",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "my-daily-task",
|
||||
"nameHint": "Lowercase letters, numbers, and hyphens only",
|
||||
"agentId": "Agent ID (optional)",
|
||||
"agentIdPlaceholder": "default",
|
||||
"scheduleType": "Schedule Type",
|
||||
"every": "Every",
|
||||
"cron": "Cron",
|
||||
"once": "Once",
|
||||
"intervalSeconds": "Interval (seconds)",
|
||||
"cronExpression": "Cron Expression",
|
||||
"cronHint": "Standard 5-field cron: min hour day month weekday",
|
||||
"onceDesc": "The job will run once, approximately 1 minute from now.",
|
||||
"message": "Message",
|
||||
"messagePlaceholder": "What should the agent do?",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"creating": "Creating..."
|
||||
},
|
||||
"enable": {
|
||||
"title": "Enable Cron Job",
|
||||
"description": "Enable \"{{name}}\"? It will start running on schedule.",
|
||||
"confirmLabel": "Enable"
|
||||
},
|
||||
"disable": {
|
||||
"title": "Disable Cron Job",
|
||||
"description": "Disable \"{{name}}\"? It will stop running until re-enabled.",
|
||||
"confirmLabel": "Disable"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete Cron Job",
|
||||
"description": "Delete \"{{name}}\"? This cannot be undone.",
|
||||
"confirmLabel": "Delete"
|
||||
},
|
||||
"runLog": {
|
||||
"title": "Run Log: {{name}}",
|
||||
"noHistory": "No run history yet.",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"detail": {
|
||||
"runNow": "Run Now",
|
||||
"running": "Running...",
|
||||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
"delete": "Delete",
|
||||
"enabled": "enabled",
|
||||
"disabled": "disabled",
|
||||
"runningStatus": "running...",
|
||||
"payload": "Payload",
|
||||
"empty": "(empty)",
|
||||
"deliverDirect": "Deliver: direct",
|
||||
"channel": "Channel: {{name}}",
|
||||
"to": "To: {{name}}",
|
||||
"lastError": "Last Error",
|
||||
"runHistory": "Run History",
|
||||
"refresh": "Refresh",
|
||||
"noHistory": "No run history yet.",
|
||||
"inOut": "{{input}} in / {{output}} out",
|
||||
"infoRows": {
|
||||
"schedule": "Schedule",
|
||||
"timezone": "Timezone",
|
||||
"nextRun": "Next Run",
|
||||
"lastRun": "Last Run",
|
||||
"created": "Created",
|
||||
"updated": "Updated",
|
||||
"autoDelete": "Auto-delete",
|
||||
"autoDeleteValue": "Yes (one-time)"
|
||||
}
|
||||
},
|
||||
"toast": {
|
||||
"created": "Cron job created",
|
||||
"createdDesc": "{{name}} has been added",
|
||||
"enabled": "Cron job enabled",
|
||||
"disabled": "Cron job disabled",
|
||||
"deleted": "Cron job deleted",
|
||||
"triggered": "Cron job triggered",
|
||||
"failedCreate": "Failed to create cron job",
|
||||
"failedToggle": "Failed to toggle cron job",
|
||||
"failedDelete": "Failed to delete cron job",
|
||||
"failedRun": "Failed to run cron job"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"title": "Delegations",
|
||||
"description": "Agent delegation history and results",
|
||||
"sourceFilterPlaceholder": "Source agent...",
|
||||
"targetFilterPlaceholder": "Target agent...",
|
||||
"filter": "Filter",
|
||||
"emptyTitle": "No delegations",
|
||||
"emptyDescription": "No delegation records found. Delegations are recorded when agents delegate tasks to other agents.",
|
||||
"columns": {
|
||||
"sourceTarget": "Source / Target",
|
||||
"task": "Task",
|
||||
"status": "Status",
|
||||
"mode": "Mode",
|
||||
"duration": "Duration",
|
||||
"time": "Time"
|
||||
},
|
||||
"statusFilter": {
|
||||
"all": "All",
|
||||
"pending": "Pending",
|
||||
"running": "Running",
|
||||
"completed": "Completed",
|
||||
"failed": "Failed",
|
||||
"cancelled": "Cancelled"
|
||||
},
|
||||
"detail": {
|
||||
"title": "Delegation Detail",
|
||||
"notFound": "Delegation not found.",
|
||||
"source": "Source:",
|
||||
"target": "Target:",
|
||||
"status": "Status:",
|
||||
"mode": "Mode:",
|
||||
"duration": "Duration:",
|
||||
"iterations": "Iterations:",
|
||||
"created": "Created:",
|
||||
"completed": "Completed:",
|
||||
"trace": "Trace:",
|
||||
"task": "Task",
|
||||
"result": "Result",
|
||||
"error": "Error"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"title": "Realtime Events",
|
||||
"description": "Live WebSocket events across all teams and agents",
|
||||
"live": "Live",
|
||||
"paused": "Paused",
|
||||
"resume": "Resume",
|
||||
"pause": "Pause",
|
||||
"clear": "Clear",
|
||||
"scrollToBottom": "Scroll to bottom",
|
||||
"eventCount": "{{count}} event",
|
||||
"eventCountPlural": "{{count}} events",
|
||||
"categories": {
|
||||
"all": "All",
|
||||
"delegation": "Delegation",
|
||||
"task": "Task",
|
||||
"message": "Message",
|
||||
"agent": "Agent",
|
||||
"teamCrud": "Team CRUD",
|
||||
"agentLink": "Agent Link"
|
||||
},
|
||||
"filters": {
|
||||
"allTeams": "All teams",
|
||||
"allUsers": "All users",
|
||||
"allChats": "All chats"
|
||||
},
|
||||
"global": "Global",
|
||||
"emptyTitle": "No events yet",
|
||||
"emptyPaused": "Event capture is paused. Resume to see new events.",
|
||||
"emptyWaiting": "Waiting for real-time events... Events will appear as agents work.",
|
||||
"detail": {
|
||||
"copy": "Copy",
|
||||
"copied": "Copied"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"subtitle": "Sign in to the gateway dashboard",
|
||||
"tabs": {
|
||||
"token": "Token",
|
||||
"pairing": "Pairing"
|
||||
},
|
||||
"token": {
|
||||
"userId": "User ID",
|
||||
"userIdPlaceholder": "your-user-id",
|
||||
"gatewayToken": "Gateway Token",
|
||||
"tokenPlaceholder": "Bearer token",
|
||||
"connect": "Connect",
|
||||
"connecting": "Connecting...",
|
||||
"errorInvalidCredentials": "Invalid credentials. Check your token and user ID.",
|
||||
"errorServer": "Server returned {{status}}. Please try again.",
|
||||
"errorCannotConnect": "Cannot connect to gateway. Check if the server is running."
|
||||
},
|
||||
"pairing": {
|
||||
"userId": "User ID",
|
||||
"userIdPlaceholder": "your-user-id",
|
||||
"noTokenNeeded": "No token needed. A pairing code will be generated for an admin to approve.",
|
||||
"requestAccess": "Request Access",
|
||||
"connecting": "Connecting...",
|
||||
"errorConnectionLost": "Connection lost. Please try again.",
|
||||
"errorCannotConnect": "Could not connect to gateway.",
|
||||
"askAdmin": "Ask an admin to approve this code:",
|
||||
"orRun": "Or run:",
|
||||
"waitingForApproval": "Waiting for approval...",
|
||||
"cancel": "Cancel",
|
||||
"approved": "Access approved! Redirecting..."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"title": "Logs",
|
||||
"description": "Live log tailing",
|
||||
"descriptionLive": "Live tailing at {{level}} level",
|
||||
"live": "Live",
|
||||
"start": "Start",
|
||||
"stop": "Stop",
|
||||
"clear": "Clear",
|
||||
"filterPlaceholder": "Filter logs...",
|
||||
"logLevelTitle": "Log level",
|
||||
"emptyWaiting": "Waiting for logs...",
|
||||
"emptyFilter": "No logs match filter.",
|
||||
"emptyStart": "Click \"Start\" to begin streaming logs.",
|
||||
"count": "{{filtered}}/{{total}}"
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"title": "MCP Servers",
|
||||
"description": "Manage Model Context Protocol server connections",
|
||||
"addServer": "Add Server",
|
||||
"searchPlaceholder": "Search servers...",
|
||||
"emptyTitle": "No MCP servers",
|
||||
"emptyDescription": "Add your first MCP server to get started.",
|
||||
"noMatchTitle": "No matching servers",
|
||||
"noMatchDescription": "Try a different search term.",
|
||||
"columns": {
|
||||
"name": "Name",
|
||||
"transport": "Transport",
|
||||
"tools": "Tools",
|
||||
"enabled": "Enabled",
|
||||
"createdBy": "Created By",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"viewTools": "View tools",
|
||||
"manageGrants": "Manage grants",
|
||||
"delete": {
|
||||
"title": "Delete MCP Server",
|
||||
"description": "Are you sure you want to delete \"{{name}}\"? This will also remove all associated grants.",
|
||||
"confirmLabel": "Delete"
|
||||
},
|
||||
"form": {
|
||||
"createTitle": "Add MCP Server",
|
||||
"editTitle": "Edit MCP Server",
|
||||
"name": "Name *",
|
||||
"nameHint": "Lowercase letters, numbers, and hyphens only",
|
||||
"displayName": "Display Name",
|
||||
"transport": "Transport *",
|
||||
"command": "Command *",
|
||||
"args": "Args (comma-separated)",
|
||||
"argsPlaceholder": "--flag1, --flag2",
|
||||
"url": "URL *",
|
||||
"headers": "Headers",
|
||||
"headerKeyPlaceholder": "Header name",
|
||||
"headerValuePlaceholder": "Header value",
|
||||
"addHeader": "Add Header",
|
||||
"env": "Environment Variables",
|
||||
"envKeyPlaceholder": "Variable name",
|
||||
"envValuePlaceholder": "Value",
|
||||
"addVariable": "Add Variable",
|
||||
"toolPrefix": "Tool Prefix",
|
||||
"toolPrefixHint": "Auto-derived from name if empty.",
|
||||
"timeout": "Timeout (seconds)",
|
||||
"enabled": "Enabled",
|
||||
"testConnection": "Test Connection",
|
||||
"testing": "Testing...",
|
||||
"toolsFound": "{{count}} tools found",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"update": "Update",
|
||||
"saving": "Saving...",
|
||||
"errors": {
|
||||
"nameRequired": "Name and transport are required",
|
||||
"nameSlug": "Name must be a valid slug (lowercase letters, numbers, hyphens only)",
|
||||
"commandRequired": "Command is required for stdio transport",
|
||||
"urlRequired": "URL is required for SSE/HTTP transport",
|
||||
"connectionFailed": "Connection failed"
|
||||
}
|
||||
},
|
||||
"grants": {
|
||||
"title": "Agent Grants - {{name}}",
|
||||
"currentGrants": "Current Grants",
|
||||
"allToolsAllowed": "All tools allowed",
|
||||
"addGrant": "Add Agent Grant",
|
||||
"editGrant": "Edit Grant",
|
||||
"selectAgent": "Select agent...",
|
||||
"toolAllowList": "Tool allow list (optional)",
|
||||
"toolDenyList": "Tool deny list (optional)",
|
||||
"allowPlaceholder": "Select tools to allow...",
|
||||
"denyPlaceholder": "Select tools to deny...",
|
||||
"grant": "Grant",
|
||||
"update": "Update",
|
||||
"cancel": "Cancel",
|
||||
"agentRequired": "Agent is required",
|
||||
"failedGrant": "Failed to grant",
|
||||
"failedRevoke": "Failed to revoke"
|
||||
},
|
||||
"tools": {
|
||||
"title": "Tools — {{name}}",
|
||||
"prefix": "prefix:",
|
||||
"discovering": "Discovering tools...",
|
||||
"noToolsTitle": "No tools discovered",
|
||||
"noToolsDescription": "The server may be offline or has no tools registered.",
|
||||
"failedLoad": "Failed to load tools",
|
||||
"filterPlaceholder": "Filter tools...",
|
||||
"noMatch": "No matching tools"
|
||||
},
|
||||
"toast": {
|
||||
"created": "MCP server created",
|
||||
"updated": "MCP server updated",
|
||||
"deleted": "MCP server deleted",
|
||||
"failedCreate": "Failed to create MCP server",
|
||||
"failedUpdate": "Failed to update MCP server",
|
||||
"failedDelete": "Failed to delete MCP server"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"title": "Memory",
|
||||
"description": "Browse and manage agent memory documents, chunks, and embeddings",
|
||||
"search": "Search",
|
||||
"create": "Create",
|
||||
"indexAll": "Index All",
|
||||
"indexing": "Indexing...",
|
||||
"emptyTitle": "No memory documents",
|
||||
"emptyAgentDescription": "This agent has no memory documents yet.",
|
||||
"emptyGlobalDescription": "No memory documents found across any agent.",
|
||||
"filters": {
|
||||
"agent": "Agent",
|
||||
"allAgents": "All agents",
|
||||
"scope": "Scope",
|
||||
"allScope": "All (global + personal)"
|
||||
},
|
||||
"tabs": {
|
||||
"documents": "Documents",
|
||||
"knowledgeGraph": "Knowledge Graph"
|
||||
},
|
||||
"columns": {
|
||||
"path": "Path",
|
||||
"agent": "Agent",
|
||||
"scope": "Scope",
|
||||
"hash": "Hash",
|
||||
"updated": "Updated",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"scopeLabel": {
|
||||
"personal": "Personal",
|
||||
"global": "Global"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete Memory Document",
|
||||
"description": "Are you sure you want to delete \"{{path}}\"? This will also delete all associated chunks and embeddings.",
|
||||
"confirmLabel": "Delete"
|
||||
},
|
||||
"createDialog": {
|
||||
"title": "Create Memory Document",
|
||||
"path": "Path *",
|
||||
"pathPlaceholder": "e.g. notes/project-overview.md",
|
||||
"content": "Content *",
|
||||
"contentPlaceholder": "Document content...",
|
||||
"agentId": "Agent *",
|
||||
"agentIdPlaceholder": "Select agent...",
|
||||
"scope": "Scope",
|
||||
"existingScope": "Existing scope",
|
||||
"customScope": "Custom",
|
||||
"selectGroupUser": "Select group/user...",
|
||||
"scopeHint": "Global = shared across all users. Personal = scoped to a specific user or group chat.",
|
||||
"autoIndex": "Auto-index after creation",
|
||||
"agentRequired": "Please select an agent",
|
||||
"pathRequired": "Path is required",
|
||||
"contentRequired": "Content is required",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"creating": "Creating..."
|
||||
},
|
||||
"documentDialog": {
|
||||
"title": "Memory Document",
|
||||
"path": "Path:",
|
||||
"agent": "Agent:",
|
||||
"scope": "Scope:",
|
||||
"hash": "Hash:",
|
||||
"updated": "Updated:",
|
||||
"content": "Content",
|
||||
"chunks": "Chunks ({{count}})",
|
||||
"chunkIndex": "Chunk {{index}}",
|
||||
"noChunks": "No chunks",
|
||||
"embedded": "Embedded",
|
||||
"noEmbedding": "No embedding"
|
||||
},
|
||||
"searchDialog": {
|
||||
"title": "Search Memory",
|
||||
"query": "Search Query",
|
||||
"queryPlaceholder": "Enter search query...",
|
||||
"agentId": "Agent ID",
|
||||
"agentIdPlaceholder": "Filter by agent...",
|
||||
"limit": "Limit",
|
||||
"search": "Search",
|
||||
"searching": "Searching...",
|
||||
"results": "Results",
|
||||
"noResults": "No results found",
|
||||
"similarity": "similarity",
|
||||
"path": "Path:"
|
||||
},
|
||||
"kg": {
|
||||
"emptyTitle": "No entities",
|
||||
"emptyDescription": "No knowledge graph entities for this agent yet.",
|
||||
"emptySearchDescription": "No entities match your search.",
|
||||
"stats": {
|
||||
"entities": "Entities: {{count}}",
|
||||
"relations": "Relations: {{count}}"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search entities...",
|
||||
"button": "Search",
|
||||
"clear": "Clear"
|
||||
},
|
||||
"refresh": "Refresh",
|
||||
"extract": "Extract",
|
||||
"columns": {
|
||||
"name": "Name",
|
||||
"type": "Type",
|
||||
"description": "Description",
|
||||
"confidence": "Confidence",
|
||||
"actions": "Actions",
|
||||
"observations": "Observations",
|
||||
"relations": "Relations"
|
||||
},
|
||||
"deleteEntity": {
|
||||
"title": "Delete Entity",
|
||||
"description": "Delete \"{{name}}\"? This will also delete all associated relations.",
|
||||
"confirmLabel": "Delete"
|
||||
},
|
||||
"entity": {
|
||||
"externalId": "External ID:",
|
||||
"confidence": "Confidence:",
|
||||
"description": "Description:",
|
||||
"source": "Source:",
|
||||
"properties": "Properties:",
|
||||
"relations": "Relations",
|
||||
"traverse": "Traverse (2-hop)",
|
||||
"traversing": "Traversing...",
|
||||
"loading": "Loading...",
|
||||
"noRelations": "No relations found.",
|
||||
"traversalResults": "Traversal Results ({{count}})",
|
||||
"direction": {
|
||||
"outgoing": "→ outgoing",
|
||||
"incoming": "← incoming"
|
||||
},
|
||||
"columns": {
|
||||
"direction": "Direction",
|
||||
"relation": "Relation",
|
||||
"target": "Target",
|
||||
"confidence": "Confidence"
|
||||
}
|
||||
},
|
||||
"extractDialog": {
|
||||
"title": "Extract Entities from Text",
|
||||
"providerLabel": "Extraction Provider",
|
||||
"modelLabel": "Extraction Model",
|
||||
"textLabel": "Text to extract from",
|
||||
"textPlaceholder": "Paste conversation text, notes, or any content to extract entities and relations from...",
|
||||
"extract": "Extract",
|
||||
"extracting": "Extracting...",
|
||||
"cancel": "Cancel",
|
||||
"agentId": "Agent ID",
|
||||
"agentIdPlaceholder": "Leave blank for global"
|
||||
},
|
||||
"graphView": {
|
||||
"loading": "Loading graph...",
|
||||
"empty": "No entities to visualize",
|
||||
"nodes": "{{count}} nodes",
|
||||
"edges": "{{count}} edges"
|
||||
}
|
||||
},
|
||||
"toast": {
|
||||
"created": "Memory document created",
|
||||
"deleted": "Memory document deleted",
|
||||
"indexed": "All documents indexed",
|
||||
"failedCreate": "Failed to create document",
|
||||
"failedDelete": "Failed to delete document",
|
||||
"failedIndex": "Failed to index documents"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"title": "Nodes",
|
||||
"description": "Manage paired devices and pending pairing requests",
|
||||
"emptyTitle": "No devices",
|
||||
"emptyDescription": "No paired devices or pending pairing requests.",
|
||||
"pendingRequests": "Pending Requests ({{count}})",
|
||||
"pairedDevices": "Paired Devices ({{count}})",
|
||||
"sender": "Sender:",
|
||||
"chat": "Chat:",
|
||||
"columns": {
|
||||
"channel": "Channel",
|
||||
"senderId": "Sender ID",
|
||||
"paired": "Paired",
|
||||
"by": "By",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"approve": "Approve",
|
||||
"deny": "Deny",
|
||||
"revoke": "Revoke",
|
||||
"confirmApprove": {
|
||||
"title": "Approve Pairing",
|
||||
"description": "Approve pairing request from {{channel}}:{{senderId}} (code: {{code}})? This device will be able to interact with agents.",
|
||||
"confirmLabel": "Approve"
|
||||
},
|
||||
"confirmDeny": {
|
||||
"title": "Deny Pairing",
|
||||
"description": "Deny pairing request from {{channel}}:{{senderId}} (code: {{code}})? The request will be removed.",
|
||||
"confirmLabel": "Deny"
|
||||
},
|
||||
"confirmRevoke": {
|
||||
"title": "Revoke Device",
|
||||
"description": "Revoke pairing for {{channel}}:{{senderId}}? The device will need to re-pair.",
|
||||
"confirmLabel": "Revoke"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"title": "Dashboard",
|
||||
"description": "Gateway overview and quota usage",
|
||||
"statCards": {
|
||||
"requestsToday": "Requests Today",
|
||||
"tokensToday": "Tokens Today",
|
||||
"agents": "Agents",
|
||||
"channels": "Channels",
|
||||
"running": "running",
|
||||
"online": "online",
|
||||
"users": "{{count}} users",
|
||||
"inOut": "{{input}} in / {{output}} out"
|
||||
},
|
||||
"providers": {
|
||||
"noProvidersTitle": "No LLM providers configured",
|
||||
"noEnabledTitle": "No LLM providers enabled",
|
||||
"noProvidersDesc": "You need to add at least one LLM provider before agents can work. ",
|
||||
"noEnabledDesc": "All providers are currently disabled. Enable at least one to start using agents. ",
|
||||
"goToSettings": "Go to Provider Settings"
|
||||
},
|
||||
"systemHealth": {
|
||||
"title": "System Health",
|
||||
"uptime": "Uptime",
|
||||
"database": "Database",
|
||||
"providers": "Providers",
|
||||
"tools": "Tools",
|
||||
"sessions": "Sessions",
|
||||
"clients": "Clients",
|
||||
"channels": "Channels",
|
||||
"active": "{{count}} active",
|
||||
"none": "none"
|
||||
},
|
||||
"connectedClients": {
|
||||
"title": "Connected Clients",
|
||||
"noClients": "No clients connected",
|
||||
"columns": {
|
||||
"ip": "IP",
|
||||
"user": "User",
|
||||
"role": "Role",
|
||||
"connected": "Connected"
|
||||
},
|
||||
"you": "you"
|
||||
},
|
||||
"cronJobs": {
|
||||
"title": "Cron Jobs",
|
||||
"manage": "Manage",
|
||||
"noJobs": "No cron jobs configured",
|
||||
"disabled": "disabled"
|
||||
},
|
||||
"recentRequests": {
|
||||
"title": "Recent Requests",
|
||||
"viewAll": "View All",
|
||||
"noRequests": "No recent requests",
|
||||
"columns": {
|
||||
"time": "Time",
|
||||
"name": "Name",
|
||||
"user": "User",
|
||||
"channel": "Channel",
|
||||
"tokens": "Tokens",
|
||||
"duration": "Duration",
|
||||
"status": "Status"
|
||||
}
|
||||
},
|
||||
"quotaUsage": {
|
||||
"title": "Quota Usage",
|
||||
"columns": {
|
||||
"userGroup": "User / Group",
|
||||
"hour": "Hour",
|
||||
"day": "Day",
|
||||
"week": "Week"
|
||||
},
|
||||
"noLimit": "no limit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"title": "Pending Messages",
|
||||
"description": "Buffered channel messages awaiting agent processing",
|
||||
"emptyTitle": "No pending messages",
|
||||
"emptyDescription": "No buffered message groups found. Messages appear here when channels buffer incoming messages before agent processing.",
|
||||
"columns": {
|
||||
"channel": "Channel",
|
||||
"group": "Group",
|
||||
"messages": "Messages",
|
||||
"status": "Status",
|
||||
"lastActivity": "Last Activity",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"status": {
|
||||
"compacted": "Compacted",
|
||||
"raw": "Raw"
|
||||
},
|
||||
"compact": "Compact",
|
||||
"clear": "Clear",
|
||||
"confirmClear": {
|
||||
"title": "Clear messages?",
|
||||
"description": "This will permanently delete all messages in {{channel}} / {{key}}. This action cannot be undone.",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Clear"
|
||||
},
|
||||
"dialog": {
|
||||
"title": "Messages — {{name}}",
|
||||
"noMessages": "No messages found.",
|
||||
"summary": "Summary"
|
||||
}
|
||||
}
|
||||
Loaded 100 of 312 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user