diff --git a/Dockerfile b/Dockerfile index 83903f47..2b128827 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,8 +38,9 @@ FROM alpine:3.22 ARG ENABLE_SANDBOX=false ARG ENABLE_PYTHON=false +ARG ENABLE_NODE=false -# Install ca-certificates + wget (healthcheck) + optionally docker-cli (sandbox) + python3 +# Install ca-certificates + wget (healthcheck) + optionally docker-cli (sandbox) + python3 + nodejs RUN set -eux; \ apk add --no-cache ca-certificates wget; \ if [ "$ENABLE_SANDBOX" = "true" ]; then \ @@ -48,6 +49,9 @@ RUN set -eux; \ if [ "$ENABLE_PYTHON" = "true" ]; then \ apk add --no-cache python3 py3-pip; \ pip3 install --break-system-packages pypdf; \ + fi; \ + if [ "$ENABLE_NODE" = "true" ]; then \ + apk add --no-cache nodejs npm; \ fi # Non-root user diff --git a/cmd/gateway_cron.go b/cmd/gateway_cron.go index 660c8d2f..4d3b739d 100644 --- a/cmd/gateway_cron.go +++ b/cmd/gateway_cron.go @@ -19,6 +19,25 @@ import ( // and integration with /stop, /stopall commands. func makeCronJobHandler(sched *scheduler.Scheduler, msgBus *bus.MessageBus, cfg *config.Config, channelMgr *channels.Manager) func(job *store.CronJob) (*store.CronJobResult, error) { return func(job *store.CronJob) (*store.CronJobResult, error) { + peerKind := resolveCronPeerKind(job) + + // Direct delivery: send Payload.Message straight to the channel without agent processing. + // This is the correct behavior for reminders/notifications — bot sends one message directly. + if job.Payload.Deliver && job.Payload.Channel != "" && job.Payload.To != "" { + outMsg := bus.OutboundMessage{ + Channel: job.Payload.Channel, + ChatID: job.Payload.To, + Content: job.Payload.Message, + } + if peerKind == "group" { + outMsg.Metadata = map[string]string{"group_id": job.Payload.To} + } + msgBus.PublishOutbound(outMsg) + + return &store.CronJobResult{Content: job.Payload.Message}, nil + } + + // Agent processing: route through scheduler for LLM-powered cron tasks. agentID := job.AgentID if agentID == "" { agentID = cfg.ResolveDefaultAgentID() @@ -32,11 +51,6 @@ func makeCronJobHandler(sched *scheduler.Scheduler, msgBus *bus.MessageBus, cfg channel = "cron" } - // Infer peer kind from the stored session metadata (group chats need it - // so that tools like message can route correctly via group APIs). - peerKind := resolveCronPeerKind(job) - - // Resolve channel type for system prompt context. channelType := resolveChannelType(channelMgr, channel) // Build cron context so the agent knows delivery target and requester. @@ -72,28 +86,12 @@ func makeCronJobHandler(sched *scheduler.Scheduler, msgBus *bus.MessageBus, cfg TraceTags: []string{"cron"}, }) - // Block until the scheduled run completes outcome := <-outCh if outcome.Err != nil { return nil, outcome.Err } result := outcome.Result - - // If job wants delivery to a channel, send the agent response to the target chat. - if job.Payload.Deliver && job.Payload.Channel != "" && job.Payload.To != "" { - outMsg := bus.OutboundMessage{ - Channel: job.Payload.Channel, - ChatID: job.Payload.To, - Content: result.Content, - } - if peerKind == "group" { - outMsg.Metadata = map[string]string{"group_id": job.Payload.To} - } - appendMediaToOutbound(&outMsg, result.Media) - msgBus.PublishOutbound(outMsg) - } - cronResult := &store.CronJobResult{ Content: result.Content, } diff --git a/internal/tools/cron.go b/internal/tools/cron.go index adbd65f1..e197d126 100644 --- a/internal/tools/cron.go +++ b/internal/tools/cron.go @@ -230,16 +230,20 @@ func (t *CronTool) handleAdd(ctx context.Context, args map[string]any, agentID, channel, _ := jobObj["channel"].(string) to, _ := jobObj["to"].(string) - // Auto-fill channel and to from context when deliver is requested. + // Auto-fill channel and to from context. // Always prefer context values over LLM-provided values to prevent // misrouted deliveries (e.g. LLM confusing guild ID with channel ID). - if deliver { - if ctxChannel := ToolChannelFromCtx(ctx); ctxChannel != "" { - channel = ctxChannel - } - if ctxChatID := ToolChatIDFromCtx(ctx); ctxChatID != "" { - to = ctxChatID - } + if ctxChannel := ToolChannelFromCtx(ctx); ctxChannel != "" { + channel = ctxChannel + } + if ctxChatID := ToolChatIDFromCtx(ctx); ctxChatID != "" { + to = ctxChatID + } + + // Default deliver=true when created from a chat context (channel + to available). + // This ensures reminders are sent directly as bot messages, not routed through the agent. + if !deliver && channel != "" && to != "" { + deliver = true } // Use agent ID from job object if explicitly provided, otherwise from context diff --git a/ui/web/src/i18n/locales/en/setup.json b/ui/web/src/i18n/locales/en/setup.json index d1ff08ea..593f3fa9 100644 --- a/ui/web/src/i18n/locales/en/setup.json +++ b/ui/web/src/i18n/locales/en/setup.json @@ -98,5 +98,8 @@ "requiredFields": "Required: {{fields}}", "failedCreate": "Failed to create channel" } + }, + "common": { + "back": "Back" } } diff --git a/ui/web/src/i18n/locales/vi/setup.json b/ui/web/src/i18n/locales/vi/setup.json index c59c1645..a2063cdd 100644 --- a/ui/web/src/i18n/locales/vi/setup.json +++ b/ui/web/src/i18n/locales/vi/setup.json @@ -98,5 +98,8 @@ "requiredFields": "Bắt buộc: {{fields}}", "failedCreate": "Không thể tạo channel" } + }, + "common": { + "back": "Quay lại" } } diff --git a/ui/web/src/i18n/locales/zh/setup.json b/ui/web/src/i18n/locales/zh/setup.json index 3a8c24db..b92bb750 100644 --- a/ui/web/src/i18n/locales/zh/setup.json +++ b/ui/web/src/i18n/locales/zh/setup.json @@ -98,5 +98,8 @@ "requiredFields": "必填项:{{fields}}", "failedCreate": "创建Channel失败" } + }, + "common": { + "back": "返回" } } diff --git a/ui/web/src/pages/setup/setup-page.tsx b/ui/web/src/pages/setup/setup-page.tsx index a12d7971..7ea100c8 100644 --- a/ui/web/src/pages/setup/setup-page.tsx +++ b/ui/web/src/pages/setup/setup-page.tsx @@ -74,6 +74,7 @@ export function SetupPage() { {step === 2 && activeProvider && ( setStep(1)} onComplete={(model) => { setSelectedModel(model); setStep(3); @@ -85,6 +86,7 @@ export function SetupPage() { setStep(2)} onComplete={(agent) => { setCreatedAgent(agent); setStep(4); @@ -95,6 +97,7 @@ export function SetupPage() { {step === 4 && ( setStep(3)} onComplete={handleFinish} onSkip={handleFinish} /> diff --git a/ui/web/src/pages/setup/step-agent.tsx b/ui/web/src/pages/setup/step-agent.tsx index b16bbd1f..38ad44e9 100644 --- a/ui/web/src/pages/setup/step-agent.tsx +++ b/ui/web/src/pages/setup/step-agent.tsx @@ -22,10 +22,11 @@ const DEFAULT_PROMPT = `You are GoClaw, my helpful assistant. I am your boss, Ne interface StepAgentProps { provider: ProviderData | null; model: string | null; + onBack?: () => void; onComplete: (agent: AgentData) => void; } -export function StepAgent({ provider, model, onComplete }: StepAgentProps) { +export function StepAgent({ provider, model, onBack, onComplete }: StepAgentProps) { const { t } = useTranslation("setup"); const { createAgent, deleteAgent, resummonAgent } = useAgents(); @@ -214,7 +215,12 @@ export function StepAgent({ provider, model, onComplete }: StepAgentProps) { {error &&

{error}

} -
+
+ {onBack && ( + + )} - +
+ {onBack && ( + + )} +
+ + +
diff --git a/ui/web/src/pages/setup/step-model.tsx b/ui/web/src/pages/setup/step-model.tsx index bbbac44f..f048e0b1 100644 --- a/ui/web/src/pages/setup/step-model.tsx +++ b/ui/web/src/pages/setup/step-model.tsx @@ -14,10 +14,11 @@ import type { ProviderData } from "@/types/provider"; interface StepModelProps { provider: ProviderData; + onBack?: () => void; onComplete: (model: string) => void; } -export function StepModel({ provider, onComplete }: StepModelProps) { +export function StepModel({ provider, onBack, onComplete }: StepModelProps) { const { t } = useTranslation("setup"); const { models, loading: modelsLoading } = useProviderModels(provider.id, provider.provider_type); const { verify, verifying, result: verifyResult, reset: resetVerify } = useProviderVerify(); @@ -90,17 +91,24 @@ export function StepModel({ provider, onComplete }: StepModelProps) {
)} -
- - +
+ {onBack && ( + + )} +
+ + +