This commit is contained in:
lpf
2026-02-26 15:45:51 +08:00
parent 879bf83102
commit 06c7f2b53f
7 changed files with 101 additions and 102 deletions

View File

@@ -29,8 +29,8 @@ func getGlobalConfigDir() string {
}
func NewContextBuilder(workspace string, toolsSummaryFunc func() []string) *ContextBuilder {
// builtin skills: 当前项目的 skills 目录
// 使用当前工作目录下的 skills/ 目录
// Built-in skills: use the current project's skills directory.
// Resolve from current working directory: skills/
wd, _ := os.Getwd()
builtinSkillsDir := filepath.Join(wd, "skills")
globalSkillsDir := filepath.Join(getGlobalConfigDir(), "skills")

View File

@@ -79,9 +79,9 @@ func ExtractLanguagePreference(text string) string {
return ""
}
enHints := []string{"speak english", "reply in english", "use english", "以后用英文", "请用英文", "用英文"}
zhHints := []string{"说中文", "用中文", "请用中文", "reply in chinese", "speak chinese"}
jaHints := []string{"日本語", "reply in japanese", "speak japanese"}
enHints := []string{"speak english", "reply in english", "use english"}
zhHints := []string{"reply in chinese", "speak chinese", "use chinese"}
jaHints := []string{"reply in japanese", "speak japanese", "use japanese"}
koHints := []string{"한국어", "reply in korean", "speak korean"}
for _, h := range enHints {

View File

@@ -235,9 +235,9 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
sessionRunLocks: map[string]*sync.Mutex{},
}
// 注入递归运行逻辑,使 subagent 具备 full tool-calling 能力
// Inject recursive run logic so subagents can use full tool-calling flows.
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/randomized key to reduce session key collisions.
return loop.ProcessDirect(ctx, task, sessionKey)
})
@@ -629,7 +629,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
})
}
messages = append(messages, assistantMsg)
// 持久化包含 ToolCalls 的助手消息
// Persist assistant message with tool calls.
al.sessions.AddMessageFull(msg.SessionKey, assistantMsg)
for _, tc := range response.ToolCalls {
@@ -652,7 +652,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
ToolCallID: tc.ID,
}
messages = append(messages, toolResultMsg)
// 持久化工具返回结果
// Persist tool result message.
al.sessions.AddMessageFull(msg.SessionKey, toolResultMsg)
}
}
@@ -673,7 +673,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
if iteration == 1 {
fallback := strings.TrimSpace(al.thinkOnlyFallback)
if fallback == "" {
fallback = "已完成思考流程。"
fallback = "Thinking process completed."
}
userContent = fallback
}
@@ -686,7 +686,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
}
al.sessions.AddMessage(msg.SessionKey, "user", msg.Content)
// 使用 AddMessageFull 存储包含思考过程或工具调用的完整助手消息
// Persist full assistant response (including reasoning/tool flow outcomes when present).
al.sessions.AddMessageFull(msg.SessionKey, providers.Message{
Role: "assistant",
Content: userContent,
@@ -716,15 +716,15 @@ func (al *AgentLoop) updateIntentHint(sessionKey, content string) {
lower := strings.ToLower(content)
// Cron natural-language intent: avoid searching project files for user timer ops.
if strings.Contains(lower, "定时") || strings.Contains(lower, "定时任务") || strings.Contains(lower, "cron") || strings.Contains(lower, "schedule") {
hint := "优先使用 cron 工具处理定时任务:查看=action=list;删除=action=delete(id);启停=action=enable/disable。不要改为在项目目录中搜索 cron 文本。"
if strings.Contains(lower, "cron") || strings.Contains(lower, "schedule") || strings.Contains(lower, "timer") || strings.Contains(lower, "reminder") {
hint := "Prioritize the cron tool for timer operations: list=action=list; delete=action=delete(id); enable/disable=action=enable/disable. Do not switch to grepping project files for cron text."
al.intentMu.Lock()
al.intentHints[sessionKey] = hint + " 用户补充=" + content
al.intentHints[sessionKey] = hint + " User details=" + content
al.intentMu.Unlock()
return
}
if !strings.Contains(lower, "提交") && !strings.Contains(lower, "推送") && !strings.Contains(lower, "commit") && !strings.Contains(lower, "push") {
if !strings.Contains(lower, "commit") && !strings.Contains(lower, "push") {
if strings.HasPrefix(content, "1.") || strings.HasPrefix(content, "2.") {
al.intentMu.Lock()
if prev := strings.TrimSpace(al.intentHints[sessionKey]); prev != "" {
@@ -734,12 +734,12 @@ func (al *AgentLoop) updateIntentHint(sessionKey, content string) {
}
return
}
hint := "执行事务: commit+push 一次闭环,包含分支/范围确认。"
if strings.Contains(lower, "所有分支") || strings.Contains(lower, "all branches") {
hint += " 范围=所有分支。"
hint := "Execute as one transaction: complete commit+push in one pass with branch/scope confirmation."
if strings.Contains(lower, "all branches") {
hint += " Scope=all branches."
}
al.intentMu.Lock()
al.intentHints[sessionKey] = hint + " 用户补充=" + content
al.intentHints[sessionKey] = hint + " User details=" + content
al.intentMu.Unlock()
}
@@ -751,7 +751,7 @@ func (al *AgentLoop) applyIntentHint(sessionKey, content string) string {
return content
}
lower := strings.ToLower(strings.TrimSpace(content))
if strings.Contains(lower, "提交") || strings.Contains(lower, "推送") || strings.HasPrefix(content, "1.") || strings.HasPrefix(content, "2.") || strings.Contains(lower, "定时") || strings.Contains(lower, "cron") || strings.Contains(lower, "schedule") {
if strings.Contains(lower, "commit") || strings.Contains(lower, "push") || strings.HasPrefix(content, "1.") || strings.HasPrefix(content, "2.") || strings.Contains(lower, "cron") || strings.Contains(lower, "schedule") || strings.Contains(lower, "timer") || strings.Contains(lower, "reminder") {
return "[Intent Slot]\n" + hint + "\n\n[User Message]\n" + content
}
return content
@@ -887,7 +887,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe
})
}
messages = append(messages, assistantMsg)
// 持久化包含 ToolCalls 的助手消息
// Persist assistant message with tool calls.
al.sessions.AddMessageFull(sessionKey, assistantMsg)
for _, tc := range response.ToolCalls {
@@ -902,7 +902,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe
ToolCallID: tc.ID,
}
messages = append(messages, toolResultMsg)
// 持久化工具返回结果
// Persist tool result message.
al.sessions.AddMessageFull(sessionKey, toolResultMsg)
}
}
@@ -914,9 +914,8 @@ 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 (last LLM turn is direct text),
// earlier steps were already persisted in-loop; this stores the final reply.
al.sessions.AddMessageFull(sessionKey, providers.Message{
Role: "assistant",
Content: finalContent,
@@ -1110,7 +1109,7 @@ func shouldRecallMemory(text string, keywords []string) bool {
return false
}
if len(keywords) == 0 {
keywords = []string{"remember", "记得", "上次", "之前", "偏好", "preference", "todo", "待办", "决定", "decision", "日期", "when did", "what did we"}
keywords = []string{"remember", "preference", "todo", "decision", "date", "when did", "what did we"}
}
for _, k := range keywords {
kk := strings.ToLower(strings.TrimSpace(k))

View File

@@ -20,23 +20,23 @@ import (
)
type Options struct {
Enabled bool
TickIntervalSec int
MinRunIntervalSec int
MaxPendingDurationSec int
MaxConsecutiveStalls int
MaxDispatchPerTick int
Workspace string
DefaultNotifyChannel string
DefaultNotifyChatID string
NotifyCooldownSec int
Enabled bool
TickIntervalSec int
MinRunIntervalSec int
MaxPendingDurationSec int
MaxConsecutiveStalls int
MaxDispatchPerTick int
Workspace string
DefaultNotifyChannel string
DefaultNotifyChatID string
NotifyCooldownSec int
NotifySameReasonCooldownSec int
QuietHours string
UserIdleResumeSec int
WaitingResumeDebounceSec int
ImportantKeywords []string
CompletionTemplate string
BlockedTemplate string
QuietHours string
UserIdleResumeSec int
WaitingResumeDebounceSec int
ImportantKeywords []string
CompletionTemplate string
BlockedTemplate string
}
type taskState struct {
@@ -436,10 +436,10 @@ func normalizeResourceKeys(keys []string) []string {
}
type todoItem struct {
ID string
Content string
Priority string
DueAt string
ID string
Content string
Priority string
DueAt string
DedupeHits int
}
@@ -549,7 +549,7 @@ func (e *Engine) sendCompletionNotification(st *taskState) {
}
tpl := strings.TrimSpace(e.opts.CompletionTemplate)
if tpl == "" {
tpl = "✅ 已完成:%s\n下一步如需我继续处理后续直接回复“继续 %s”"
tpl = "✅ Completed: %s\nNext step: reply \"continue %s\" if you want me to proceed."
}
e.bus.PublishOutbound(bus.OutboundMessage{
Channel: e.opts.DefaultNotifyChannel,
@@ -566,7 +566,7 @@ func (e *Engine) sendFailureNotification(st *taskState, reason string) {
}
tpl := strings.TrimSpace(e.opts.BlockedTemplate)
if tpl == "" {
tpl = "⚠️ 任务受阻:%s\n原因%s\n建议回复“继续 %s”我会按当前状态重试。"
tpl = "⚠️ Task blocked: %s\nReason: %s\nSuggestion: reply \"continue %s\" and I will retry from current state."
}
e.bus.PublishOutbound(bus.OutboundMessage{
Channel: e.opts.DefaultNotifyChannel,
@@ -942,7 +942,7 @@ func (e *Engine) isHighValueCompletion(st *taskState) bool {
s := strings.ToLower(strings.TrimSpace(st.Content))
keywords := e.opts.ImportantKeywords
if len(keywords) == 0 {
keywords = []string{"urgent", "重要", "付款", "payment", "上线", "release", "deadline", "截止"}
keywords = []string{"urgent", "payment", "release", "deadline", "p0", "asap"}
}
for _, k := range keywords {
kk := strings.ToLower(strings.TrimSpace(k))

View File

@@ -45,19 +45,19 @@ type AgentDefaults struct {
}
type AutonomyConfig struct {
Enabled bool `json:"enabled" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_ENABLED"`
TickIntervalSec int `json:"tick_interval_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_TICK_INTERVAL_SEC"`
MinRunIntervalSec int `json:"min_run_interval_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MIN_RUN_INTERVAL_SEC"`
MaxPendingDurationSec int `json:"max_pending_duration_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_PENDING_DURATION_SEC"`
MaxConsecutiveStalls int `json:"max_consecutive_stalls" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_CONSECUTIVE_STALLS"`
MaxDispatchPerTick int `json:"max_dispatch_per_tick" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_DISPATCH_PER_TICK"`
NotifyCooldownSec int `json:"notify_cooldown_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_COOLDOWN_SEC"`
NotifySameReasonCooldownSec int `json:"notify_same_reason_cooldown_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_SAME_REASON_COOLDOWN_SEC"`
QuietHours string `json:"quiet_hours" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_QUIET_HOURS"`
UserIdleResumeSec int `json:"user_idle_resume_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_USER_IDLE_RESUME_SEC"`
WaitingResumeDebounceSec int `json:"waiting_resume_debounce_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_WAITING_RESUME_DEBOUNCE_SEC"`
NotifyChannel string `json:"notify_channel" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_CHANNEL"`
NotifyChatID string `json:"notify_chat_id" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_CHAT_ID"`
Enabled bool `json:"enabled" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_ENABLED"`
TickIntervalSec int `json:"tick_interval_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_TICK_INTERVAL_SEC"`
MinRunIntervalSec int `json:"min_run_interval_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MIN_RUN_INTERVAL_SEC"`
MaxPendingDurationSec int `json:"max_pending_duration_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_PENDING_DURATION_SEC"`
MaxConsecutiveStalls int `json:"max_consecutive_stalls" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_CONSECUTIVE_STALLS"`
MaxDispatchPerTick int `json:"max_dispatch_per_tick" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_DISPATCH_PER_TICK"`
NotifyCooldownSec int `json:"notify_cooldown_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_COOLDOWN_SEC"`
NotifySameReasonCooldownSec int `json:"notify_same_reason_cooldown_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_SAME_REASON_COOLDOWN_SEC"`
QuietHours string `json:"quiet_hours" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_QUIET_HOURS"`
UserIdleResumeSec int `json:"user_idle_resume_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_USER_IDLE_RESUME_SEC"`
WaitingResumeDebounceSec int `json:"waiting_resume_debounce_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_WAITING_RESUME_DEBOUNCE_SEC"`
NotifyChannel string `json:"notify_channel" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_CHANNEL"`
NotifyChatID string `json:"notify_chat_id" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_CHAT_ID"`
}
type AgentTextConfig struct {
@@ -304,42 +304,42 @@ func DefaultConfig() *Config {
Temperature: 0.7,
MaxToolIterations: 20,
Heartbeat: HeartbeatConfig{
Enabled: true,
EverySec: 30 * 60,
AckMaxChars: 64,
Enabled: true,
EverySec: 30 * 60,
AckMaxChars: 64,
PromptTemplate: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.",
},
Autonomy: AutonomyConfig{
Enabled: false,
TickIntervalSec: 30,
MinRunIntervalSec: 20,
MaxPendingDurationSec: 180,
MaxConsecutiveStalls: 3,
MaxDispatchPerTick: 2,
NotifyCooldownSec: 300,
Enabled: false,
TickIntervalSec: 30,
MinRunIntervalSec: 20,
MaxPendingDurationSec: 180,
MaxConsecutiveStalls: 3,
MaxDispatchPerTick: 2,
NotifyCooldownSec: 300,
NotifySameReasonCooldownSec: 900,
QuietHours: "23:00-08:00",
UserIdleResumeSec: 20,
WaitingResumeDebounceSec: 5,
NotifyChannel: "",
NotifyChatID: "",
QuietHours: "23:00-08:00",
UserIdleResumeSec: 20,
WaitingResumeDebounceSec: 5,
NotifyChannel: "",
NotifyChatID: "",
},
Texts: AgentTextConfig{
NoResponseFallback: "I've completed processing but have no response to give.",
ThinkOnlyFallback: "Thinking process completed.",
MemoryRecallKeywords: []string{"remember", "记得", "上次", "之前", "偏好", "preference", "todo", "待办", "决定", "decision", "日期", "when did", "what did we"},
LangUsage: "Usage: /lang <code>",
LangInvalid: "Invalid language code.",
LangUpdatedTemplate: "Language preference updated to %s",
SubagentsNone: "No subagents.",
SessionsNone: "No sessions.",
UnsupportedAction: "unsupported action",
NoResponseFallback: "I've completed processing but have no response to give.",
ThinkOnlyFallback: "Thinking process completed.",
MemoryRecallKeywords: []string{"remember", "previous", "preference", "todo", "decision", "date", "when did", "what did we"},
LangUsage: "Usage: /lang <code>",
LangInvalid: "Invalid language code.",
LangUpdatedTemplate: "Language preference updated to %s",
SubagentsNone: "No subagents.",
SessionsNone: "No sessions.",
UnsupportedAction: "unsupported action",
SystemRewriteTemplate: "Rewrite the following internal system update in concise user-facing language:\n\n%s",
RuntimeCompactionNote: "[runtime-compaction] removed %d old messages, kept %d recent messages",
StartupCompactionNote: "[startup-compaction] removed %d old messages, kept %d recent messages",
AutonomyImportantKeywords: []string{"urgent", "重要", "付款", "payment", "上线", "release", "deadline", "截止"},
AutonomyCompletionTemplate: "✅ 已完成:%s\n回复“继续 %s”可继续下一步。",
AutonomyBlockedTemplate: "⚠️ 任务受阻:%s%s\n回复“继续 %s”我会重试。",
AutonomyImportantKeywords: []string{"urgent", "payment", "release", "deadline", "p0", "asap"},
AutonomyCompletionTemplate: "✅ Completed: %s\nReply \"continue %s\" to proceed to the next step.",
AutonomyBlockedTemplate: "⚠️ Task blocked: %s (%s)\nReply \"continue %s\" and I will retry.",
},
ContextCompaction: ContextCompactionConfig{
Enabled: true,

View File

@@ -1418,16 +1418,16 @@ func (s *RegistryServer) checkAuth(r *http.Request) bool {
func hotReloadFieldInfo() []map[string]interface{} {
return []map[string]interface{}{
{"path": "logging.*", "name": "日志配置", "description": "日志等级、落盘等"},
{"path": "sentinel.*", "name": "哨兵配置", "description": "健康检查与自动修复"},
{"path": "agents.*", "name": "Agent 配置", "description": "模型、策略、默认行为"},
{"path": "providers.*", "name": "Provider 配置", "description": "LLM 提供商与代理"},
{"path": "tools.*", "name": "工具配置", "description": "工具开关、执行参数"},
{"path": "channels.*", "name": "渠道配置", "description": "Telegram/其它渠道参数"},
{"path": "cron.*", "name": "定时任务配置", "description": "cron 全局运行参数"},
{"path": "agents.defaults.heartbeat.*", "name": "心跳策略", "description": "心跳频率、提示词"},
{"path": "agents.defaults.autonomy.*", "name": "自治策略", "description": "自治任务开关与限流"},
{"path": "gateway.*", "name": "网关配置", "description": "多数可热更,监听地址/端口可能需重启"},
{"path": "logging.*", "name": "Logging", "description": "Log level, persistence, and related settings"},
{"path": "sentinel.*", "name": "Sentinel", "description": "Health checks and auto-heal behavior"},
{"path": "agents.*", "name": "Agent", "description": "Models, policies, and default behavior"},
{"path": "providers.*", "name": "Providers", "description": "LLM providers and proxy settings"},
{"path": "tools.*", "name": "Tools", "description": "Tool toggles and runtime options"},
{"path": "channels.*", "name": "Channels", "description": "Telegram and other channel settings"},
{"path": "cron.*", "name": "Cron", "description": "Global cron runtime settings"},
{"path": "agents.defaults.heartbeat.*", "name": "Heartbeat", "description": "Heartbeat interval and prompt template"},
{"path": "agents.defaults.autonomy.*", "name": "Autonomy", "description": "Autonomy toggles and throttling"},
{"path": "gateway.*", "name": "Gateway", "description": "Mostly hot-reloadable; host/port may require restart"},
}
}

View File

@@ -108,7 +108,7 @@ func (sm *SessionManager) AddMessageFull(sessionKey string, msg providers.Messag
session.Updated = time.Now()
session.mu.Unlock()
// 立即持久化 (Append-only)
// Persist immediately (append-only).
sm.appendMessage(sessionKey, msg)
}
@@ -323,8 +323,8 @@ func toOpenClawMessageEvent(msg providers.Message) openClawEvent {
Type: "message",
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
Message: &struct {
Role string `json:"role"`
Content []struct {
Role string `json:"role"`
Content []struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
} `json:"content,omitempty"`