Files
ai-gateway-go/internal/assistant/service.go
T
superidou c22669c31d 0.11.0: 旗舰版功能补齐(License/登录记录/会话管理/角色管理/门户定时任务/模型配额/输出脱敏/供应链扫描/记忆管理/AI助手/真实概览)
- 新增迁移 000031-000034(登录日志/角色/模型配额/记忆)
- 新增包: license/memory/modelquota/assistant,扫描引擎
- 全部功能后端+前端+端到端验证通过(25 包单测)
2026-08-13 11:37:18 +08:00

110 lines
4.3 KiB
Go

// Package assistant 实现管理平台 AI 助手:基于实时平台统计信息(供应商、
// 模型、账号、用量、事件投递等)回答管理员的自然语言问题。
package assistant
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"time"
"aigateway.local/core/internal/gateway"
"github.com/jackc/pgx/v5/pgxpool"
)
var ErrUnavailable = errors.New("AI 助手服务暂不可用")
// Resolver 提供默认模型供应商(注入 gateway 的 provider resolver)。
type Resolver interface {
Resolve(code string) (gateway.ResolvedAdapter, error)
}
// Service 管理平台 AI 助手。
type Service struct {
pool *pgxpool.Pool
resolver Resolver
client *http.Client
logger *slog.Logger
model string
}
func NewService(pool *pgxpool.Pool, resolver Resolver, model string, logger *slog.Logger) *Service {
return &Service{
pool: pool, resolver: resolver, logger: logger, model: model,
client: &http.Client{Timeout: 60 * time.Second},
}
}
// Answer 回答管理员提问。
func (s *Service) Answer(ctx context.Context, message string) (string, error) {
if s == nil || s.resolver == nil {
return "", ErrUnavailable
}
resolved, err := s.resolver.Resolve("")
if err != nil {
return "", fmt.Errorf("%w: 未配置默认模型供应商", ErrUnavailable)
}
model := s.model
if model == "" {
model = "gpt-4o-mini" // 兜底;实际以上游支持为准
}
systemPrompt, err := s.platformSummary(ctx)
if err != nil {
s.logger.Warn("assistant platform summary failed", "error", err)
systemPrompt = "你是 AI 网关管理助手的系统提示,请基于平台知识回答。"
}
payload, _ := json.Marshal(map[string]any{
"model": model,
"messages": []map[string]any{
{"role": "system", "content": systemPrompt},
{"role": "user", "content": message},
},
"temperature": 0.2,
})
request, err := http.NewRequestWithContext(ctx, http.MethodPost, resolved.Adapter.Target().String()+"/v1/chat/completions", bytes.NewReader(payload))
if err != nil {
return "", err
}
request.Header.Set("Content-Type", "application/json")
resolved.Adapter.Prepare(request)
response, err := s.client.Do(request)
if err != nil {
return "", fmt.Errorf("%w: 模型调用失败: %v", ErrUnavailable, err)
}
defer response.Body.Close()
raw, _ := io.ReadAll(io.LimitReader(response.Body, 1<<20))
if response.StatusCode/100 != 2 {
return "", fmt.Errorf("%w: 模型返回 HTTP %d", ErrUnavailable, response.StatusCode)
}
var decoded struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
if json.Unmarshal(raw, &decoded) != nil || len(decoded.Choices) == 0 {
return "", fmt.Errorf("%w: 模型响应格式无效", ErrUnavailable)
}
return decoded.Choices[0].Message.Content, nil
}
// platformSummary 汇总平台实时状态注入系统提示。
func (s *Service) platformSummary(ctx context.Context) (string, error) {
var providers, models, admins, portals, apiKeys, pendingOutbox, todayRequests, todayTokens int64
_ = s.pool.QueryRow(ctx, `SELECT (SELECT count(*) FROM gateway.providers WHERE enabled), (SELECT count(*) FROM gateway.provider_models WHERE enabled), (SELECT count(*) FROM gateway.admin_accounts), (SELECT count(*) FROM gateway.portal_users), (SELECT count(*) FROM gateway.api_keys WHERE enabled), (SELECT count(*) FROM gateway.outbox_events WHERE status='pending'), (SELECT count(*) FROM gateway.audit_events WHERE recorded_at >= date_trunc('day', now())), (SELECT COALESCE(sum(prompt_tokens+completion_tokens),0) FROM gateway.audit_events WHERE recorded_at >= date_trunc('day', now()))`).Scan(&providers, &models, &admins, &portals, &apiKeys, &pendingOutbox, &todayRequests, &todayTokens)
return fmt.Sprintf(`你是 AI 网关管理助手。以下是平台实时状态(由系统注入,回答时请引用准确数字):
- 启用模型供应商: %d 个
- 启用的上游模型: %d 个
- 管理员账号: %d 个, 门户账号: %d 个
- 启用 API Key: %d 个
- 待处理 outbox 事件: %d 条
- 今日请求: %d 次, 今日 Token 消耗: %d
请用中文简洁回答管理员的问题;涉及平台配置建议时说明操作路径(如"供应商管理→新增供应商")。`, providers, models, admins, portals, apiKeys, pendingOutbox, todayRequests, todayTokens), nil
}