Files
LoganandClaude 471353e3d7 feat: 监控告警体系统一重构 + 自动化任务调度引擎 + 监控大屏拆分
告警中心
- 新增统一告警(alert_unified)、监控告警规则(monitor_alert_rule)、指标目录(alert_metric_catalog)
- 通知策略与消息分发(alert_notify_policy/dispatch/message)、静默规则
- 告警引擎按阈值/SNMP 拆分(alert_engine_monitor/snmp)

监控采集
- SNMP 常量/模板与历史指标(snmp templates/constants、snmp_metrics_history、snmp_collect)
- 中间件监控 Redis 采集与种子(middleware_monitor_redis/seed)
- 站点探测 pkg(siteprobe) + 站点视图(site_view)

自动化任务
- cron 调度器(cronscheduler)、脚本运行器(scriptrunner)、系统任务(systask)及运行记录(cron_job_run、system_task)

审计与凭证
- 审计设置(audit_setting) + 历史清理(auditcleaner)

前端
- 监控页面(host/middleware/site/snmp)拆分为子模块并大屏化
- 告警中心多 Tab(规则/指标目录/通知策略/静默)、任务管理组件重构、Agent CLI Markdown 渲染
- 移除旧脚本卡片/详情/日志组件,统一为 ScriptTable/ScriptCodeEditor/TerminalOutput

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-01 23:44:45 +08:00

