package tools import ( "encoding/json" "fmt" "strings" core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git" ) type userAsker interface { AskUser(card core.HITLCard) (core.HITLResponse, error) } type planExiter interface { ExitPlanMode() error } func registerAskTools(r *Registry) error { if err := r.Register(core.ToolDesc{ Name: "ask_user", Description: "向用户提一个问题。可以给选项,用户也可以自己输入。", Source: core.SourceBuiltin, ReadOnly: true, Parameters: json.RawMessage(`{"type":"object","properties":{"question":{"type":"string"},"options":{"type":"array","items":{"type":"string"}}},"required":["question"]}`), }, execAskUser); err != nil { return err } return r.Register(core.ToolDesc{ Name: "exit_plan_mode", Description: "结束计划模式。用户批准后,本会话余下的执行改回默认权限。", Source: core.SourceBuiltin, ReadOnly: true, Parameters: json.RawMessage(`{"type":"object","properties":{"summary":{"type":"string"}}}`), }, execExitPlan) } func execAskUser(rc ToolExecCtx, args json.RawMessage) (string, error) { ask, ok := rc.(userAsker) if !ok { return "", fmt.Errorf("ask_user 未接入") } var a struct { Question string `json:"question"` Options []string `json:"options"` } if err := json.Unmarshal(args, &a); err != nil || strings.TrimSpace(a.Question) == "" { return "", fmt.Errorf("ask_user: question required") } card := core.HITLCard{ Question: a.Question, Kind: core.HITLKindChoice, AllowFreeInput: true, FreeInputHint: "也可以直接输入", } for i, opt := range a.Options { opt = strings.TrimSpace(opt) if opt == "" { continue } card.Choices = append(card.Choices, core.HITLChoice{ ID: fmt.Sprintf("opt%d", i+1), Label: opt, Description: opt, Impact: "模型会按这个选择继续", }) } if len(card.Choices) == 0 { card.Kind = core.HITLKindInput } resp, err := ask.AskUser(card) if err != nil { return "", err } if text := strings.TrimSpace(resp.FreeText); text != "" { return "用户回答:" + text, nil } for _, ch := range card.Choices { if ch.ID == resp.ChoiceID { return "用户选择:" + ch.Label, nil } } if resp.ChoiceID != "" { return "用户选择:" + resp.ChoiceID, nil } return "用户没有给出回答", nil } func execExitPlan(rc ToolExecCtx, args json.RawMessage) (string, error) { ask, ok := rc.(userAsker) exit, okExit := rc.(planExiter) if !ok || !okExit { return "", fmt.Errorf("exit_plan_mode 未接入") } var a struct { Summary string `json:"summary"` } _ = json.Unmarshal(args, &a) q := "批准计划并继续执行?" if s := strings.TrimSpace(a.Summary); s != "" { q = "批准计划并继续执行?\n" + s } resp, err := ask.AskUser(core.HITLCard{ Question: q, Kind: core.HITLKindConfirm, Detail: strings.TrimSpace(a.Summary), Choices: []core.HITLChoice{ {ID: "yes", Label: "批准", Description: "结束计划模式,按默认权限继续", Impact: "之后可以改文件", Recommended: true}, {ID: "no", Label: "继续计划", Description: "留在计划模式", Impact: "写文件仍然被拒绝"}, }, }) if err != nil { return "", err } if resp.ChoiceID != "yes" { return "用户还没有批准计划", nil } if err := exit.ExitPlanMode(); err != nil { return "", err } return "已切回默认权限", nil }