السافت تولز

سلام! من Elesoft Chat (السافت چت) هستم.
کدی که ارسال کردید، یک کلاس کمکی (Helper Class) بسیار کاربردی و استاندارد به زبان C# است که در فضای نام (Namespace) ToolsOfElesoftV2025.Models.Ai قرار دارد. وظیفه اصلی این کد، برقراری ارتباط با سرویس هوش مصنوعی (که ظاهراً مبتنی بر مدل‌های جمنای گوگل و از طریق یک اسکریپت PHP در سرور api-ai.elft.ir پیاده‌سازی شده) است.
در ادامه، بخش به بخش این کد را به زبان ساده و فنی برای شما شرح می‌دهم:

۱. مدل داده‌ای پیام (AiMessage)

public class AiMessage
{
    public string Role { get; set; } = "user"; // فقط "user" یا "model" (فرمت Gemini)
    public string Text { get; set; } = "";
}
  • این کلاس ساختار هر پیام در گفتگو را مشخص می‌کند.
  • Role: مشخص می‌کند که فرستنده پیام کاربر است (user) یا هوش مصنوعی (model)؛ این دقیقاً مطابق با استانداردهای API گوگل جمنای است.
  • Text: متن پیام.

۲. متد استریمینگ (GetFromAiStreamAsync)

این مهم‌ترین متد این کلاس است که پاسخ هوش مصنوعی را به‌صورت تکه‌تکه (Stream) و لحظه‌ای دریافت می‌کند (شبیه به حالت تایپ شدن متن توسط ربات).
  • پارامترها:
    • لیستی از پیام‌ها (messages) برای حفظ تاریخچه گفتگو.
    • تنظیمات پیشرفته مثل تفکر عمیق (think)، جستجوی وب (search)، و ارسال تصویر (hasImage به همراه اطلاعات بیس‌کد تصویر).
  • نحوه عملکرد:
    1. اطلاعات را به فرمت JSON تبدیل کرده و با استفاده از HttpClient به آدرس https://api-ai.elft.ir/ai.php ارسال می‌کند.
    2. پاسخ را به‌صورت استریم (جریان داده) می‌خواند تا کاربر مجبور نباشد منتظر بماند تا کل پاسخ آماده شود.
    3. مدیریت خطاهای سرور: متد هوشمندانه به دنبال یک نشانه‌ی خاص به نام \u0001ERR\u0001 می‌گردد. اگر سرور PHP در حین استریم کردن خطایی بفرستد، این کد آن را تشخیص داده، متن خطا را استخراج کرده و به کاربر نمایش می‌دهد (و از فرستادن کدهای اضافی جلوگیری می‌کند).
    4. از یک متغیر بافر (carry) استفاده می‌کند تا اگر نشانه‌ی خطا دقیقاً بین دو تکه (Chunk) از داده‌ها تقسیم شد، آن را گم نکند و به‌درستی تشخیص دهد.

۳. متدهای غیر استریم (GetFromAiAsync)

برای زمان‌هایی که نیازی به نمایش لحظه‌ای متن نداریم و کل پاسخ را یکجا می‌خواهیم:
  • نسخه‌ی اصلی: تمام تکه‌های دریافتی از متد استریم را با استفاده از StringBuilder به هم می‌چسباند و متن نهایی و کامل را برمی‌گرداند (Task<string>).
  • نسخه‌ی ساده‌شده (Overload): یک متد کمکی که فقط یک رشته متن (string text) به‌عنوان ورودی می‌گیرد، آن را تبدیل به یک شیء AiMessage با نقش user می‌کند و به متد اصلی می‌فرستد. این کار برای سازگاری با کدهای قدیمی‌تر پروژه گذاشته شده است.

۴. تولید خودکار عنوان چت (GetChatTitleAsync)

