feat: 初始化NewLife Studio项目,完成基础框架与数据管理模块
何炳宏 authored at 2026-05-26 12:09:09
8.41 KiB
NewLife.Studio
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Interactivity;
using NewLife.Studio.AI;
using NewLife.Studio.AI.ToolCalling;
using NewLife.Studio.AI.ToolCalling.BuiltInTools;
using NewLife.Studio.AI.Models;
using NewLife.Studio.Store;
using NewLife.Studio.Data;
using NewLife.Studio.Core;
using NewLife.Studio.Core.DTOs;
using NewLife.Log;

namespace NewLife.Studio.App.Controls;

public partial class AIPanel : UserControl
{
    private AIService? _aiService;
    private IStoreService? _storeService;
    private IDataProvider? _dataProvider;
    private IDbSession? _activeSession;
    private bool _configured;

    public AIPanel()
    {
        InitializeComponent();

        SendButton.Click += async (_, _) =>
        {
            if (_aiService == null || !_configured)
            {
                AddMessage("系统", "请先在设置中配置 AI 服务", Colors.Orange);
                return;
            }

            var text = ChatInput.Text?.Trim();
            if (string.IsNullOrEmpty(text)) return;

            ChatInput.Text = "";
            AddMessage("用户", text, Colors.DodgerBlue);

            try
            {
                SendButton.IsEnabled = false;
                var reply = await _aiService.ChatAsync(text, (tc, tr) =>
                {
                    AddToolCall(tc, tr);
                });

                if (!string.IsNullOrEmpty(reply))
                {
                    AddMessage("AI", reply, Colors.DimGray);
                }
            }
            catch (Exception ex)
            {
                AddMessage("错误", ex.Message, Colors.Red);
            }
            finally
            {
                SendButton.IsEnabled = true;
            }
        };
    }

    public void Initialize(IStoreService storeService, IDataProvider dataProvider)
    {
        _storeService = storeService;
        _dataProvider = dataProvider;
        _ = LoadAiConfigAsync();
    }

    private async Task LoadAiConfigAsync()
    {
        if (_storeService == null) return;

        var profile = await _storeService.GetAiProfileAsync();
        if (profile != null && !string.IsNullOrEmpty(profile.ApiKey))
        {
            var provider = AIProviderFactory.Create(
                StudioServices.GetRequiredService<HttpClient>(),
                profile.ProviderType,
                profile.Endpoint,
                profile.ApiKey,
                profile.Model);

            var registry = new ToolRegistry();
            RegisterBuiltInTools(registry);

            _aiService = new AIService(provider, registry,
                "You are a database assistant for NewLife Studio. You can analyze database structures and execute read-only queries. Always explain your analysis in the user's language.");

            _configured = true;
            AddMessage("系统", "AI 助手已就绪", Colors.Green);
            XTrace.WriteLine("AIPanel: AI service initialized");
        }
        else
        {
            AddMessage("系统", "请先配置 AI 服务(设置 -> AI 配置)", Colors.Orange);
            XTrace.WriteLine("AIPanel: AI not configured");
        }
    }

