feat(ai): Agent SSE + Anthropic/responses tools + P1 确认写
- Agent 支持 SSE(步骤边推、正文分块),JSON 非流式 fallback - tool calling 覆盖 openai-chat / anthropic / responses(单测解析回灌) - P1:mcp.json mode=read_write 才可写;需 confirm_danger;Agent 剥离模型自带令牌 - GET /api/ai/mcp-status、POST /api/ai/agent/confirm;审计 source=mcp|agent
This commit is contained in:
1 parent
1d6e89449a
commit
2336c9433c
17 files changed
+1324
-197
No files matched your search
@@ -475,6 +475,7 @@ func NewRouter(token string) *gin.Engine {
|
||||
})
|
||||
|
||||
// 侧栏 Agent:多轮 tool call → 同一 aitools.Runtime(进程内,不绕 stdio)。Ask 仍走 /ai/chat。
|
||||
// stream=true 或 Accept: text/event-stream 时走 SSE(步骤边推 + 正文分块);否则 JSON fallback。
|
||||
api.POST("/ai/agent", func(c *gin.Context) {
|
||||
var req ai.AgentReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -491,6 +492,30 @@ func NewRouter(token string) *gin.Engine {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
rt.Source = "agent"
|
||||
rt.RejectCallerConfirm = true
|
||||
|
||||
wantStream := req.Stream || strings.Contains(c.GetHeader("Accept"), "text/event-stream")
|
||||
if wantStream {
|
||||
flusher, ok := c.Writer.(http.Flusher)
|
||||
if !ok {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "streaming unsupported"})
|
||||
return
|
||||
}
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Status(http.StatusOK)
|
||||
flusher.Flush()
|
||||
_, err := ai.RunAgentStream(c.Request.Context(), llm, rt, req, func(ev ai.AgentEvent) {
|
||||
_ = ai.WriteAgentSSE(c.Writer, flusher, ev)
|
||||
})
|
||||
if err != nil && c.Request.Context().Err() == nil {
|
||||
_ = ai.WriteAgentSSE(c.Writer, flusher, ai.AgentEvent{Type: ai.AgentEventError, Error: err.Error()})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
res, err := ai.RunAgent(c.Request.Context(), llm, rt, req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error(), "steps": res.Steps, "rounds": res.Rounds})
|
||||
@@ -499,6 +524,55 @@ func NewRouter(token string) *gin.Engine {
|
||||
c.JSON(http.StatusOK, res)
|
||||
})
|
||||
|
||||
// 用户在气泡确认条点「执行」后:单次写操作(AllowConfirmedWrite),绝不由模型静默触发。
|
||||
api.POST("/ai/agent/confirm", func(c *gin.Context) {
|
||||
var req struct {
|
||||
ConnectionID string `json:"connectionId"`
|
||||
SQL string `json:"sql"`
|
||||
Command string `json:"command"`
|
||||
Database string `json:"database"`
|
||||
Kind string `json:"kind"` // query | redis
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
rt, err := aitools.DefaultRuntime()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !rt.Policy.WritesEnabled() {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "writes disabled: set mode=read_write in ~/.opsxdb/mcp.json"})
|
||||
return
|
||||
}
|
||||
rt.Source = "agent"
|
||||
rt.RejectCallerConfirm = false
|
||||
rt.AllowConfirmedWrite = true
|
||||
kind := strings.ToLower(strings.TrimSpace(req.Kind))
|
||||
args := map[string]any{"connection_id": req.ConnectionID, "database": req.Database, "confirm_danger": true}
|
||||
var out any
|
||||
if kind == "redis" || (kind == "" && req.Command != "") {
|
||||
args["command"] = req.Command
|
||||
if args["command"] == "" || args["command"] == nil {
|
||||
args["command"] = req.SQL
|
||||
}
|
||||
out, err = rt.Call(c.Request.Context(), aitools.ToolExecuteRedisCommand, args)
|
||||
} else {
|
||||
args["sql"] = req.SQL
|
||||
out, err = rt.Call(c.Request.Context(), aitools.ToolExecuteQuery, args)
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, out)
|
||||
})
|
||||
|
||||
api.GET("/ai/mcp-status", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, aitools.GetMCPStatus())
|
||||
})
|
||||
|
||||
api.POST("/ai/generate", func(c *gin.Context) {
|
||||
var req ai.ChatReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
Reference in new issue
Block a user