diff --git a/cmd/clawgo/main.go b/cmd/clawgo/main.go index 9922155..fb902b4 100644 --- a/cmd/clawgo/main.go +++ b/cmd/clawgo/main.go @@ -145,7 +145,7 @@ func main() { workspace := cfg.WorkspacePath() installer := skills.NewSkillInstaller(workspace) - // 获取全局配置目录和内置 skills 目录 + // Get global config directory and built-in skills directory globalDir := filepath.Dir(getConfigPath()) globalSkillsDir := filepath.Join(globalDir, "skills") builtinSkillsDir := filepath.Join(globalDir, "clawgo", "skills") @@ -1779,7 +1779,7 @@ func skillsCmd() { workspace := cfg.WorkspacePath() installer := skills.NewSkillInstaller(workspace) - // 获取全局配置目录和内置 skills 目录 + // Get global config directory and built-in skills directory globalDir := filepath.Dir(getConfigPath()) globalSkillsDir := filepath.Join(globalDir, "skills") builtinSkillsDir := filepath.Join(globalDir, "clawgo", "skills") diff --git a/pkg/agent/context.go b/pkg/agent/context.go index b5d21c1..0702335 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -30,8 +30,8 @@ func getGlobalConfigDir() string { } func NewContextBuilder(workspace string, memCfg config.MemoryConfig, toolsSummaryFunc func() []string) *ContextBuilder { - // builtin skills: 当前项目的 skills 目录 - // 使用当前工作目录下的 skills/ 目录 + // Built-in skills: the current project's skills directory. + // Use the skills/ directory under the current working directory. wd, _ := os.Getwd() builtinSkillsDir := filepath.Join(wd, "skills") globalSkillsDir := filepath.Join(getGlobalConfigDir(), "skills") diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 0110b3a..e16a691 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -129,6 +129,7 @@ type taskExecutionDirectivesLLMResponse struct { type stageReporter struct { onUpdate func(content string) + localize func(content string) string } type StartupSelfCheckReport struct { @@ -140,17 +141,38 @@ func (sr *stageReporter) Publish(stage int, total int, status string, detail str if sr == nil || sr.onUpdate == nil { return } + _ = stage + _ = total detail = strings.TrimSpace(detail) - if detail == "" { - detail = "-" + if detail != "" { + if sr.localize != nil { + detail = sr.localize(detail) + } + sr.onUpdate(detail) + return } status = strings.TrimSpace(status) - if status == "" { - status = "进度更新" + if status != "" { + if sr.localize != nil { + status = sr.localize(status) + } + sr.onUpdate(status) + return } - sr.onUpdate(fmt.Sprintf("[进度] %s:%s", status, detail)) + fallback := "Processing update" + if sr.localize != nil { + fallback = sr.localize(fallback) + } + sr.onUpdate(fallback) } +type userLanguageHint struct { + sessionKey string + content string +} + +type userLanguageHintKey struct{} + func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers.LLMProvider, cs *cron.CronService) *AgentLoop { workspace := cfg.WorkspacePath() os.MkdirAll(workspace, 0755) @@ -267,9 +289,9 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers autonomyBySess: make(map[string]*autonomySession), } - // 注入递归运行逻辑,使 subagent 具备 full tool-calling 能力 + // Inject recursive run logic so subagent has full tool-calling capability. subagentManager.SetRunFunc(func(ctx context.Context, task, channel, chatID string) (string, error) { - sessionKey := fmt.Sprintf("subagent:%d", os.Getpid()) // 改用 PID 或随机数,避免 sessionKey 冲突 + sessionKey := fmt.Sprintf("subagent:%d", os.Getpid()) // Use PID/random value to avoid session key collisions. return loop.ProcessDirect(ctx, task, sessionKey) }) @@ -345,7 +367,7 @@ func (al *AgentLoop) enqueueMessage(ctx context.Context, msg bus.InboundMessage) Buttons: nil, Channel: msg.Channel, ChatID: msg.ChatID, - Content: al.naturalizeUserFacingText(ctx, "消息队列当前较忙,请稍后再试。"), + Content: al.localizeUserFacingText(ctx, msg.SessionKey, msg.Content, "The message queue is currently busy. Please try again shortly."), }) } } @@ -403,7 +425,7 @@ func (al *AgentLoop) runSessionWorker(ctx context.Context, sessionKey string, wo if errors.Is(err, context.Canceled) { return } - response = al.formatProcessingErrorMessage(msg, err) + response = al.formatProcessingErrorMessage(taskCtx, msg, err) } if response != "" && shouldPublishSyntheticResponse(msg) { @@ -425,11 +447,8 @@ func (al *AgentLoop) clearWorkerCancel(worker *sessionWorker) { worker.cancelMu.Unlock() } -func (al *AgentLoop) formatProcessingErrorMessage(msg bus.InboundMessage, err error) string { - if al.preferChineseUserFacingText(msg.SessionKey, msg.Content) { - return err.Error() - } - return err.Error() +func (al *AgentLoop) formatProcessingErrorMessage(ctx context.Context, msg bus.InboundMessage, err error) string { + return al.localizeUserFacingText(ctx, msg.SessionKey, msg.Content, fmt.Sprintf("Error processing message: %v", err)) } func (al *AgentLoop) preferChineseUserFacingText(sessionKey, currentContent string) bool { @@ -520,7 +539,7 @@ func (al *AgentLoop) stopAllAutonomySessions() { func (al *AgentLoop) startAutonomy(ctx context.Context, msg bus.InboundMessage, idleInterval time.Duration, focus string) string { if msg.Channel == "cli" { - return al.naturalizeUserFacingText(ctx, "自主模式需要在 gateway 运行模式下使用(持续消息循环)。") + return al.naturalizeUserFacingText(ctx, "Autonomy mode requires gateway runtime mode (continuous message loop).") } if idleInterval <= 0 { @@ -538,7 +557,8 @@ func (al *AgentLoop) startAutonomy(ctx context.Context, msg bus.InboundMessage, delete(al.autonomyBySess, msg.SessionKey) } - sessionCtx, cancel := context.WithCancel(context.Background()) + langCtx := withUserLanguageHint(context.Background(), msg.SessionKey, msg.Content) + sessionCtx, cancel := context.WithCancel(langCtx) s := &autonomySession{ cancel: cancel, started: time.Now(), @@ -566,9 +586,9 @@ func (al *AgentLoop) startAutonomy(ctx context.Context, msg bus.InboundMessage, }) } if s.focus != "" { - return al.naturalizeUserFacingText(ctx, fmt.Sprintf("自主模式已开启,当前研究方向:%s。系统会持续静默推进,并每 %s 汇报一次进度或结果。", s.focus, idleInterval.Truncate(time.Second))) + return al.naturalizeUserFacingText(ctx, fmt.Sprintf("Autonomy mode is enabled. Current focus: %s. The system will continue in the background and report progress or results every %s.", s.focus, idleInterval.Truncate(time.Second))) } - return al.naturalizeUserFacingText(ctx, fmt.Sprintf("自主模式已开启:自动拆解执行 + 静默推进;每 %s 汇报一次进度或结果。", idleInterval.Truncate(time.Second))) + return al.naturalizeUserFacingText(ctx, fmt.Sprintf("Autonomy mode is enabled: automatic decomposition + background execution; reports progress or results every %s.", idleInterval.Truncate(time.Second))) } func (al *AgentLoop) stopAutonomy(sessionKey string) bool { @@ -625,21 +645,21 @@ func (al *AgentLoop) autonomyStatus(ctx context.Context, sessionKey string) stri s, ok := al.autonomyBySess[sessionKey] if !ok || s == nil { - return al.naturalizeUserFacingText(ctx, "自主模式未开启。") + return al.naturalizeUserFacingText(ctx, "Autonomy mode is not enabled.") } uptime := time.Since(s.started).Truncate(time.Second) idle := time.Since(s.lastUserAt).Truncate(time.Second) focus := strings.TrimSpace(s.focus) if focus == "" { - focus = "未设置" + focus = "not set" } - fallback := fmt.Sprintf("自主模式运行中:汇报周期 %s,已运行 %s,最近用户活跃距今 %s,自动推进 %d 轮。", + fallback := fmt.Sprintf("Autonomy mode is running: report interval %s, uptime %s, time since last user activity %s, automatic rounds %d.", s.idleInterval.Truncate(time.Second), uptime, idle, s.rounds, - ) + fmt.Sprintf(" 当前研究方向:%s。", focus) + ) + fmt.Sprintf(" Current focus: %s.", focus) return al.naturalizeUserFacingText(ctx, fallback) } @@ -713,25 +733,25 @@ func (al *AgentLoop) finishAutonomyRound(sessionKey string) { func buildAutonomyFollowUpPrompt(round int, focus string, reportDue bool) string { focus = strings.TrimSpace(focus) if focus == "" && reportDue { - return fmt.Sprintf("自主模式第 %d 轮推进:用户暂时未继续输入。请基于当前会话上下文和已完成工作,自主完成一个高价值下一步,并用自然语言给出进度或结果。", round) + return fmt.Sprintf("Autonomy round %d: the user has not provided new input yet. Based on the current session context and completed work, autonomously complete one high-value next step and report progress or results in natural language.", round) } if focus == "" && !reportDue { - return fmt.Sprintf("自主模式第 %d 轮推进:用户暂时未继续输入。请基于当前会话上下文和已完成工作,自主完成一个高价值下一步。本轮仅执行,不对外回复。", round) + return fmt.Sprintf("Autonomy round %d: the user has not provided new input yet. Based on the current session context and completed work, autonomously complete one high-value next step. This round is execution-only; do not send an external reply.", round) } if reportDue { - return fmt.Sprintf("自主模式第 %d 轮推进:用户暂时未继续输入。请优先围绕研究方向“%s”推进;如果该方向已完成,请说明并转向其他高价值下一步。完成后用自然语言给出进度或结果。", round, focus) + return fmt.Sprintf("Autonomy round %d: the user has not provided new input yet. Prioritize progress around the focus \"%s\"; if that focus is complete, explain and move to another high-value next step. After completion, report progress or results in natural language.", round, focus) } - return fmt.Sprintf("自主模式第 %d 轮推进:用户暂时未继续输入。请优先围绕研究方向“%s”推进;如果该方向已完成,请说明并转向其他高价值下一步。本轮仅执行,不对外回复。", round, focus) + return fmt.Sprintf("Autonomy round %d: the user has not provided new input yet. Prioritize progress around the focus \"%s\"; if that focus is complete, explain and move to another high-value next step. This round is execution-only; do not send an external reply.", round, focus) } func buildAutonomyFocusPrompt(focus string) string { focus = strings.TrimSpace(focus) - return fmt.Sprintf("自主模式已启动,本轮请优先围绕研究方向“%s”展开:先明确本轮目标,再执行并汇报阶段性进展与结果。", focus) + return fmt.Sprintf("Autonomy mode started. For this round, prioritize the focus \"%s\": clarify the round goal first, then execute and report progress and results.", focus) } func (al *AgentLoop) startAutoLearner(ctx context.Context, msg bus.InboundMessage, interval time.Duration) string { if msg.Channel == "cli" { - return al.naturalizeUserFacingText(ctx, "自动学习需要在 gateway 运行模式下使用(持续消息循环)。") + return al.naturalizeUserFacingText(ctx, "Auto-learn requires gateway runtime mode (continuous message loop).") } if interval <= 0 { @@ -749,7 +769,8 @@ func (al *AgentLoop) startAutoLearner(ctx context.Context, msg bus.InboundMessag delete(al.autoLearners, msg.SessionKey) } - learnerCtx, cancel := context.WithCancel(context.Background()) + langCtx := withUserLanguageHint(context.Background(), msg.SessionKey, msg.Content) + learnerCtx, cancel := context.WithCancel(langCtx) learner := &autoLearner{ cancel: cancel, started: time.Now(), @@ -760,7 +781,7 @@ func (al *AgentLoop) startAutoLearner(ctx context.Context, msg bus.InboundMessag go al.runAutoLearnerLoop(learnerCtx, msg) - return al.naturalizeUserFacingText(ctx, fmt.Sprintf("自动学习已开启:每 %s 执行 1 轮。使用 /autolearn stop 可停止。", interval.Truncate(time.Second))) + return al.naturalizeUserFacingText(ctx, fmt.Sprintf("Auto-learn is enabled: one round every %s. Use /autolearn stop to stop it.", interval.Truncate(time.Second))) } func (al *AgentLoop) runAutoLearnerLoop(ctx context.Context, msg bus.InboundMessage) { @@ -773,7 +794,7 @@ func (al *AgentLoop) runAutoLearnerLoop(ctx context.Context, msg bus.InboundMess al.bus.PublishOutbound(bus.OutboundMessage{ Channel: msg.Channel, ChatID: msg.ChatID, - Content: al.naturalizeUserFacingText(ctx, fmt.Sprintf("自动学习第 %d 轮开始。", round)), + Content: al.naturalizeUserFacingText(ctx, fmt.Sprintf("Auto-learn round %d started.", round)), }) al.bus.PublishInbound(bus.InboundMessage{ @@ -853,11 +874,11 @@ func (al *AgentLoop) autoLearnerStatus(ctx context.Context, sessionKey string) s learner, ok := al.autoLearners[sessionKey] if !ok || learner == nil { - return al.naturalizeUserFacingText(ctx, "自动学习未开启。使用 /autolearn start 可开启。") + return al.naturalizeUserFacingText(ctx, "Auto-learn is not enabled. Use /autolearn start to enable it.") } uptime := time.Since(learner.started).Truncate(time.Second) - fallback := fmt.Sprintf("自动学习运行中:每 %s 一轮,已运行 %s,累计 %d 轮。", + fallback := fmt.Sprintf("Auto-learn is running: one round every %s, uptime %s, total rounds %d.", learner.interval.Truncate(time.Second), uptime, learner.rounds, @@ -866,11 +887,11 @@ func (al *AgentLoop) autoLearnerStatus(ctx context.Context, sessionKey string) s } func buildAutoLearnPrompt(round int) string { - return fmt.Sprintf("自动学习模式第 %d 轮:无需用户下发任务。请基于当前会话与项目上下文,自主选择一个高价值的小任务并完成。要求:1) 明确本轮学习目标;2) 必要时调用工具执行;3) 将关键结论写入 memory/MEMORY.md;4) 输出简短进展报告。", round) + return fmt.Sprintf("Auto-learn round %d: no user task is required. Based on current session and project context, choose and complete one high-value small task autonomously. Requirements: 1) define the learning goal for this round; 2) call tools when needed; 3) write key conclusions to memory/MEMORY.md; 4) output a concise progress report.", round) } func buildAutonomyTaskPrompt(task string) string { - return fmt.Sprintf("开启自主执行策略。请直接推进任务并在关键节点自然汇报进展,最后给出结果与下一步建议。\n\n用户任务:%s", strings.TrimSpace(task)) + return fmt.Sprintf("Enable autonomous execution strategy. Proceed with the task directly, report progress naturally at key points, and finally provide results plus next-step suggestions.\n\nUser task: %s", strings.TrimSpace(task)) } func isSyntheticMessage(msg bus.InboundMessage) bool { @@ -898,21 +919,47 @@ func shouldHandleControlIntents(msg bus.InboundMessage) bool { return !isSyntheticMessage(msg) } +func withUserLanguageHint(ctx context.Context, sessionKey, content string) context.Context { + if ctx == nil { + ctx = context.Background() + } + return context.WithValue(ctx, userLanguageHintKey{}, userLanguageHint{ + sessionKey: strings.TrimSpace(sessionKey), + content: content, + }) +} + +func (al *AgentLoop) localizeUserFacingText(ctx context.Context, sessionKey, currentContent, fallback string) string { + return al.naturalizeUserFacingText(withUserLanguageHint(ctx, sessionKey, currentContent), fallback) +} + func (al *AgentLoop) naturalizeUserFacingText(ctx context.Context, fallback string) string { text := strings.TrimSpace(fallback) if text == "" || ctx == nil { return fallback } + if al == nil || (al.provider == nil && len(al.providersByProxy) == 0) { + return fallback + } + + targetLanguage := "English" + if al != nil { + if hint, ok := ctx.Value(userLanguageHintKey{}).(userLanguageHint); ok { + if al.preferChineseUserFacingText(hint.sessionKey, hint.content) { + targetLanguage = "Simplified Chinese" + } + } + } llmCtx, cancel := context.WithTimeout(ctx, 4*time.Second) defer cancel() - systemPrompt := al.withBootstrapPolicy(`You rewrite assistant control/status replies in natural conversational Chinese. + systemPrompt := al.withBootstrapPolicy(fmt.Sprintf(`You rewrite assistant control/status replies in natural conversational %s. Rules: - Keep factual meaning unchanged. - Use concise natural wording, no rigid templates. - No markdown, no code block, no extra explanation. -- Return plain text only.`) +- Return plain text only.`, targetLanguage)) resp, err := al.callLLMWithModelFallback(llmCtx, []providers.Message{ {Role: "system", Content: systemPrompt}, @@ -958,6 +1005,8 @@ func (al *AgentLoop) ProcessDirect(ctx context.Context, content, sessionKey stri } func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) (string, error) { + ctx = withUserLanguageHint(ctx, msg.SessionKey, msg.Content) + // Add message preview to log preview := truncate(msg.Content, 80) logger.InfoCF("agent", fmt.Sprintf("Processing message from %s:%s: %s", msg.Channel, msg.SenderID, preview), @@ -1001,14 +1050,14 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) return al.startAutonomy(ctx, msg, idle, intent.focus), nil case "clear_focus": if al.clearAutonomyFocus(msg.SessionKey) { - return al.naturalizeUserFacingText(ctx, "已确认:当前研究方向已完成,后续自主推进将转向其他高价值任务。"), nil + return al.naturalizeUserFacingText(ctx, "Confirmed: the current focus is complete. Subsequent autonomous rounds will shift to other high-value tasks."), nil } - return al.naturalizeUserFacingText(ctx, "自主模式当前未运行,无法清空研究方向。"), nil + return al.naturalizeUserFacingText(ctx, "Autonomy mode is not running, so the focus cannot be cleared."), nil case "stop": if al.stopAutonomy(msg.SessionKey) { - return al.naturalizeUserFacingText(ctx, "自主模式已关闭。"), nil + return al.naturalizeUserFacingText(ctx, "Autonomy mode stopped."), nil } - return al.naturalizeUserFacingText(ctx, "自主模式当前未运行。"), nil + return al.naturalizeUserFacingText(ctx, "Autonomy mode is not running."), nil case "status": return al.autonomyStatus(ctx, msg.SessionKey), nil } @@ -1024,9 +1073,9 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) return al.startAutoLearner(ctx, msg, interval), nil case "stop": if al.stopAutoLearner(msg.SessionKey) { - return al.naturalizeUserFacingText(ctx, "自动学习已停止。"), nil + return al.naturalizeUserFacingText(ctx, "Auto-learn stopped."), nil } - return al.naturalizeUserFacingText(ctx, "自动学习当前未运行。"), nil + return al.naturalizeUserFacingText(ctx, "Auto-learn is not running."), nil case "status": return al.autoLearnerStatus(ctx, msg.SessionKey), nil } @@ -1056,6 +1105,9 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) var progress *stageReporter if directives.stageReport { progress = &stageReporter{ + localize: func(content string) string { + return al.localizeUserFacingText(ctx, msg.SessionKey, msg.Content, content) + }, onUpdate: func(content string) { al.bus.PublishOutbound(bus.OutboundMessage{ Channel: msg.Channel, @@ -1064,8 +1116,8 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) }) }, } - progress.Publish(1, 5, "开始", "已接收任务") - progress.Publish(2, 5, "分析", "正在构建上下文") + progress.Publish(1, 5, "start", "I received your task and will clarify the goal and constraints first.") + progress.Publish(2, 5, "analysis", "I am building the context needed for execution.") } // Update tool contexts @@ -1093,13 +1145,13 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) ) if progress != nil { - progress.Publish(3, 5, "执行", "正在运行任务") + progress.Publish(3, 5, "execution", "I am starting step-by-step execution.") } finalContent, iteration, err := al.runLLMToolLoop(ctx, messages, msg.SessionKey, false, progress) if err != nil { if progress != nil { - progress.Publish(5, 5, "失败", err.Error()) + progress.Publish(5, 5, "failure", err.Error()) } return "", err } @@ -1124,7 +1176,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) al.sessions.AddMessage(msg.SessionKey, "user", msg.Content) - // 使用 AddMessageFull 存储包含思考过程或工具调用的完整助手消息 + // Use AddMessageFull to persist the complete assistant message, including thoughts/tool calls. al.sessions.AddMessageFull(msg.SessionKey, providers.Message{ Role: "assistant", Content: userContent, @@ -1147,8 +1199,8 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) }) if progress != nil { - progress.Publish(4, 5, "收敛", "已生成最终回复") - progress.Publish(5, 5, "完成", fmt.Sprintf("任务执行结束(迭代 %d)", iteration)) + progress.Publish(4, 5, "finalization", "Final response is ready.") + progress.Publish(5, 5, "done", fmt.Sprintf("Completed after %d iterations.", iteration)) } return userContent, nil @@ -1216,9 +1268,9 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe // Save to session with system message marker al.sessions.AddMessage(sessionKey, "user", fmt.Sprintf("[System: %s] %s", msg.SenderID, msg.Content)) - // 如果 finalContent 中没有包含 tool calls (即最后一次 LLM 返回的结果) - // 我们已经通过循环内部的 AddMessageFull 存储了前面的步骤 - // 这里的 AddMessageFull 会存储最终回复 + // If finalContent has no tool calls (i.e., the final LLM output), + // earlier steps were already stored via AddMessageFull in the loop. + // This AddMessageFull stores the final reply. al.sessions.AddMessageFull(sessionKey, providers.Message{ Role: "assistant", Content: finalContent, @@ -1256,7 +1308,7 @@ func (al *AgentLoop) runLLMToolLoop( for iteration < al.maxIterations { iteration++ if progress != nil { - progress.Publish(3, 5, "执行", fmt.Sprintf("第 %d 轮推理", iteration)) + progress.Publish(3, 5, "execution", fmt.Sprintf("Running iteration %d.", iteration)) } if !systemMode { @@ -1387,9 +1439,9 @@ func (al *AgentLoop) runLLMToolLoop( } if progress != nil { if err != nil { - progress.Publish(3, 5, "执行", fmt.Sprintf("工具 %s 失败: %v", tc.Name, err)) + progress.Publish(3, 5, "execution", fmt.Sprintf("Tool %s failed: %v", tc.Name, err)) } else { - progress.Publish(3, 5, "执行", fmt.Sprintf("工具 %s 完成", tc.Name)) + progress.Publish(3, 5, "execution", fmt.Sprintf("Tool %s completed.", tc.Name)) } } lastToolResult = result @@ -1596,7 +1648,7 @@ func (al *AgentLoop) RunStartupSelfCheckAllSessions(ctx context.Context) Startup } report.TotalSessions = len(sessions) - // 启动阶段只做历史会话压缩检测,避免额外触发自检任务。 + // During startup, only run historical-session compaction checks to avoid extra self-check tasks. for _, sessionKey := range sessions { select { case <-ctx.Done(): @@ -2627,7 +2679,7 @@ func (al *AgentLoop) handleSlashCommand(ctx context.Context, msg bus.InboundMess ), nil case "/autolearn": if len(fields) < 2 { - return true, al.naturalizeUserFacingText(ctx, "用法:/autolearn start [interval] | /autolearn stop | /autolearn status"), nil + return true, al.naturalizeUserFacingText(ctx, "Usage: /autolearn start [interval] | /autolearn stop | /autolearn status"), nil } switch strings.ToLower(fields[1]) { case "start": @@ -2642,17 +2694,17 @@ func (al *AgentLoop) handleSlashCommand(ctx context.Context, msg bus.InboundMess return true, al.startAutoLearner(ctx, msg, interval), nil case "stop": if al.stopAutoLearner(msg.SessionKey) { - return true, al.naturalizeUserFacingText(ctx, "自动学习已停止。"), nil + return true, al.naturalizeUserFacingText(ctx, "Auto-learn stopped."), nil } - return true, al.naturalizeUserFacingText(ctx, "自动学习当前未运行。"), nil + return true, al.naturalizeUserFacingText(ctx, "Auto-learn is not running."), nil case "status": return true, al.autoLearnerStatus(ctx, msg.SessionKey), nil default: - return true, al.naturalizeUserFacingText(ctx, "用法:/autolearn start [interval] | /autolearn stop | /autolearn status"), nil + return true, al.naturalizeUserFacingText(ctx, "Usage: /autolearn start [interval] | /autolearn stop | /autolearn status"), nil } case "/autonomy": if len(fields) < 2 { - return true, al.naturalizeUserFacingText(ctx, "用法:/autonomy start [idle] | /autonomy stop | /autonomy status"), nil + return true, al.naturalizeUserFacingText(ctx, "Usage: /autonomy start [idle] | /autonomy stop | /autonomy status"), nil } switch strings.ToLower(fields[1]) { case "start": @@ -2672,13 +2724,13 @@ func (al *AgentLoop) handleSlashCommand(ctx context.Context, msg bus.InboundMess return true, al.startAutonomy(ctx, msg, idle, focus), nil case "stop": if al.stopAutonomy(msg.SessionKey) { - return true, al.naturalizeUserFacingText(ctx, "自主模式已关闭。"), nil + return true, al.naturalizeUserFacingText(ctx, "Autonomy mode stopped."), nil } - return true, al.naturalizeUserFacingText(ctx, "自主模式当前未运行。"), nil + return true, al.naturalizeUserFacingText(ctx, "Autonomy mode is not running."), nil case "status": return true, al.autonomyStatus(ctx, msg.SessionKey), nil default: - return true, al.naturalizeUserFacingText(ctx, "用法:/autonomy start [idle] | /autonomy stop | /autonomy status"), nil + return true, al.naturalizeUserFacingText(ctx, "Usage: /autonomy start [idle] | /autonomy stop | /autonomy status"), nil } case "/reload": running, err := al.triggerGatewayReloadFromAgent() diff --git a/pkg/agent/loop_directive_test.go b/pkg/agent/loop_directive_test.go index 14678f3..2093dd1 100644 --- a/pkg/agent/loop_directive_test.go +++ b/pkg/agent/loop_directive_test.go @@ -8,8 +8,8 @@ import ( ) func TestParseTaskExecutionDirectives_RunCommand(t *testing.T) { - d := parseTaskExecutionDirectives("/run 修复构建脚本 --stage-report") - if d.task != "修复构建脚本" { + d := parseTaskExecutionDirectives("/run fix build script --stage-report") + if d.task != "fix build script" { t.Fatalf("unexpected task: %q", d.task) } if !d.stageReport { @@ -18,8 +18,8 @@ func TestParseTaskExecutionDirectives_RunCommand(t *testing.T) { } func TestParseTaskExecutionDirectives_Default(t *testing.T) { - d := parseTaskExecutionDirectives("帮我看看今天的日志异常") - if d.task != "帮我看看今天的日志异常" { + d := parseTaskExecutionDirectives("Please check today's log anomalies") + if d.task != "Please check today's log anomalies" { t.Fatalf("unexpected task: %q", d.task) } if d.stageReport { @@ -75,7 +75,7 @@ func TestParseAutoLearnIntent_StopFallbackCommand(t *testing.T) { } func TestParseAutoLearnIntent_NoNaturalLanguageFallback(t *testing.T) { - if _, ok := parseAutoLearnIntent("请开始自动学习"); ok { + if _, ok := parseAutoLearnIntent("please start auto learning"); ok { t.Fatalf("expected no fallback match") } } @@ -127,7 +127,7 @@ func TestParseAutonomyIdleInterval(t *testing.T) { } func TestParseAutonomyIntent_NoNaturalLanguageFallback(t *testing.T) { - if intent, ok := parseAutonomyIntent("请自动执行这个任务"); ok { + if intent, ok := parseAutonomyIntent("please run this task automatically"); ok { t.Fatalf("expected no intent, got: %+v", intent) } } @@ -158,7 +158,7 @@ func TestExtractJSONObject_Invalid(t *testing.T) { func TestShouldHandleControlIntents_UserMessage(t *testing.T) { msg := bus.InboundMessage{ SenderID: "user", - Content: "请进入自主模式", + Content: "please enter autonomy mode", } if !shouldHandleControlIntents(msg) { t.Fatalf("expected user message to be control-eligible") @@ -168,7 +168,7 @@ func TestShouldHandleControlIntents_UserMessage(t *testing.T) { func TestShouldHandleControlIntents_AutonomySyntheticSender(t *testing.T) { msg := bus.InboundMessage{ SenderID: "autonomy", - Content: "自主模式第 1 轮推进", + Content: "autonomy round 1", } if shouldHandleControlIntents(msg) { t.Fatalf("expected autonomy synthetic message to be ignored for control intents") @@ -178,7 +178,7 @@ func TestShouldHandleControlIntents_AutonomySyntheticSender(t *testing.T) { func TestShouldHandleControlIntents_AutoLearnSyntheticMetadata(t *testing.T) { msg := bus.InboundMessage{ SenderID: "gateway", - Content: "自动学习第 1 轮", + Content: "auto-learn round 1", Metadata: map[string]string{ "source": "autolearn", }, diff --git a/pkg/agent/loop_language_test.go b/pkg/agent/loop_language_test.go index 5bc95b7..e209b6d 100644 --- a/pkg/agent/loop_language_test.go +++ b/pkg/agent/loop_language_test.go @@ -1,24 +1,24 @@ package agent import ( + "context" "errors" - "strings" "testing" "clawgo/pkg/bus" "clawgo/pkg/session" ) -func TestFormatProcessingErrorMessage_ChineseCurrentMessage(t *testing.T) { +func TestFormatProcessingErrorMessage_CurrentMessage(t *testing.T) { al := &AgentLoop{} msg := bus.InboundMessage{ - SessionKey: "s-zh-current", - Content: "请帮我看一下这个错误", + SessionKey: "s-current", + Content: "Please help check this error", } - out := al.formatProcessingErrorMessage(msg, errors.New("boom")) - if !strings.HasPrefix(out, "处理消息时发生错误:") { - t.Fatalf("expected Chinese error prefix, got %q", out) + out := al.formatProcessingErrorMessage(context.Background(), msg, errors.New("boom")) + if out != "Error processing message: boom" { + t.Fatalf("expected formatted error message, got %q", out) } } @@ -29,15 +29,15 @@ func TestFormatProcessingErrorMessage_EnglishCurrentMessage(t *testing.T) { Content: "Please help debug this issue", } - out := al.formatProcessingErrorMessage(msg, errors.New("boom")) - if !strings.HasPrefix(out, "Error processing message:") { - t.Fatalf("expected English error prefix, got %q", out) + out := al.formatProcessingErrorMessage(context.Background(), msg, errors.New("boom")) + if out != "Error processing message: boom" { + t.Fatalf("expected formatted error message, got %q", out) } } -func TestFormatProcessingErrorMessage_UsesSessionHistoryLanguage(t *testing.T) { +func TestFormatProcessingErrorMessage_UsesSessionHistory(t *testing.T) { sm := session.NewSessionManager(t.TempDir()) - sm.AddMessage("s-history", "user", "请继续,按这个方向修复") + sm.AddMessage("s-history", "user", "Please continue fixing in this direction") al := &AgentLoop{sessions: sm} msg := bus.InboundMessage{ @@ -45,8 +45,8 @@ func TestFormatProcessingErrorMessage_UsesSessionHistoryLanguage(t *testing.T) { Content: "ok", } - out := al.formatProcessingErrorMessage(msg, errors.New("boom")) - if !strings.HasPrefix(out, "处理消息时发生错误:") { - t.Fatalf("expected Chinese error prefix from session history, got %q", out) + out := al.formatProcessingErrorMessage(context.Background(), msg, errors.New("boom")) + if out != "Error processing message: boom" { + t.Fatalf("expected formatted error message from session history, got %q", out) } } diff --git a/pkg/channels/base.go b/pkg/channels/base.go index 585bddc..d501a38 100644 --- a/pkg/channels/base.go +++ b/pkg/channels/base.go @@ -82,7 +82,7 @@ func (c *BaseChannel) HandleMessage(senderID, chatID, content string, media []st return } - // 生成 SessionKey: channel:chatID + // Build session key: channel:chatID sessionKey := fmt.Sprintf("%s:%s", c.name, chatID) msg := bus.InboundMessage{ diff --git a/pkg/channels/dingtalk.go b/pkg/channels/dingtalk.go index 102ff94..240b8f0 100644 --- a/pkg/channels/dingtalk.go +++ b/pkg/channels/dingtalk.go @@ -15,7 +15,7 @@ import ( "github.com/open-dingtalk/dingtalk-stream-sdk-go/client" ) -// DingTalkChannel implements the Channel interface for DingTalk (钉钉) +// DingTalkChannel implements the Channel interface for DingTalk. // It uses WebSocket for receiving messages via stream mode and API for sending type DingTalkChannel struct { *BaseChannel diff --git a/pkg/channels/qq.go b/pkg/channels/qq.go index 4512d38..0445bd2 100644 --- a/pkg/channels/qq.go +++ b/pkg/channels/qq.go @@ -49,32 +49,32 @@ func (c *QQChannel) Start(ctx context.Context) error { logger.InfoC("qq", "Starting QQ bot (WebSocket mode)") - // 创建 token source + // Create token source credentials := &token.QQBotCredentials{ AppID: c.config.AppID, AppSecret: c.config.AppSecret, } c.tokenSource = token.NewQQBotTokenSource(credentials) - // 创建子 context + // Create child context runCtx, cancel := context.WithCancel(ctx) c.runCancel.set(cancel) - // 启动自动刷新 token 协程 + // Start token auto-refresh goroutine if err := token.StartRefreshAccessToken(runCtx, c.tokenSource); err != nil { return fmt.Errorf("failed to start token refresh: %w", err) } - // 初始化 OpenAPI 客户端 + // Initialize OpenAPI client c.api = botgo.NewOpenAPI(c.config.AppID, c.tokenSource).WithTimeout(5 * time.Second) - // 注册事件处理器 + // Register event handlers intent := event.RegisterHandlers( c.handleC2CMessage(), c.handleGroupATMessage(), ) - // 获取 WebSocket 接入点 + // Get WebSocket endpoint wsInfo, err := c.api.WS(runCtx, nil, "") if err != nil { return fmt.Errorf("failed to get websocket info: %w", err) @@ -84,10 +84,10 @@ func (c *QQChannel) Start(ctx context.Context) error { "shards": wsInfo.Shards, }) - // 创建并保存 sessionManager + // Create and store session manager c.sessionManager = botgo.NewSessionManager() - // 在 goroutine 中启动 WebSocket 连接,避免阻塞 + // Start WebSocket connection in a goroutine to avoid blocking runChannelTask("qq", "websocket session", func() error { return c.sessionManager.Start(wsInfo, c.tokenSource, &intent) }, func(_ error) { @@ -116,12 +116,12 @@ func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { return fmt.Errorf("QQ bot not running") } - // 构造消息 + // Build message msgToCreate := &dto.MessageToCreate{ Content: msg.Content, } - // C2C 消息发送 + // Send C2C message _, err := c.api.PostC2CMessage(ctx, msg.ChatID, msgToCreate) if err != nil { logger.ErrorCF("qq", "Failed to send C2C message", map[string]interface{}{ @@ -133,15 +133,15 @@ func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { return nil } -// handleC2CMessage 处理 QQ 私聊消息 +// handleC2CMessage handles QQ private messages func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler { return func(event *dto.WSPayload, data *dto.WSC2CMessageData) error { - // 去重检查 + // Deduplication check if c.isDuplicate(data.ID) { return nil } - // 提取用户信息 + // Extract sender information var senderID string if data.Author != nil && data.Author.ID != "" { senderID = data.Author.ID @@ -150,7 +150,7 @@ func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler { return nil } - // 提取消息内容 + // Extract message content content := data.Content if content == "" { logger.DebugC("qq", "Received empty message, ignoring") @@ -163,7 +163,7 @@ func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler { logger.FieldMessageContentLength: len(content), }) - // 转发到消息总线 + // Forward to message bus metadata := map[string]string{ "message_id": data.ID, } @@ -174,15 +174,15 @@ func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler { } } -// handleGroupATMessage 处理群@消息 +// handleGroupATMessage handles group @ messages func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler { return func(event *dto.WSPayload, data *dto.WSGroupATMessageData) error { - // 去重检查 + // Deduplication check if c.isDuplicate(data.ID) { return nil } - // 提取用户信息 + // Extract sender information var senderID string if data.Author != nil && data.Author.ID != "" { senderID = data.Author.ID @@ -191,7 +191,7 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler { return nil } - // 提取消息内容(去掉 @ 机器人部分) + // Extract message content (remove bot mention) content := data.Content if content == "" { logger.DebugC("qq", "Received empty group message, ignoring") @@ -204,7 +204,7 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler { logger.FieldMessageContentLength: len(content), }) - // 转发到消息总线(使用 GroupID 作为 ChatID) + // Forward to message bus (use GroupID as ChatID) metadata := map[string]string{ "message_id": data.ID, "group_id": data.GroupID, @@ -216,7 +216,7 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler { } } -// isDuplicate 检查消息是否重复 +// isDuplicate checks whether a message is duplicated func (c *QQChannel) isDuplicate(messageID string) bool { c.mu.Lock() defer c.mu.Unlock() @@ -227,9 +227,9 @@ func (c *QQChannel) isDuplicate(messageID string) bool { c.processedIDs[messageID] = true - // 简单清理:限制 map 大小 + // Simple cleanup: limit map size if len(c.processedIDs) > 10000 { - // 清空一半 + // Remove half of the entries count := 0 for id := range c.processedIDs { if count >= 5000 { diff --git a/pkg/cron/cron_expr.go b/pkg/cron/cron_expr.go index e1c0b66..611a11f 100644 --- a/pkg/cron/cron_expr.go +++ b/pkg/cron/cron_expr.go @@ -6,8 +6,8 @@ import ( "time" ) -// SimpleCronParser 实现了一个极简的 Cron 表达式解析器 (minute hour day month dayOfWeek) -// 借鉴 goclaw 可能使用的标准 cron 逻辑,补全 ClawGo 缺失的 Expr 调度能力 +// SimpleCronParser is a minimal cron expression parser (minute hour day month dayOfWeek). +// It approximates standard cron behavior to provide missing Expr scheduling support. type SimpleCronParser struct { expr string } @@ -19,14 +19,15 @@ func NewSimpleCronParser(expr string) *SimpleCronParser { func (p *SimpleCronParser) Next(from time.Time) time.Time { fields := strings.Fields(p.expr) if len(fields) != 5 { - return time.Time{} // 格式错误 + return time.Time{} // Invalid format } - // 这是一个极简实现,仅支持 * 和 数字 - // 生产环境下建议引入 github.com/robfig/cron/v3 + // This minimal implementation only supports "*" and exact numbers. + // For production, use github.com/robfig/cron/v3. next := from.Add(1 * time.Minute).Truncate(time.Minute) - - // 这里逻辑简化:如果不是 * 且不匹配,则递增直到匹配 (最大搜索 1 年) + + // Simplified logic: if it is not "*" and does not match, keep incrementing until matched + // (up to one year of search). for i := 0; i < 525600; i++ { if p.match(next, fields) { return next diff --git a/pkg/session/manager.go b/pkg/session/manager.go index 71756c9..c2ddf2e 100644 --- a/pkg/session/manager.go +++ b/pkg/session/manager.go @@ -92,7 +92,7 @@ func (sm *SessionManager) AddMessageFull(sessionKey string, msg providers.Messag session.Updated = time.Now() session.mu.Unlock() - // 立即持久化 (Append-only) + // Persist immediately (append-only) if err := sm.appendMessage(sessionKey, msg); err != nil { logger.ErrorCF("session", "Failed to persist session message", map[string]interface{}{ "session_key": sessionKey, @@ -277,8 +277,8 @@ func (sm *SessionManager) CompactHistory(key, summary string, keepLast int) (int } func (sm *SessionManager) Save(session *Session) error { - // 现已通过 AddMessageFull 实时增量持久化 - // 这里保留 Save 方法用于更新 Summary 等元数据 + // Messages are now persisted incrementally via AddMessageFull. + // Keep Save for summary and other metadata updates. if sm.storage == "" { return nil } @@ -307,7 +307,7 @@ func (sm *SessionManager) loadSessions() error { continue } - // 处理 JSONL 历史消息 + // Load JSONL history messages if filepath.Ext(file.Name()) == ".jsonl" { sessionKey := strings.TrimSuffix(file.Name(), ".jsonl") session := sm.GetOrCreate(sessionKey) @@ -334,7 +334,7 @@ func (sm *SessionManager) loadSessions() error { _ = f.Close() } - // 处理元数据 + // Load metadata if filepath.Ext(file.Name()) == ".meta" { sessionKey := strings.TrimSuffix(file.Name(), ".meta") session := sm.GetOrCreate(sessionKey) diff --git a/pkg/skills/loader.go b/pkg/skills/loader.go index b5436b6..50b4134 100644 --- a/pkg/skills/loader.go +++ b/pkg/skills/loader.go @@ -23,9 +23,9 @@ type SkillInfo struct { type SkillsLoader struct { workspace string - workspaceSkills string // workspace skills (项目级别) - globalSkills string // 全局 skills (~/.clawgo/skills) - builtinSkills string // 内置 skills + workspaceSkills string // workspace-level skills + globalSkills string // global skills (~/.clawgo/skills) + builtinSkills string // built-in skills } func NewSkillsLoader(workspace string, globalSkills string, builtinSkills string) *SkillsLoader { @@ -62,14 +62,14 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo { } } - // 全局 skills (~/.clawgo/skills) - 被 workspace skills 覆盖 + // Global skills (~/.clawgo/skills) - overridden by workspace skills if sl.globalSkills != "" { if dirs, err := os.ReadDir(sl.globalSkills); err == nil { for _, dir := range dirs { if dir.IsDir() { skillFile := filepath.Join(sl.globalSkills, dir.Name(), "SKILL.md") if _, err := os.Stat(skillFile); err == nil { - // 检查是否已被 workspace skills 覆盖 + // Check whether overridden by workspace skills exists := false for _, s := range skills { if s.Name == dir.Name() && s.Source == "workspace" { @@ -103,7 +103,7 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo { if dir.IsDir() { skillFile := filepath.Join(sl.builtinSkills, dir.Name(), "SKILL.md") if _, err := os.Stat(skillFile); err == nil { - // 检查是否已被 workspace 或 global skills 覆盖 + // Check whether overridden by workspace or global skills exists := false for _, s := range skills { if s.Name == dir.Name() && (s.Source == "workspace" || s.Source == "global") { @@ -135,7 +135,7 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo { } func (sl *SkillsLoader) LoadSkill(name string) (string, bool) { - // 1. 优先从 workspace skills 加载(项目级别) + // 1. Prefer workspace skills (project-level) if sl.workspaceSkills != "" { skillFile := filepath.Join(sl.workspaceSkills, name, "SKILL.md") if content, err := os.ReadFile(skillFile); err == nil { @@ -143,7 +143,7 @@ func (sl *SkillsLoader) LoadSkill(name string) (string, bool) { } } - // 2. 其次从全局 skills 加载 (~/.clawgo/skills) + // 2. Then load from global skills (~/.clawgo/skills) if sl.globalSkills != "" { skillFile := filepath.Join(sl.globalSkills, name, "SKILL.md") if content, err := os.ReadFile(skillFile); err == nil { @@ -151,7 +151,7 @@ func (sl *SkillsLoader) LoadSkill(name string) (string, bool) { } } - // 3. 最后从内置 skills 加载 + // 3. Finally load from built-in skills if sl.builtinSkills != "" { skillFile := filepath.Join(sl.builtinSkills, name, "SKILL.md") if content, err := os.ReadFile(skillFile); err == nil { diff --git a/pkg/tools/memory.go b/pkg/tools/memory.go index d88b43f..22d418a 100644 --- a/pkg/tools/memory.go +++ b/pkg/tools/memory.go @@ -84,7 +84,7 @@ func (t *MemorySearchTool) Execute(ctx context.Context, args map[string]interfac resultsChan := make(chan []searchResult, len(files)) var wg sync.WaitGroup - // 并发搜索所有文件 + // Search all files concurrently for _, file := range files { wg.Add(1) go func(f string) { @@ -96,7 +96,7 @@ func (t *MemorySearchTool) Execute(ctx context.Context, args map[string]interfac }(file) } - // 异步关闭通道 + // Close channel asynchronously go func() { wg.Wait() close(resultsChan) diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index c14632c..304faaa 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -119,7 +119,7 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) (st } func (t *ExecTool) executeInSandbox(ctx context.Context, command, cwd string) (string, error) { - // 实现 Docker 沙箱执行逻辑 + // Execute command inside Docker sandbox absCwd, _ := filepath.Abs(cwd) dockerArgs := []string{ "run", "--rm", diff --git a/pkg/tools/subagent.go b/pkg/tools/subagent.go index a70132d..e6a012a 100644 --- a/pkg/tools/subagent.go +++ b/pkg/tools/subagent.go @@ -89,11 +89,11 @@ func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask) { _ = sm.orc.MarkTaskRunning(task.PipelineID, task.PipelineTask) } - // 1. 独立 Agent 逻辑:支持递归工具调用 - // 这里简单实现:通过共享 AgentLoop 的逻辑来实现 full subagent 能力 - // 但目前 subagent.go 不方便反向依赖 agent 包,我们暂时通过 Inject 方式解决 + // 1. Independent agent logic: supports recursive tool calling. + // This lightweight approach reuses AgentLoop logic for full subagent capability. + // subagent.go cannot depend on agent package inversely, so use function injection. - // 如果没有注入 RunFunc,则退化为简单的一步 Chat + // Fall back to one-shot chat when RunFunc is not injected. if sm.runFunc != nil { result, err := sm.runFunc(ctx, task.Task, task.OriginChannel, task.OriginChatID) sm.mu.Lock() @@ -112,7 +112,7 @@ func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask) { } sm.mu.Unlock() } else { - // 原有的 One-shot 逻辑 + // Original one-shot logic messages := []providers.Message{ { Role: "system", @@ -145,7 +145,7 @@ func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask) { sm.mu.Unlock() } - // 2. 结果广播 (原有逻辑保持) + // 2. Result broadcast (keep existing behavior) if sm.bus != nil { prefix := "Task completed" if task.Label != "" {