package tools import ( "context" "encoding/json" "fmt" "path/filepath" "strings" "time" "agent-system-backend/internal/execenv" core "agent-system-core" ) // envBroker 由组合根注入。未注入时 python_env 明确报错,不假装能 pip install。 var envBroker *execenv.Broker // envApproval 在申请需要人批时写一张选择卡。空则只把 pending 文本回给模型。 var envApproval func(owner, conversationID, requestID, detail string) error // SetEnvBroker 注入分层环境代理。 func SetEnvBroker(b *execenv.Broker) { envBroker = b } // SetEnvApproval 注入审批卡写入。 func SetEnvApproval(fn func(owner, conversationID, requestID, detail string) error) { envApproval = fn } func registerPythonEnvTool(r *Registry) error { return r.Register(core.ToolDesc{ Name: "python_env", Source: core.SourceBuiltin, Risk: "medium", Description: "向系统申请 Python 包,或查看本会话已经能用的环境。沙箱不能联网安装。action=status 查看基础层与本会话的层;action=request 申请包(packages)或登记工作区内的 wheel;白名单内的包会自动装好并在下一次 run_python 生效,其余会进入审批。action=wait 等待 requestId 结束。不要用 pip install。", Parameters: json.RawMessage(`{"type":"object","properties":{` + `"action":{"type":"string","enum":["status","request","wait"],"description":"status | request | wait"},` + `"packages":{"type":"array","items":{"type":"string"},"description":"包规格,如 pdfplumber 或 numpy==2.2.0"},` + `"reason":{"type":"string","description":"为什么需要这些包"},` + `"wheel":{"type":"string","description":"工作区内的 .whl 相对路径,上传的包需要审批"},` + `"request_id":{"type":"string","description":"wait 时的申请 ID"},` + `"timeout_sec":{"type":"integer","description":"wait 的秒数,默认 120"}}` + `,"required":["action"]}`), }, execPythonEnv) } func execPythonEnv(rc ToolExecCtx, args json.RawMessage) (string, error) { if err := execGate(rc); err != nil { return "", err } if envBroker == nil { return "", fmt.Errorf("python_env: 环境代理未配置(启动参数 execEnvRoot)") } var a struct { Action string `json:"action"` Packages []string `json:"packages"` Reason string `json:"reason"` Wheel string `json:"wheel"` RequestID string `json:"request_id"` TimeoutS int `json:"timeout_sec"` } if len(args) > 0 { if err := json.Unmarshal(args, &a); err != nil { return "", fmt.Errorf("python_env: bad args: %w", err) } } switch a.Action { case "status": return envBroker.StatusText(rc.ConversationID()) case "wait": if strings.TrimSpace(a.RequestID) == "" { return "", fmt.Errorf("python_env: request_id required") } timeout := time.Duration(a.TimeoutS) * time.Second out, err := envBroker.Wait(execParentCtx(rc), a.RequestID, timeout) if err != nil { return "", err } raw, _ := json.Marshal(out) return string(raw), nil case "request": wheel := "" if strings.TrimSpace(a.Wheel) != "" { sandbox, err := execSandbox(rc) if err != nil { return "", err } wheel, err = filepath.Abs(filepath.Join(sandbox, a.Wheel)) if err != nil { return "", err } if !strings.HasPrefix(wheel, sandbox+string(filepath.Separator)) { return "", fmt.Errorf("python_env: wheel must stay inside the workspace") } } out, err := envBroker.RequestPackages(execParentCtx(rc), rc.ConversationID(), rc.OwnerID(), a.Reason, a.Packages, wheel) if err != nil { return "", err } if out.Status == "pending_approval" && envApproval != nil { if err := envApproval(rc.OwnerID(), rc.ConversationID(), out.RequestID, out.Detail); err != nil { out.Detail += ";审批卡未写入:" + err.Error() } } raw, _ := json.Marshal(out) return string(raw), nil default: return "", fmt.Errorf("python_env: unknown action %q", a.Action) } } // ExecDoctor 用当前执行后端探针导入基础交付物库。依赖层已配置但导入失败时返回错误。 func ExecDoctor() error { bin, err := resolveInterpreter("python3") if err != nil { return err } dirs, _ := pythonPathFor("") mounts := append([]string{}, dirs...) if execPythonPrefix != "" { mounts = append(mounts, execPythonPrefix) } if execShimDir != "" { mounts = append(mounts, execShimDir) } return execenv.Doctor(context.Background(), bin, dirs, nil, ExecBackendName() == "sandbox", mounts) }