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
@@ -0,0 +1,98 @@
|
||||
package aitools
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const mcpActiveWindow = 5 * time.Minute
|
||||
|
||||
// MCPStatus 供侧栏 MCP 状态点 / 设置页轻量指示。
|
||||
type MCPStatus struct {
|
||||
Configured bool `json:"configured"`
|
||||
Mode string `json:"mode"`
|
||||
WhitelistCount int `json:"whitelistCount"`
|
||||
WritesEnabled bool `json:"writesEnabled"`
|
||||
Active bool `json:"active"` // 近 5 分钟有 source=mcp 审计
|
||||
Recent []AuditEntry `json:"recent,omitempty"`
|
||||
PolicyPath string `json:"policyPath,omitempty"`
|
||||
AuditPath string `json:"auditPath,omitempty"`
|
||||
}
|
||||
|
||||
// GetMCPStatus 读 mcp.json + 尾部审计;不探测外部 Cursor 进程。
|
||||
func GetMCPStatus() MCPStatus {
|
||||
st := MCPStatus{Mode: ModeReadOnly}
|
||||
if path, err := PolicyPath(); err == nil {
|
||||
st.PolicyPath = path
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
st.Configured = true
|
||||
}
|
||||
}
|
||||
if p, err := LoadPolicy(); err == nil {
|
||||
st.Mode = p.Mode
|
||||
if st.Mode == "" {
|
||||
st.Mode = ModeReadOnly
|
||||
}
|
||||
st.WhitelistCount = len(p.AllowedConnectionIDs)
|
||||
st.WritesEnabled = p.WritesEnabled()
|
||||
st.Configured = st.Configured || st.WhitelistCount > 0
|
||||
}
|
||||
if ap, err := AuditPath(); err == nil {
|
||||
st.AuditPath = ap
|
||||
recent := tailAudit(ap, 20)
|
||||
st.Recent = filterSource(recent, "mcp")
|
||||
if len(st.Recent) == 0 {
|
||||
// 兼容旧审计无 source:仍展示最近条目,但不标 active
|
||||
st.Recent = recent
|
||||
} else {
|
||||
cutoff := time.Now().Add(-mcpActiveWindow)
|
||||
for _, e := range st.Recent {
|
||||
if t, err := time.Parse(time.RFC3339, e.Time); err == nil && t.After(cutoff) {
|
||||
st.Active = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
func filterSource(in []AuditEntry, source string) []AuditEntry {
|
||||
var out []AuditEntry
|
||||
for _, e := range in {
|
||||
if e.Source == source {
|
||||
out = append(out, e)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tailAudit(path string, n int) []AuditEntry {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
var lines []string
|
||||
sc := bufio.NewScanner(f)
|
||||
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
for sc.Scan() {
|
||||
lines = append(lines, sc.Text())
|
||||
if len(lines) > n*4 { // 粗裁
|
||||
lines = lines[len(lines)-n*2:]
|
||||
}
|
||||
}
|
||||
if len(lines) > n {
|
||||
lines = lines[len(lines)-n:]
|
||||
}
|
||||
out := make([]AuditEntry, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
var e AuditEntry
|
||||
if json.Unmarshal([]byte(line), &e) == nil {
|
||||
out = append(out, e)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user