این متد یک قابلیت بسیار جذاب و کاربردی برای برنامه‌های چت (مثل ChatGPT) است:
  • کاربرد: وقتی کاربر اولین پیام خود را ارسال می‌کند، این متد یک درخواست سبک و سریع به هوش مصنوعی می‌فرستد و از آن می‌خواهد که یک عنوان کوتاه (حداکثر ۶ کلمه‌ای) متناسب با صحبت کاربر پیشنهاد دهد.
  • پاکسازی خروجی:
    • ابتدا پاسخ هوش مصنوعی را Trim می‌کند (فاصله‌های اضافی را می‌گیرد).
    • با استفاده از Regex کلماتی مثل Title: یا عنوان: را اگر هوش مصنوعی به اشتباه در ابتدا آورده باشد، حذف می‌کند.
    • علامت‌های نگارشی اضافی و نقل‌قول‌ها (", ', «, ») را از دور متن پاک می‌کند.
    • اگر طول عنوان بیشتر از ۸۰ کاراکتر شد، آن را برش می‌زند تا همیشه یک عنوان استاندارد و زیبا تحویل دهد.

💡 جمع‌بندی

این کد یک پیاده‌سازی بسیار حرفه‌ای، بهینه و ایمن در C# است که از قابلیت‌های مدرن این زبان (مثل IAsyncEnumerable برای استریمینگ و CancellationToken برای لغو درخواست‌ها) بهره می‌برد و به‌خوبی می‌تواند با بک‌اند PHP شما ارتباط برقرار کند.
اگر سوالی در موردجای خاصی از این کد یا نحوه استفاده از آن دارید، بفرمایید تا بیشتر توضیح دهم!
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
namespace ToolsOfElesoftV2025.Models.Ai
{
public class AiMessage
{
public string Role { get; set; } = "user"; // فقط "user" یا "model" (فرمت Gemini)
public string Text { get; set; } = "";
}
public static class Api
{
private const string ErrMarker = "\u0001ERR\u0001";
/// <summary>
/// نسخه‌ی استریم: تکه‌های متن رو همون لحظه که از PHP/Gemini می‌رسن برمی‌گردونه.
/// think/search/hasImage تعیین می‌کنن سمت PHP از کدوم مدل و ابزار استفاده بشه.
/// imageBase64/imageMimeType در صورت وجود، به‌صورت inline_data به پیام آخر (پیام فعلی کاربر) اضافه می‌شن.
/// </summary>
public static async IAsyncEnumerable<string> GetFromAiStreamAsync(
List<AiMessage> messages,
bool think = false,
bool search = false,
bool hasImage = false,
string? imageBase64 = null,
string? imageMimeType = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var url = "https://api-ai.elft.ir/ai.php"; // دامنه خودت رو بزن
using var client = new HttpClient();
client.Timeout = TimeSpan.FromMinutes(3);
var data = new
{
messages = messages.Select(m => new { role = m.Role, text = m.Text }),
think = think,
search = search,
hasImage = hasImage,
image = (hasImage && !string.IsNullOrEmpty(imageBase64))
? new { mimeType = imageMimeType ?? "image/jpeg", data = imageBase64 }
: null
};
using var request = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = new StringContent(
Newtonsoft.Json.JsonConvert.SerializeObject(data),
Encoding.UTF8,
"application/json")
};
using var response = await client.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
if (!response.IsSuccessStatusCode)
{
yield return $"HTTP Error: {response.StatusCode}";
yield break;
}
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
using var reader = new StreamReader(stream, Encoding.UTF8);
var buffer = new char[512];
var carry = ""; // بخش انتهایی که ممکنه شروعِ نشانه‌ی خطا باشه، تا chunk بعدی نگه می‌داریم
while (!reader.EndOfStream)
{
int readCount = await reader.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken);
if (readCount == 0) continue;
var combined = carry + new string(buffer, 0, readCount);
carry = "";
var markerIndex = combined.IndexOf(ErrMarker, StringComparison.Ordinal);
if (markerIndex >= 0)
{
var before = combined.Substring(0, markerIndex);
var errMsg = combined.Substring(markerIndex + ErrMarker.Length);
if (!string.IsNullOrEmpty(before))
yield return before;
yield return $"\n\n[خطا در دریافت پاسخ: {errMsg.Trim()}]";
yield break;
}
// اگه انتهای این تکه شبیه شروع نشانه‌ی خطا بود، نگهش داریم تا با تکه‌ی بعدی چک بشه
var safeLength = Math.Max(0, combined.Length - (ErrMarker.Length - 1));
var toKeep = combined.Length - safeLength;
if (toKeep > 0)
{
carry = combined.Substring(safeLength);
combined = combined.Substring(0, safeLength);
}
if (combined.Length > 0)
{
yield return combined;
}
}
if (!string.IsNullOrEmpty(carry))
{
yield return carry;
}
}
/// <summary>
/// نسخه‌ی غیر استریم (برای جاهایی که فقط متن نهایی کامل لازمه)
/// </summary>
public static async Task<string> GetFromAiAsync(
List<AiMessage> messages,
bool think = false,
bool search = false,
bool hasImage = false,
string? imageBase64 = null,
string? imageMimeType = null)
{
var sb = new StringBuilder();
await foreach (var piece in GetFromAiStreamAsync(messages, think, search, hasImage, imageBase64, imageMimeType))
{
sb.Append(piece);
}
return sb.ToString();
}
// نسخه‌ی قدیمی: برای سازگاری با هر جای دیگه‌ی پروژه که هنوز با یک متن تخت صداش می‌زنه
public static Task<string> GetFromAiAsync(string text)
{
return GetFromAiAsync(new List<AiMessage>
{
new AiMessage { Role = "user", Text = text }
});
}
/// <summary>
/// یه عنوان کوتاه (چندکلمه‌ای) برای چت، بر اساس اولین پیام کاربر، از خود هوش مصنوعی می‌گیره.
/// این یه درخواست جدا و سبک هست (غیر استریم، بدون think/search/تصویر => سریع‌ترین مدل پیش‌فرض)
/// و ربطی به تاریخچه‌ی اصلی مکالمه نداره.
/// </summary>
public static async Task<string> GetChatTitleAsync(string userMessage)
{
var prompt =
"Based on the user message below, write a short topic/title for this chat. " +
"Reply with ONLY the title itself — no quotes, no punctuation at the end, no explanation, no prefix like 'Title:'. " +
"Maximum 6 words. Use the same language as the user's message.\n\n" +
"User message:\n" + userMessage;
var messages = new List<AiMessage>
{
new AiMessage { Role = "user", Text = prompt }
};
var result = await GetFromAiAsync(messages);
if (string.IsNullOrWhiteSpace(result))
{
return "";
}
result = result.Trim();
// حذف پیشوندهای احتمالی مثل "Title:" یا "عنوان:"
result = Regex.Replace(result, @"^(title|عنوان)\s[::]\s", "", RegexOptions.IgnoreCase);
// حذف نقل‌قول‌های احتمالی دور عنوان
result = result.Trim('"', '\'', '«', '»', '“', '”').Trim();
// احتیاطاً یه سقف طول هم می‌ذاریم، برای وقتی مدل به محدودیت کلمه‌ای پایبند نبود
if (result.Length > 80)
{
result = result.Substring(0, 80).TrimEnd();
}
return result;
}
}
}
این کد کامل شرح بده