    private void RegisterBuiltInTools(ToolRegistry registry)
    {
        BuiltInDatabaseTools.RegisterAll(registry,
            listConnections: async () =>
            {
                if (_storeService == null) return "[]";
                var conns = await _storeService.ListConnectionsAsync();
                var names = conns.Select(c => $"{c.Name} ({c.ProviderType})");
                return string.Join("\n", names);
            },
            openDatabase: async (name) =>
            {
                if (_dataProvider == null || _storeService == null) return "No data provider";
                var conns = await _storeService.ListConnectionsAsync();
                var conn = conns.FirstOrDefault(c => c.Name == name);
                if (conn == null) return $"Connection '{name}' not found";
                _activeSession = await _dataProvider.OpenSessionAsync(conn);
                return $"Opened: {_activeSession.SessionId}";
            },
            listTables: async (_) =>
            {
                if (_activeSession == null) return "No active session";
                var tables = await _activeSession.GetTablesAsync();
                return string.Join("\n", tables.Select(t => $"{t.Name} (rows: {t.RowCount})"));
            },
            describeTable: async (table) =>
            {
                if (_activeSession == null) return "No active session";
                var cols = await _activeSession.GetColumnsAsync(table);
                var lines = cols.Select(c =>
                    $"{c.Name}: {c.DataType}{(c.IsNullable ? " NULL" : " NOT NULL")}{(c.IsPrimaryKey ? " PK" : "")}");
                return string.Join("\n", lines);
            },
            executeSelect: async (sql) =>
            {
                if (_activeSession == null) return "No active session";
                var result = await _activeSession.ExecuteQueryAsync(new QueryRequest
                {
                    Sql = sql,
                    MaxRows = 100,
                    TimeoutSeconds = 30
                });

                if (result.Error != null) return $"Error: {result.Error}";
                var lines = new List<string> { $"Columns: {string.Join(", ", result.Columns.Select(c => c.Name))}" };
                lines.Add($"Rows: {result.RowCount}, Time: {result.ElapsedMs}ms");
                foreach (var row in result.Rows.Take(10))
                {
                    lines.Add(string.Join(" | ", row.Select(v => v?.ToString() ?? "NULL")));
                }
                if (result.Truncated) lines.Add("(results truncated)");
                return string.Join("\n", lines);
            },
            sampleData: async (table, limit) =>
            {
                if (_activeSession == null) return "No active session";
                var result = await _activeSession.ExecuteQueryAsync(new QueryRequest
                {
                    Sql = $"SELECT * FROM {table} LIMIT {limit}",
                    MaxRows = limit,
                    TimeoutSeconds = 30
                });
                if (result.Error != null) return $"Error: {result.Error}";
                return $"Sample from {table} ({result.RowCount} rows, {result.ElapsedMs}ms):\n" +
                       string.Join("\n", result.Rows.Select(r => string.Join(" | ", r.Select(v => v?.ToString() ?? "NULL"))));
            }
        );
    }

    private void AddMessage(string role, string content, Color color)
    {
        var border = new Border
        {
            Background = new SolidColorBrush(color, 0.1),
            CornerRadius = new Avalonia.CornerRadius(4),
            Padding = new Avalonia.Thickness(8),
            Margin = new Avalonia.Thickness(0, 4)
        };

        var panel = new StackPanel();
        panel.Children.Add(new TextBlock
        {
            Text = $"[{role}]",
            FontWeight = FontWeight.Bold,
            FontSize = 11,
            Foreground = new SolidColorBrush(color)
        });
        panel.Children.Add(new TextBlock
        {
            Text = content,
            TextWrapping = TextWrapping.Wrap,
            FontSize = 12
        });

        border.Child = panel;
        ChatHistory.Items.Add(border);
    }

    private void AddToolCall(ToolCall tc, ToolResult tr)
    {
        var expander = new Expander
        {
            Header = $"Tool: {tc.Function.Name}",
            Margin = new Avalonia.Thickness(0, 2)
        };

        var content = new StackPanel();
        content.Children.Add(new TextBlock
        {
            Text = $"Args: {tc.Function.Arguments}",
            FontSize = 10,
            Foreground = Brushes.Gray,
            TextWrapping = TextWrapping.Wrap
        });

        if (tr.Error != null)
        {
            content.Children.Add(new TextBlock
            {
                Text = $"Error: {tr.Error}",
                FontSize = 10,
                Foreground = Brushes.Red,
                TextWrapping = TextWrapping.Wrap
            });
        }
        else
        {
            content.Children.Add(new TextBlock
            {
                Text = $"Result: {tr.Output}",
                FontSize = 10,
                Foreground = Brushes.DarkGreen,
                TextWrapping = TextWrapping.Wrap,
                MaxHeight = 200
            });
        }

        expander.Content = new ScrollViewer { Content = content };
        ChatHistory.Items.Add(expander);
    }
}