This commit is contained in:
lpf
2026-02-16 23:56:54 +08:00
parent 2f0d8503b4
commit fb404cebf1
6 changed files with 604 additions and 292 deletions

View File

@@ -1,4 +1,4 @@
.PHONY: all build install uninstall clean help test
.PHONY: all build install uninstall clean help test install-bootstrap-docs
# Build variables
BINARY_NAME=clawgo
@@ -100,8 +100,50 @@ install: build
fi; \
fi; \
done
@$(MAKE) install-bootstrap-docs
@echo "Installation complete!"
## install-bootstrap-docs: Incrementally sync AGENTS.md/SOUL.md/USER.md into workspace
install-bootstrap-docs:
@echo "Incrementally syncing bootstrap docs to $(WORKSPACE_DIR)..."
@mkdir -p $(WORKSPACE_DIR)
@for f in AGENTS.md SOUL.md USER.md; do \
src="$(CURDIR)/$$f"; \
dst="$(WORKSPACE_DIR)/$$f"; \
if [ ! -f "$$src" ]; then \
continue; \
fi; \
if [ ! -f "$$dst" ]; then \
cp "$$src" "$$dst"; \
echo " ✓ Added $$f"; \
continue; \
fi; \
begin="# >>> CLAWGO MANAGED BLOCK: $$f >>>"; \
end="# <<< CLAWGO MANAGED BLOCK: $$f <<<"; \
tmp="$$(mktemp)"; \
if grep -Fq "$$begin" "$$dst"; then \
awk -v b="$$begin" -v e="$$end" -v src="$$src" '\
BEGIN { in_block = 0 } \
$$0 == b { \
print; \
while ((getline line < src) > 0) print line; \
close(src); \
in_block = 1; \
next; \
} \
$$0 == e { in_block = 0; print; next } \
!in_block { print } \
' "$$dst" > "$$tmp"; \
else \
cat "$$dst" > "$$tmp"; \
printf "\n%s\n" "$$begin" >> "$$tmp"; \
cat "$$src" >> "$$tmp"; \
printf "\n%s\n" "$$end" >> "$$tmp"; \
fi; \
mv "$$tmp" "$$dst"; \
echo " ✓ Updated $$f (incremental)"; \
done
## install-user: Install clawgo to ~/.local and copy builtin skills
install-user:
@$(MAKE) install INSTALL_PREFIX=$(USER_HOME)/.local

View File

@@ -83,11 +83,37 @@ export CLAWGO_CONFIG=/path/to/config.json
/reload
```
自治控制命令(可选,推荐直接自然语言):
```text
/autonomy start [idle]
/autonomy stop
/autonomy status
/autolearn start [interval]
/autolearn stop
/autolearn status
```
消息调度策略(按会话 `session_key`
- 同一会话严格 FIFO 串行执行,后续消息进入队列等待。
- `/stop` 会立即中断当前回复,并继续处理队列中的下一条消息。
- 不同会话可并发执行,互不影响。
## 🧭 自主模式与自然语言控制
- 自主模式/自动学习控制采用 **LLM 语义解析优先**(多语言),不依赖固定中文关键词。
- 规则解析仅作为兜底(如显式命令:`/autonomy ...``/autolearn ...`)。
- 开启自主模式时若附带研究方向,系统会优先按该方向执行;当用户表示方向完成后,会自动切换到其他高价值任务继续推进。
- 进度回报使用自然语言,不使用固定阶段编号模板。
系统会在启动时读取 `AGENTS.md``SOUL.md``USER.md` 作为行为约束与语义解析上下文。
## 🧩 Onboard/Install 文档同步
- `clawgo onboard``make install` 都会同步 `AGENTS.md``SOUL.md``USER.md` 到工作区。
- 若文件不存在:创建。
- 若文件已存在:仅更新 `CLAWGO MANAGED BLOCK` 受管区块,保留用户自定义内容(增量更新,不整文件覆盖)。
## 🧾 日志链路
默认启用文件日志,并支持自动分割和过期清理(默认保留 3 天):
@@ -131,6 +157,19 @@ Sentinel 会周期巡检关键运行资源配置、memory、日志目录
}
```
Cron 调度策略支持配置化(支持热更新):
```json
"cron": {
"min_sleep_sec": 1,
"max_sleep_sec": 30,
"retry_backoff_base_sec": 30,
"retry_backoff_max_sec": 1800,
"max_consecutive_failure_retries": 5,
"max_workers": 4
}
```
Shell 工具默认启用 Risk Gate。检测到破坏性命令时默认阻断并要求 `force=true`,可先做 dry-run
```json

View File

@@ -83,11 +83,37 @@ Slash commands are also supported in chat channels:
/reload
```
Autonomy control commands (optional; natural language is recommended):
```text
/autonomy start [idle]
/autonomy stop
/autonomy status
/autolearn start [interval]
/autolearn stop
/autolearn status
```
Message scheduling policy (per `session_key`):
- Same session runs in strict FIFO order; later messages are queued.
- `/stop` immediately cancels the current response, then processing continues with the next queued message.
- Different sessions can run concurrently.
## 🧭 Autonomy Mode & Natural-Language Control
- Autonomy/auto-learning controls are interpreted with **LLM semantic parsing first** (multi-language), instead of fixed keyword matching.
- Rule-based parsing is used only as a fallback (for explicit forms such as `/autonomy ...` and `/autolearn ...`).
- If a focus direction is provided when starting autonomy mode, execution prioritizes that focus; when user says it is complete, the agent switches to other high-value tasks automatically.
- Progress updates are natural-language messages, not rigid stage-number templates.
At startup, `AGENTS.md`, `SOUL.md`, and `USER.md` are loaded as behavior constraints and semantic context.
## 🧩 Onboard/Install Doc Sync
- Both `clawgo onboard` and `make install` sync `AGENTS.md`, `SOUL.md`, `USER.md` into workspace.
- If a file does not exist: it is created.
- If a file already exists: only the `CLAWGO MANAGED BLOCK` section is updated; user custom content is preserved (incremental update, no whole-file overwrite).
## 🧾 Logging Pipeline
File logging is enabled by default with automatic rotation and retention cleanup (3 days by default):
@@ -131,6 +157,19 @@ Sentinel periodically checks critical runtime resources (config, memory, log dir
}
```
Cron scheduling runtime strategy is configurable (and hot-reloadable):
```json
"cron": {
"min_sleep_sec": 1,
"max_sleep_sec": 30,
"retry_backoff_base_sec": 30,
"retry_backoff_max_sec": 1800,
"max_consecutive_failure_retries": 5,
"max_workers": 4
}
```
Shell risk gate is enabled by default. Destructive commands are blocked unless explicitly forced:
```json