364 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// anthropic.go - Anthropic Messages 协议 LLM 客户端实现 // Logan.Li
// 支持官方 Claude(api.anthropic.com)及智谱 GLM 编码套餐(open.bigmodel.cn/api/anthropic)等
// 所有 Anthropic /v1/messages 兼容端点。与 openai.go 实现同一 Client 接口,由 client.go 按 Protocol 路由。
// 鉴权统一用 Authorization: Bearer(智谱编码套餐实测通过;官方 Claude 较新版本亦支持)+ anthropic-version。
package llm
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// AnthropicClient Anthropic Messages 协议客户端 // Logan.Li
type AnthropicClient struct {
cfg Config
client *http.Client
}
// NewAnthropicClient 创建 Anthropic 兼容客户端 // Logan.Li
func NewAnthropicClient(cfg Config) Client {
timeout := cfg.Timeout
if timeout <= 0 {
timeout = 180 * time.Second
}
return &AnthropicClient{cfg: cfg, client: &http.Client{Timeout: timeout}}
}
// Name 返回客户端名称 // Logan.Li
func (c *AnthropicClient) Name() string { return "anthropic(" + c.cfg.Model + ")" }
// ---- 请求体结构(Anthropic Messages 格式) // Logan.Li ----
// anthropicMessage Anthropic 消息:content 可为字符串或 block 数组,用 RawMessage 原样嵌入。
type anthropicMessage struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
}
// anthropicThinking 启用 Claude thinking 模式(Claude 3.7 Sonnet 支持) // Logan.Li
type anthropicThinking struct {
Type string `json:"type"`
BudgetTokens int `json:"budget_tokens"`
}
// anthropicRequest Anthropic /v1/messages 请求体 // Logan.Li
type anthropicRequest struct {
Model string `json:"model"`
MaxTokens int `json:"max_tokens"`
System string `json:"system,omitempty"` // system 抽到顶层(不在 messages 内)
Messages []anthropicMessage `json:"messages"`
Tools []anthropicTool `json:"tools,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
Stream bool `json:"stream,omitempty"`
Thinking *anthropicThinking `json:"thinking,omitempty"` // Claude 3.7 Sonnet 推理模式 // Logan.Li
}
// anthropicTool Anthropic 工具定义(input_schema,对应 OpenAI 的 parameters) // Logan.Li
type anthropicTool struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema map[string]interface{} `json:"input_schema"`
}
// 响应 content block:text 或 tool_use(互斥字段) // Logan.Li
type anthropicBlock struct {
Type string `json:"type"` // text | tool_use
Text string `json:"text,omitempty"`
ID string `json:"id,omitempty"` // tool_use
Name string `json:"name,omitempty"` // tool_use
Input json.RawMessage `json:"input,omitempty"` // tool_use
}
// 请求侧 content block(assistant 输出 tool_use / user 输出 tool_result) // Logan.Li
type textBlock struct {
Type string `json:"type"` // 固定 "text"
Text string `json:"text"`
}
type toolUseBlock struct {
Type string `json:"type"` // 固定 "tool_use"
ID string `json:"id"`
Name string `json:"name"`
Input json.RawMessage `json:"input"`
}
type toolResultBlock struct {
Type string `json:"type"` // 固定 "tool_result"
ToolUseID string `json:"tool_use_id"`
Content string `json:"content"`
}
// anthropicResponse Anthropic /v1/messages 响应体 // Logan.Li
type anthropicResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Content []anthropicBlock `json:"content"`
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
Error *struct {
Type string `json:"type"`
Message string `json:"message"`
} `json:"error,omitempty"`
}
// buildMessages 把 OpenAI 风格 Message[] 转成 Anthropic messages + 顶层 system // Logan.Li
// - system role 抽到顶层 system 字段
// - tool role(工具结果)→ user 消息的 tool_result 块
// - assistant 带 tool_calls → content 含 tool_use 块
func (c *AnthropicClient) buildMessages(req *CompletionRequest) (string, []anthropicMessage) {
var system string
var msgs []anthropicMessage
for _, m := range req.Messages {
switch m.Role {
case "system":
if system == "" {
system = m.Content
} else {
system += "\n" + m.Content
}
case "tool":
raw, _ := json.Marshal([]toolResultBlock{{
Type: "tool_result", ToolUseID: m.ToolCallID, Content: m.Content,
}})
msgs = append(msgs, anthropicMessage{Role: "user", Content: raw})
case "assistant":
// assistant 消息可能含文本 + tool_use 计划,组装成 content block 数组
blocks := make([]interface{}, 0, 1+len(m.ToolCalls))
if m.Content != "" {
blocks = append(blocks, textBlock{Type: "text", Text: m.Content})
}
for _, tc := range m.ToolCalls {
args := json.RawMessage("{}")
if strings.TrimSpace(tc.Function.Arguments) != "" {
args = json.RawMessage(tc.Function.Arguments)
}
blocks = append(blocks, toolUseBlock{
Type: "tool_use", ID: tc.ID, Name: tc.Function.Name, Input: args,
})
}
raw, _ := json.Marshal(blocks)
msgs = append(msgs, anthropicMessage{Role: "assistant", Content: raw})
default: // user
raw, _ := json.Marshal(m.Content)
msgs = append(msgs, anthropicMessage{Role: "user", Content: raw})
}
}
return system, msgs
}
// buildTools OpenAI Tool[](parameters) → Anthropic [](input_schema) // Logan.Li
func buildTools(tools []Tool) []anthropicTool {
out := make([]anthropicTool, 0, len(tools))
for _, t := range tools {
out = append(out, anthropicTool{
Name: t.Function.Name, Description: t.Function.Description, InputSchema: t.Function.Parameters,
})
}
return out
}
// isAnthropicThinkingModel 判断是否为支持 thinking 的 Claude 模型 // Logan.Li
func isAnthropicThinkingModel(model string) bool {
m := strings.ToLower(model)
return strings.Contains(m, "claude-3-7") || strings.Contains(m, "claude-3.7")
}
// buildRequest 构造 Anthropic 请求体(Complete 与 Stream 共用) // Logan.Li
func (c *AnthropicClient) buildRequest(req *CompletionRequest, stream bool) ([]byte, error) {
system, msgs := c.buildMessages(req)
payload := anthropicRequest{
Model: c.cfg.Model, MaxTokens: c.cfg.MaxTokens, System: system,
Messages: msgs, Tools: buildTools(req.Tools),
Temperature: c.cfg.Temperature, Stream: stream,
}
if payload.MaxTokens <= 0 {
payload.MaxTokens = 4096
}
// 启用 Claude thinking 模式时,temperature 必须固定为 1,max_tokens 需大于 budget_tokens // Logan.Li
enabled := req.ReasoningEnabled || c.cfg.ReasoningEnabled
if enabled && isAnthropicThinkingModel(payload.Model) {
payload.Temperature = 1
budget := payload.MaxTokens / 2
if budget < 1024 {
budget = 1024
}
if payload.MaxTokens <= budget {
payload.MaxTokens = budget + 1024
}
payload.Thinking = &anthropicThinking{Type: "enabled", BudgetTokens: budget}
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("序列化请求失败: %w", err)
}
return body, nil
}
// newHTTPRequest 构造 POST /v1/messages 请求(Bearer + anthropic-version) // Logan.Li
func (c *AnthropicClient) newHTTPRequest(ctx context.Context, body []byte) (*http.Request, error) {
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.cfg.Endpoint+"/v1/messages", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("构造请求失败: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+c.cfg.APIKey)
httpReq.Header.Set("anthropic-version", "2023-06-01")
return httpReq, nil
}
// Complete 一次性补全(Anthropic /v1/messages) // Logan.Li
// 响应 content blocks:text 拼为 Content,tool_use 转为 ToolCalls;usage 映射。
func (c *AnthropicClient) Complete(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) {
body, err := c.buildRequest(req, false)
if err != nil {
return nil, err
}
httpReq, err := c.newHTTPRequest(ctx, body)
if err != nil {
return nil, err
}
resp, err := c.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("LLM 连接失败: %w", err)
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("LLM 返回错误 (HTTP %d): %s", resp.StatusCode, extractErrorMessage(raw))
}
var parsed anthropicResponse
if err := json.Unmarshal(raw, &parsed); err != nil {
return nil, fmt.Errorf("解析 LLM 响应失败: %w", err)
}
if parsed.Error != nil && parsed.Error.Message != "" {
return nil, fmt.Errorf("LLM 错误: %s", parsed.Error.Message)
}
var textBuf strings.Builder
var toolCalls []ToolCall
for _, b := range parsed.Content {
switch b.Type {
case "text", "thinking":
textBuf.WriteString(b.Text)
case "tool_use":
args := string(b.Input)
if strings.TrimSpace(args) == "" {
args = "{}"
}
toolCalls = append(toolCalls, ToolCall{
ID: b.ID, Type: "function",
Function: FunctionCall{Name: b.Name, Arguments: args},
})
}
}
return &CompletionResponse{
ID: parsed.ID,
Model: parsed.Model,
Message: Message{
Role: "assistant",
Content: textBuf.String(),
ToolCalls: toolCalls,
},
Usage: Usage{
PromptTokens: parsed.Usage.InputTokens,
CompletionTokens: parsed.Usage.OutputTokens,
TotalTokens: parsed.Usage.InputTokens + parsed.Usage.OutputTokens,
},
}, nil
}
// Stream 流式补全(Anthropic SSE) // Logan.Li
// 逐 token 回调 text_delta;usage 从 message_start(input) + message_delta(output) 取。
// 注:流式不组装 tool_use,仅用于智能助手纯对话;Agent CLI ReAct 循环必须使用 Complete。
func (c *AnthropicClient) Stream(ctx context.Context, req *CompletionRequest, onDelta func(string)) (*CompletionResponse, error) {
body, err := c.buildRequest(req, true)
if err != nil {
return nil, err
}
httpReq, err := c.newHTTPRequest(ctx, body)
if err != nil {
return nil, err
}
httpReq.Header.Set("Accept", "text/event-stream")
streamClient := &http.Client{Timeout: 0} // 流式由 ctx 控制超时
resp, err := streamClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("LLM 连接失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
raw, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("LLM 返回错误 (HTTP %d): %s", resp.StatusCode, extractErrorMessage(raw))
}
var contentBuilder strings.Builder
var usage Usage
var eventType string
reader := bufio.NewReader(resp.Body)
for {
line, readErr := reader.ReadString('\n')
trimmed := strings.TrimSpace(line)
switch {
case strings.HasPrefix(trimmed, "event:"):
eventType = strings.TrimSpace(strings.TrimPrefix(trimmed, "event:"))
case strings.HasPrefix(trimmed, "data:"):
data := strings.TrimSpace(strings.TrimPrefix(trimmed, "data:"))
if data != "" && data != "[DONE]" {
switch eventType {
case "content_block_delta":
var d struct {
Delta struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"delta"`
}
if json.Unmarshal([]byte(data), &d) == nil && d.Delta.Type == "text_delta" && d.Delta.Text != "" {
contentBuilder.WriteString(d.Delta.Text)
if onDelta != nil {
onDelta(d.Delta.Text)
}
}
case "message_start":
var d struct {
Message struct {
Usage struct {
InputTokens int `json:"input_tokens"`
} `json:"usage"`
} `json:"message"`
}
if json.Unmarshal([]byte(data), &d) == nil {
usage.PromptTokens = d.Message.Usage.InputTokens
}
case "message_delta":
var d struct {
Usage struct {
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
}
if json.Unmarshal([]byte(data), &d) == nil {
usage.CompletionTokens = d.Usage.OutputTokens
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
}
}
}
eventType = "" // 消费完 data 重置,防默认事件串用
}
if readErr != nil {
break
}
}
return &CompletionResponse{
Model: c.cfg.Model,
Message: Message{Role: "assistant", Content: contentBuilder.String()},
Usage: usage,
}, nil
}