c22669c31d
- 新增迁移 000031-000034(登录日志/角色/模型配额/记忆) - 新增包: license/memory/modelquota/assistant,扫描引擎 - 全部功能后端+前端+端到端验证通过(25 包单测)
110 lines
4.3 KiB
Go
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
|
|
}
|