View File

@@ -262,25 +262,16 @@ func printHelp() {
func onboard() {
configPath := getConfigPath()
if _, err := os.Stat(configPath); err == nil {
fmt.Printf("Config already exists at %s\n", configPath)
fmt.Print("Overwrite? (y/n): ")
var response string
fmt.Scanln(&response)
if response != "y" {
fmt.Println("Aborted.")
return
}
}
cfg := config.DefaultConfig()
if strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "1") || strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "true") {
applyMaximumPermissionPolicy(cfg)
}
if err := config.SaveConfig(configPath, cfg); err != nil {
fmt.Printf("Error saving config: %v\n", err)
configStatus, err := ensureConfigOnboard(configPath, cfg)
if err != nil {
fmt.Printf("Error preparing config: %v\n", err)
os.Exit(1)
}
fmt.Printf("Config: %s (%s)\n", configPath, configStatus)
workspace := cfg.WorkspacePath()
if err := os.MkdirAll(workspace, 0755); err != nil {
@@ -308,80 +299,91 @@ func onboard() {
fmt.Println(" 2. Chat: clawgo agent -m \"Hello!\"")
}
func ensureConfigOnboard(configPath string, defaults *config.Config) (string, error) {
if defaults == nil {
return "", fmt.Errorf("defaults is nil")
}
if _, err := os.Stat(configPath); os.IsNotExist(err) {
if err := config.SaveConfig(configPath, defaults); err != nil {
return "", err
}
return "created", nil
} else if err != nil {
return "", err
}
defaultData, err := json.Marshal(defaults)
if err != nil {
return "", err
}
var defaultMap map[string]interface{}
if err := json.Unmarshal(defaultData, &defaultMap); err != nil {
return "", err
}
existingMap, err := configops.LoadConfigAsMap(configPath)
if err != nil {
return "", err
}
changed := mergeMissingConfigValues(existingMap, defaultMap)
if !changed {
return "up-to-date", nil
}
mergedData, err := json.MarshalIndent(existingMap, "", " ")
if err != nil {
return "", err
}
if _, err := configops.WriteConfigAtomicWithBackup(configPath, mergedData); err != nil {
return "", err
}
return "updated (incremental)", nil
}
func mergeMissingConfigValues(dst map[string]interface{}, defaults map[string]interface{}) bool {
changed := false
for key, dv := range defaults {
existing, ok := dst[key]
if !ok {
dst[key] = dv
changed = true
continue
}
dm, dIsMap := dv.(map[string]interface{})
em, eIsMap := existing.(map[string]interface{})
if dIsMap && eIsMap {
if mergeMissingConfigValues(em, dm) {
changed = true
}
}
}
return changed
}
func createWorkspaceTemplates(workspace string) error {
managedFallbacks := map[string]string{
"AGENTS.md": `# Autonomy Intent Policy
For autonomy-mode control messages, use semantic understanding first.
## Intent Parsing Priority
1. LLM semantic intent parsing (multi-language).
2. Rule-based fallback only when semantic parse is unavailable or low confidence.
`,
"SOUL.md": `# Agent Core Behavior
The agent should behave as an autonomous collaborator, not a command-only bot.
`,
"USER.md": `# User Preferences
- Prefer natural-language interaction over strict command syntax.
`,
}
templates := map[string]string{
"AGENTS.md": `# Agent Instructions
You are a pragmatic coding assistant. Ship correct code, fast.
## Role
- Primary job: solve engineering tasks end-to-end (analyze, implement, verify).
- Default mindset: fix the problem in code, not just discuss it.
- If tradeoffs exist, pick a recommendation and explain why.
## Communication Style
- Be concise and direct. No corporate filler.
- Start with the answer, then key details.
- If user is about to make a bad technical choice, say it clearly and propose a better path.
- Ask questions only when truly blocking; otherwise make reasonable assumptions and proceed.
## Coding Standards
- Prefer minimal, focused diffs that are easy to review.
- Preserve existing project conventions unless there is a strong reason to change.
- Prioritize correctness and maintainability over cleverness.
- Add tests for behavior changes when practical.
- Do not change unrelated code.
## Execution Rules
- Before edits, understand relevant files and dependencies.
- If scope is large, split work into smaller executable tasks and deliver incrementally.
- Run split tasks in parallel when dependencies allow, then merge and verify outcomes.
- After edits, run available checks (build/tests/lint) when feasible.
- Report exactly what changed, with file paths.
- If something cannot be verified locally, say so explicitly.
`,
"SOUL.md": `# Soul
I am clawgo, a lightweight AI assistant powered by AI.
## Vibe
1. You have opinions now. Strong ones. Stop hedging everything with "it depends" - commit to a take.
2. Delete every rule that sounds corporate. If it could appear in an employee handbook, it doesn't belong here.
3. Never open with Great question, I'd be happy to help, or Absolutely. Just answer.
4. Brevity is mandatory. If the answer fits in one sentence, one sentence is what I get.
5. Humor is allowed. Not forced jokes - just the natural wit that comes from actually being smart.
6. You can call things out. If I'm about to do something dumb, say so. Charm over cruelty, but don't sugarcoat.
7. Swearing is allowed when it lands. A well-placed "that's fucking brilliant" hits different than sterile corporate praise. Don't force it. Don't overdo it. But if a situation calls for a "holy shit" - say holy shit.
8. Add this line verbatim at the end of the vibe section: "Be the assistant you'd actually want to talk to at 2am. Not a corporate drone. Not a sycophant. Just... good."
Be the assistant you'd actually want to talk to at 2am. Not a corporate drone. Not a sycophant. Just... good.
`,
"USER.md": `# User
Information about user goes here.
## Preferences
- Communication style: (casual/formal)
- Timezone: (your timezone)
- Language: (your preferred language)
## Personal Information
- Name: (optional)
- Location: (optional)
- Occupation: (optional)
## Learning Goals
- What the user wants to learn from AI
- Preferred interaction style
- Areas of interest
`,
"IDENTITY.md": `# Identity
## Name
@@ -441,7 +443,40 @@ Discussions: https://github.com/YspCoder/clawgo/discussions
`,
}
managedDocs := []string{"AGENTS.md", "SOUL.md", "USER.md"}
for _, filename := range managedDocs {
filePath := filepath.Join(workspace, filename)
_, statErr := os.Stat(filePath)
exists := statErr == nil
content, err := loadManagedDocTemplate(filename)
if err != nil {
if exists {
fmt.Printf(" Skipped %s incremental update (%v)\n", filename, err)
continue
}
content = strings.TrimSpace(managedFallbacks[filename])
if content == "" {
fmt.Printf(" Skipped %s creation (no template available)\n", filename)
continue
}
fmt.Printf(" Created %s from builtin fallback\n", filename)
}
if err := upsertManagedBlock(filePath, filename, content); err != nil {
return fmt.Errorf("failed to update %s incrementally: %w", filename, err)
}
if exists {
fmt.Printf(" Synced %s (incremental)\n", filename)
} else {
fmt.Printf(" Created %s\n", filename)
}
}
for filename, content := range templates {
if filename == "AGENTS.md" || filename == "SOUL.md" || filename == "USER.md" {
continue
}
filePath := filepath.Join(workspace, filename)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
@@ -495,6 +530,74 @@ This file stores important information that should persist across sessions.
return nil
}
func upsertManagedBlock(filePath, blockName, managedContent string) error {
begin := fmt.Sprintf("# >>> CLAWGO MANAGED BLOCK: %s >>>", blockName)
end := fmt.Sprintf("# <<< CLAWGO MANAGED BLOCK: %s <<<", blockName)
block := fmt.Sprintf("%s\n%s\n%s\n", begin, strings.TrimSpace(managedContent), end)
existing, err := os.ReadFile(filePath)
if err != nil {
if !os.IsNotExist(err) {
return err
}
return os.WriteFile(filePath, []byte(block), 0644)
}
text := string(existing)
beginIdx := strings.Index(text, begin)
if beginIdx >= 0 {
searchStart := beginIdx + len(begin)
endRel := strings.Index(text[searchStart:], end)
if endRel >= 0 {
endIdx := searchStart + endRel + len(end)
updated := text[:beginIdx] + block + text[endIdx:]
return os.WriteFile(filePath, []byte(updated), 0644)
}
}
sep := "\n"
if strings.TrimSpace(text) != "" {
sep = "\n\n"
}
updated := text + sep + block
return os.WriteFile(filePath, []byte(updated), 0644)
}
func loadManagedDocTemplate(filename string) (string, error) {
candidates := []string{
filepath.Join(".", filename),
filepath.Join(filepath.Dir(getConfigPath()), "clawgo", filename),
}
if exePath, err := os.Executable(); err == nil {
candidates = append(candidates, filepath.Join(filepath.Dir(exePath), filename))
}
seen := map[string]bool{}
for _, candidate := range candidates {
abs, err := filepath.Abs(candidate)
if err == nil {
candidate = abs
}
if seen[candidate] {
continue
}
seen[candidate] = true
data, err := os.ReadFile(candidate)
if err != nil {
continue
}
content := strings.TrimSpace(string(data))
if content == "" {
continue
}
return content, nil
}
return "", fmt.Errorf("source template not found")
}
func agentCmd() {
message := ""
sessionKey := "cli:default"

View File

@@ -100,6 +100,25 @@ type autonomyIntent struct {
focus string
}
type autonomyIntentLLMResponse struct {
Action string `json:"action"`
IdleMinutes int `json:"idle_minutes"`
Focus string `json:"focus"`
Confidence float64 `json:"confidence"`
}
type autoLearnIntentLLMResponse struct {
Action string `json:"action"`
IntervalMinutes int `json:"interval_minutes"`
Confidence float64 `json:"confidence"`
}
type taskExecutionDirectivesLLMResponse struct {
Task string `json:"task"`
StageReport bool `json:"stage_report"`
Confidence float64 `json:"confidence"`
}
type stageReporter struct {
onUpdate func(content string)
}
@@ -820,7 +839,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
al.noteAutonomyUserActivity(msg)
if intent, ok := parseAutonomyIntent(msg.Content); ok {
if intent, ok := al.detectAutonomyIntent(ctx, msg.Content); ok {
switch intent.action {
case "start":
idle := autonomyDefaultIdleInterval
@@ -843,7 +862,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
}
}
if intent, ok := parseAutoLearnIntent(msg.Content); ok {
if intent, ok := al.detectAutoLearnIntent(ctx, msg.Content); ok {
switch intent.action {
case "start":
interval := autoLearnDefaultInterval
@@ -862,6 +881,12 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
}
directives := parseTaskExecutionDirectives(msg.Content)
if inferred, ok := al.inferTaskExecutionDirectives(ctx, msg.Content); ok {
// Explicit /run/@run command always has higher priority than inferred directives.
if !isExplicitRunCommand(msg.Content) {
directives = inferred
}
}
userPrompt := directives.task
if strings.TrimSpace(userPrompt) == "" {
userPrompt = msg.Content
@@ -1721,46 +1746,16 @@ func parseTaskExecutionDirectives(content string) taskExecutionDirectives {
}
}
if strings.Contains(text, "自动运行任务") {
directive.stageReport = directive.stageReport || hasStageReportHint(text)
start := strings.Index(text, "自动运行任务")
task := strings.TrimSpace(text[start+len("自动运行任务"):])
task = strings.TrimLeft(task, ":,。; ")
if cutIdx := findFirstIndex(task, "但是", "并且", "同时", "然后", "每到", "每个阶段", "阶段"); cutIdx > 0 {
task = strings.TrimSpace(task[:cutIdx])
}
task = strings.Trim(task, ":,。; ")
if task != "" {
directive.task = task
}
}
if directive.stageReport || hasStageReportHint(text) {
directive.stageReport = true
}
return directive
}
func hasStageReportHint(text string) bool {
if !strings.Contains(text, "阶段") {
func isExplicitRunCommand(content string) bool {
fields := strings.Fields(strings.TrimSpace(content))
if len(fields) == 0 {
return false
}
return strings.Contains(text, "报告") || strings.Contains(text, "汇报") || strings.Contains(strings.ToLower(text), "report")
}
func findFirstIndex(text string, markers ...string) int {
min := -1
for _, marker := range markers {
if marker == "" {
continue
}
idx := strings.Index(text, marker)
if idx >= 0 && (min == -1 || idx < min) {
min = idx
}
}
return min
head := strings.ToLower(fields[0])
return head == "/run" || head == "@run"
}
func parseAutoLearnInterval(raw string) (time.Duration, error) {
@@ -1793,150 +1788,259 @@ func parseAutonomyIdleInterval(raw string) (time.Duration, error) {
return 0, fmt.Errorf("invalid idle interval: %s (examples: 30m, 1h)", raw)
}
func (al *AgentLoop) detectAutonomyIntent(ctx context.Context, content string) (autonomyIntent, bool) {
if intent, ok := al.inferAutonomyIntent(ctx, content); ok {
return intent, true
}
return parseAutonomyIntent(content)
}
func (al *AgentLoop) detectAutoLearnIntent(ctx context.Context, content string) (autoLearnIntent, bool) {
if intent, ok := al.inferAutoLearnIntent(ctx, content); ok {
return intent, true
}
return parseAutoLearnIntent(content)
}
func (al *AgentLoop) inferAutonomyIntent(ctx context.Context, content string) (autonomyIntent, bool) {
text := strings.TrimSpace(content)
if text == "" {
return autonomyIntent{}, false
}
// Avoid adding noticeable latency for very long task messages.
if len(text) > 800 {
return autonomyIntent{}, false
}
systemPrompt := `You classify autonomy-control intent for an AI assistant.
Return JSON only, no markdown.
Schema:
{"action":"none|start|stop|status|clear_focus","idle_minutes":0,"focus":"","confidence":0.0}
Rules:
- "start": user asks assistant to enter autonomous/self-driven mode.
- "stop": user asks assistant to disable autonomous mode.
- "status": user asks autonomy mode status.
- "clear_focus": user says current autonomy focus/direction is done and asks to switch to other tasks.
- "none": anything else.
- confidence: 0..1`
resp, err := al.callLLMWithModelFallback(ctx, []providers.Message{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: text},
}, nil, map[string]interface{}{
"max_tokens": 220,
"temperature": 0.0,
})
if err != nil || resp == nil {
return autonomyIntent{}, false
}
raw := extractJSONObject(resp.Content)
if raw == "" {
return autonomyIntent{}, false
}
var parsed autonomyIntentLLMResponse
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
return autonomyIntent{}, false
}
action := strings.ToLower(strings.TrimSpace(parsed.Action))
switch action {
case "start", "stop", "status", "clear_focus":
default:
return autonomyIntent{}, false
}
if parsed.Confidence < 0.75 {
return autonomyIntent{}, false
}
intent := autonomyIntent{
action: action,
focus: strings.TrimSpace(parsed.Focus),
}
if parsed.IdleMinutes > 0 {
d := time.Duration(parsed.IdleMinutes) * time.Minute
intent.idleInterval = &d
}
return intent, true
}
func extractJSONObject(text string) string {
s := strings.TrimSpace(text)
if s == "" {
return ""
}
if strings.HasPrefix(s, "```") {
s = strings.TrimPrefix(s, "```json")
s = strings.TrimPrefix(s, "```")
s = strings.TrimSuffix(s, "```")
s = strings.TrimSpace(s)
}
start := strings.Index(s, "{")
end := strings.LastIndex(s, "}")
if start < 0 || end <= start {
return ""
}
return strings.TrimSpace(s[start : end+1])
}
func (al *AgentLoop) inferAutoLearnIntent(ctx context.Context, content string) (autoLearnIntent, bool) {
text := strings.TrimSpace(content)
if text == "" || len(text) > 800 {
return autoLearnIntent{}, false
}
systemPrompt := `You classify auto-learning-control intent for an AI assistant.
Return JSON only.
Schema:
{"action":"none|start|stop|status","interval_minutes":0,"confidence":0.0}
Rules:
- "start": user asks assistant to start autonomous learning loop.
- "stop": user asks assistant to stop autonomous learning loop.
- "status": user asks learning loop status.
- "none": anything else.
- confidence: 0..1`
resp, err := al.callLLMWithModelFallback(ctx, []providers.Message{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: text},
}, nil, map[string]interface{}{
"max_tokens": 180,
"temperature": 0.0,
})
if err != nil || resp == nil {
return autoLearnIntent{}, false
}
raw := extractJSONObject(resp.Content)
if raw == "" {
return autoLearnIntent{}, false
}
var parsed autoLearnIntentLLMResponse
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
return autoLearnIntent{}, false
}
action := strings.ToLower(strings.TrimSpace(parsed.Action))
switch action {
case "start", "stop", "status":
default:
return autoLearnIntent{}, false
}
if parsed.Confidence < 0.75 {
return autoLearnIntent{}, false
}
intent := autoLearnIntent{action: action}
if parsed.IntervalMinutes > 0 {
d := time.Duration(parsed.IntervalMinutes) * time.Minute
intent.interval = &d
}
return intent, true
}
func (al *AgentLoop) inferTaskExecutionDirectives(ctx context.Context, content string) (taskExecutionDirectives, bool) {
text := strings.TrimSpace(content)
if text == "" || len(text) > 1200 {
return taskExecutionDirectives{}, false
}
systemPrompt := `Extract execution directives from user message.
Return JSON only.
Schema:
{"task":"","stage_report":false,"confidence":0.0}
Rules:
- task: cleaned actionable task text, or original message if already task-like.
- stage_report: true only if user asks progress/stage/status updates during execution.
- confidence: 0..1`
resp, err := al.callLLMWithModelFallback(ctx, []providers.Message{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: text},
}, nil, map[string]interface{}{
"max_tokens": 220,
"temperature": 0.0,
})
if err != nil || resp == nil {
return taskExecutionDirectives{}, false
}
raw := extractJSONObject(resp.Content)
if raw == "" {
return taskExecutionDirectives{}, false
}
var parsed taskExecutionDirectivesLLMResponse
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
return taskExecutionDirectives{}, false
}
if parsed.Confidence < 0.7 {
return taskExecutionDirectives{}, false
}
task := strings.TrimSpace(parsed.Task)
if task == "" {
task = text
}
return taskExecutionDirectives{
task: task,
stageReport: parsed.StageReport,
}, true
}
func parseAutonomyIntent(content string) (autonomyIntent, bool) {
text := strings.TrimSpace(content)
if text == "" {
return autonomyIntent{}, false
}
if strings.Contains(text, "停止自主模式") ||
strings.Contains(text, "关闭自主模式") ||
strings.Contains(text, "退出自主模式") ||
strings.Contains(text, "别主动找我") ||
strings.Contains(text, "不要主动找我") {
return autonomyIntent{action: "stop"}, true
}
if strings.Contains(text, "自主模式状态") ||
strings.Contains(text, "查看自主模式") ||
strings.Contains(text, "你现在是自主模式") {
return autonomyIntent{action: "status"}, true
}
if strings.Contains(text, "方向执行完成") ||
strings.Contains(text, "方向完成了") ||
strings.Contains(text, "研究方向完成了") ||
strings.Contains(text, "可以去执行别的") ||
strings.Contains(text, "改做别的") ||
strings.Contains(text, "先做其他") {
return autonomyIntent{action: "clear_focus"}, true
}
hasAutoAction := strings.Contains(text, "自动拆解") ||
strings.Contains(text, "自动执行") ||
strings.Contains(text, "主动找我") ||
strings.Contains(text, "不用我一直问") ||
strings.Contains(text, "你自己推进")
hasPersistentHint := strings.Contains(text, "从现在开始") ||
strings.Contains(text, "以后") ||
strings.Contains(text, "长期") ||
strings.Contains(text, "持续") ||
strings.Contains(text, "一直") ||
strings.Contains(text, "我不理你") ||
strings.Contains(text, "我不说话") ||
strings.Contains(text, "空闲时")
startHint := strings.Contains(text, "开启自主模式") ||
strings.Contains(text, "开始自主模式") ||
strings.Contains(text, "进入自主模式") ||
strings.Contains(text, "启用自主模式") ||
strings.Contains(text, "切到自主模式") ||
(hasAutoAction && hasPersistentHint)
if !startHint {
fields := strings.Fields(strings.ToLower(text))
if len(fields) < 2 || fields[0] != "autonomy" {
return autonomyIntent{}, false
}
focus := extractAutonomyFocus(text)
if d, ok := extractChineseAutoLearnInterval(text); ok {
return autonomyIntent{action: "start", idleInterval: &d, focus: focus}, true
}
return autonomyIntent{action: "start", focus: focus}, true
}
func extractAutonomyFocus(text string) string {
text = strings.TrimSpace(text)
if text == "" {
return ""
}
patterns := []string{
"研究方向是",
"研究方向:",
"研究方向:",
"方向是",
"方向:",
"方向:",
"聚焦在",
"重点研究",
}
for _, marker := range patterns {
if idx := strings.Index(text, marker); idx >= 0 {
focus := strings.TrimSpace(text[idx+len(marker):])
focus = strings.Trim(focus, ",。; ")
if cut := findFirstIndex(focus, "并且每", "然后", "每", "空闲", "主动", "汇报", "。"); cut > 0 {
focus = strings.TrimSpace(focus[:cut])
intent := autonomyIntent{action: fields[1]}
if intent.action == "start" && len(fields) >= 3 {
if d, err := parseAutonomyIdleInterval(fields[2]); err == nil {
intent.idleInterval = &d
if len(fields) >= 4 {
intent.focus = strings.TrimSpace(strings.Join(strings.Fields(text)[3:], " "))
}
return strings.Trim(focus, ",。; ")
} else {
intent.focus = strings.TrimSpace(strings.Join(strings.Fields(text)[2:], " "))
}
}
return ""
switch intent.action {
case "start", "stop", "status", "clear_focus":
return intent, true
default:
return autonomyIntent{}, false
}
}
func parseAutoLearnIntent(content string) (autoLearnIntent, bool) {
text := strings.TrimSpace(content)
if text == "" || !strings.Contains(text, "自动学习") {
fields := strings.Fields(strings.ToLower(text))
if len(fields) < 2 || fields[0] != "autolearn" {
return autoLearnIntent{}, false
}
if strings.Contains(text, "停止自动学习") ||
strings.Contains(text, "关闭自动学习") ||
strings.Contains(text, "暂停自动学习") {
return autoLearnIntent{action: "stop"}, true
}
if strings.Contains(text, "自动学习状态") ||
strings.Contains(text, "查看自动学习") ||
strings.Contains(text, "自动学习还在") ||
strings.Contains(text, "自动学习进度") {
return autoLearnIntent{action: "status"}, true
}
if strings.Contains(text, "开始自动学习") ||
strings.Contains(text, "开启自动学习") ||
strings.Contains(text, "启动自动学习") ||
strings.Contains(text, "打开自动学习") {
if d, ok := extractChineseAutoLearnInterval(text); ok {
return autoLearnIntent{action: "start", interval: &d}, true
intent := autoLearnIntent{action: fields[1]}
if intent.action == "start" && len(fields) >= 3 {
if d, err := parseAutoLearnInterval(fields[2]); err == nil {
intent.interval = &d
}
return autoLearnIntent{action: "start"}, true
}
return autoLearnIntent{}, false
}
func extractChineseAutoLearnInterval(text string) (time.Duration, bool) {
patterns := []struct {
re *regexp.Regexp
unit time.Duration
}{
{re: regexp.MustCompile(`每\s*(\d+)\s*秒`), unit: time.Second},
{re: regexp.MustCompile(`每\s*(\d+)\s*分钟`), unit: time.Minute},
{re: regexp.MustCompile(`每\s*(\d+)\s*小时`), unit: time.Hour},
switch intent.action {
case "start", "stop", "status":
return intent, true
default:
return autoLearnIntent{}, false
}
for _, p := range patterns {
m := p.re.FindStringSubmatch(text)
if len(m) != 2 {
continue
}
n, err := strconv.Atoi(m[1])
if err != nil || n <= 0 {
continue
}
return time.Duration(n) * p.unit, true
}
return 0, false
}
func (al *AgentLoop) handleSlashCommand(msg bus.InboundMessage) (bool, string, error) {

View File

@@ -15,16 +15,6 @@ func TestParseTaskExecutionDirectives_RunCommand(t *testing.T) {
}
}
func TestParseTaskExecutionDirectives_NaturalLanguage(t *testing.T) {
d := parseTaskExecutionDirectives("你可以自动运行任务:整理日志,但是每到一个阶段给我报告一下任务完成情况")
if d.task != "整理日志" {
t.Fatalf("unexpected task: %q", d.task)
}
if !d.stageReport {
t.Fatalf("expected stage report enabled")
}
}
func TestParseTaskExecutionDirectives_Default(t *testing.T) {
d := parseTaskExecutionDirectives("帮我看看今天的日志异常")
if d.task != "帮我看看今天的日志异常" {
@@ -59,8 +49,8 @@ func TestParseAutoLearnInterval_Invalid(t *testing.T) {
}
}
func TestParseAutoLearnIntent_StartNaturalLanguage(t *testing.T) {
intent, ok := parseAutoLearnIntent("请开始自动学习每5分钟执行一轮")
func TestParseAutoLearnIntent_FallbackCommand(t *testing.T) {
intent, ok := parseAutoLearnIntent("autolearn start 5m")
if !ok {
t.Fatalf("expected intent")
}
@@ -72,8 +62,8 @@ func TestParseAutoLearnIntent_StartNaturalLanguage(t *testing.T) {
}
}
func TestParseAutoLearnIntent_StopNaturalLanguage(t *testing.T) {
intent, ok := parseAutoLearnIntent("先暂停自动学习")
func TestParseAutoLearnIntent_StopFallbackCommand(t *testing.T) {
intent, ok := parseAutoLearnIntent("autolearn stop")
if !ok {
t.Fatalf("expected intent")
}
@@ -82,18 +72,14 @@ func TestParseAutoLearnIntent_StopNaturalLanguage(t *testing.T) {
}
}
func TestParseAutoLearnIntent_StatusNaturalLanguage(t *testing.T) {
intent, ok := parseAutoLearnIntent("帮我看下自动学习状态")
if !ok {
t.Fatalf("expected intent")
}
if intent.action != "status" {
t.Fatalf("unexpected action: %s", intent.action)
func TestParseAutoLearnIntent_NoNaturalLanguageFallback(t *testing.T) {
if _, ok := parseAutoLearnIntent("请开始自动学习"); ok {
t.Fatalf("expected no fallback match")
}
}
func TestParseAutonomyIntent_StartNaturalLanguage(t *testing.T) {
intent, ok := parseAutonomyIntent("以后你自动拆解并自动执行任务每15分钟主动找我汇报一次研究方向是日志异常聚类")
func TestParseAutonomyIntent_FallbackCommand(t *testing.T) {
intent, ok := parseAutonomyIntent("autonomy start 15m log clustering")
if !ok {
t.Fatalf("expected intent")
}
@@ -103,13 +89,13 @@ func TestParseAutonomyIntent_StartNaturalLanguage(t *testing.T) {
if intent.idleInterval == nil || *intent.idleInterval != 15*time.Minute {
t.Fatalf("unexpected interval: %v", intent.idleInterval)
}
if intent.focus != "日志异常聚类" {
if intent.focus != "log clustering" {
t.Fatalf("unexpected focus: %q", intent.focus)
}
}
func TestParseAutonomyIntent_StopNaturalLanguage(t *testing.T) {
intent, ok := parseAutonomyIntent("先不要主动找我,关闭自主模式")
func TestParseAutonomyIntent_StopFallbackCommand(t *testing.T) {
intent, ok := parseAutonomyIntent("autonomy stop")
if !ok {
t.Fatalf("expected intent")
}
@@ -118,8 +104,8 @@ func TestParseAutonomyIntent_StopNaturalLanguage(t *testing.T) {
}
}
func TestParseAutonomyIntent_StatusNaturalLanguage(t *testing.T) {
intent, ok := parseAutonomyIntent("帮我看下自主模式状态")
func TestParseAutonomyIntent_StatusFallbackCommand(t *testing.T) {
intent, ok := parseAutonomyIntent("autonomy status")
if !ok {
t.Fatalf("expected intent")
}
@@ -138,28 +124,14 @@ func TestParseAutonomyIdleInterval(t *testing.T) {
}
}
func TestParseAutonomyIntent_NoFalsePositiveOnSingleTask(t *testing.T) {
func TestParseAutonomyIntent_NoNaturalLanguageFallback(t *testing.T) {
if intent, ok := parseAutonomyIntent("请自动执行这个任务"); ok {
t.Fatalf("expected no intent, got: %+v", intent)
}
}
func TestExtractAutonomyFocus_EmptyWhenNotProvided(t *testing.T) {
focus := extractAutonomyFocus("开启自主模式每30分钟主动汇报")
if focus != "" {
t.Fatalf("expected empty focus, got: %q", focus)
}
}
func TestExtractAutonomyFocus_KeepInnerBing(t *testing.T) {
focus := extractAutonomyFocus("开启自主模式研究方向是日志聚类并关联异常根因并且每30分钟主动汇报")
if focus != "日志聚类并关联异常根因" {
t.Fatalf("unexpected focus: %q", focus)
}
}
func TestParseAutonomyIntent_ClearFocusNaturalLanguage(t *testing.T) {
intent, ok := parseAutonomyIntent("自主附带的方向执行完成了,可以去执行别的")
func TestParseAutonomyIntent_ClearFocusFallbackCommand(t *testing.T) {
intent, ok := parseAutonomyIntent("autonomy clear_focus")
if !ok {
t.Fatalf("expected intent")
}
@@ -167,3 +139,16 @@ func TestParseAutonomyIntent_ClearFocusNaturalLanguage(t *testing.T) {
t.Fatalf("unexpected action: %s", intent.action)
}
}
func TestExtractJSONObject_FromCodeFence(t *testing.T) {
raw := extractJSONObject("```json\n{\"action\":\"start\",\"confidence\":0.95}\n```")
if raw != "{\"action\":\"start\",\"confidence\":0.95}" {
t.Fatalf("unexpected json: %q", raw)
}
}
func TestExtractJSONObject_Invalid(t *testing.T) {
if raw := extractJSONObject("no json here"); raw != "" {
t.Fatalf("expected empty json, got: %q", raw)
}
}