Files
2026-10-01 01:16:52 +03:30

648 lines
28 KiB
HTML
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.
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>پیاده‌سازی استخراج محتوا با استفاده از xFileService در xAiApi - فن آوران ساحر علم</title>
<style>
:root {
--primary: #1e3a8a;
--secondary: #3b82f6;
--accent: #f59e0b;
--success: #10b981;
--danger: #ef4444;
--warning: #f97316;
--bg-light: #f8fafc;
--bg-code: #1e293b;
--text-dark: #0f172a;
--text-muted: #64748b;
--border: #e2e8f0;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Tahoma', 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #f8fafc 0%, #e0e7ff 100%);
color: var(--text-dark);
line-height: 1.8;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
color: white;
padding: 40px;
text-align: center;
position: relative;
}
.header h1 { font-size: 2.2em; margin-bottom: 10px; position: relative; }
.header .subtitle { font-size: 1.1em; opacity: 0.95; position: relative; }
.meta-bar {
display: flex;
justify-content: space-between;
background: var(--bg-light);
padding: 15px 30px;
border-bottom: 2px solid var(--border);
flex-wrap: wrap;
gap: 15px;
}
.meta-item { display: flex; align-items: center; gap: 8px; font-size: 0.9em; color: var(--text-muted); }
.meta-item strong { color: var(--primary); }
.content { padding: 40px; }
.section {
margin-bottom: 35px;
padding: 25px;
background: var(--bg-light);
border-radius: 12px;
border-right: 5px solid var(--secondary);
}
.section h2 {
color: var(--primary);
font-size: 1.6em;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid var(--border);
display: flex;
align-items: center;
gap: 10px;
}
.section h3 { color: var(--secondary); font-size: 1.2em; margin: 20px 0 12px; }
.step {
background: white;
padding: 20px;
margin: 15px 0;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
border-right: 4px solid var(--accent);
}
.step-number {
display: inline-block;
background: var(--accent);
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
text-align: center;
line-height: 32px;
font-weight: bold;
margin-left: 10px;
}
.arch-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin: 20px 0;
}
.arch-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
border-top: 4px solid var(--secondary);
transition: transform 0.3s;
}
.arch-card:hover { transform: translateY(-5px); }
.arch-card h4 { color: var(--primary); margin-bottom: 12px; font-size: 1.15em; }
.arch-card ul { list-style: none; padding-right: 0; }
.arch-card li { padding: 6px 0; padding-right: 20px; position: relative; font-size: 0.95em; }
.arch-card li::before { content: '▸'; position: absolute; right: 0; color: var(--accent); font-weight: bold; }
.tech-badge {
display: inline-block;
background: var(--secondary);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
margin: 3px;
}
.tech-badge.primary { background: var(--primary); }
.tech-badge.success { background: var(--success); }
.tech-badge.accent { background: var(--accent); }
pre {
background: var(--bg-code);
color: #e2e8f0;
padding: 18px;
border-radius: 8px;
overflow-x: auto;
direction: ltr;
text-align: left;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 0.85em;
margin: 15px 0;
border-right: 4px solid var(--accent);
}
code {
background: #fef3c7;
color: #92400e;
padding: 2px 8px;
border-radius: 4px;
font-family: 'Consolas', monospace;
font-size: 0.9em;
direction: ltr;
display: inline-block;
}
table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
th { background: var(--primary); color: white; padding: 12px; text-align: right; font-weight: bold; }
td { padding: 12px; border-bottom: 1px solid var(--border); }
tr:hover { background: var(--bg-light); }
.flow-diagram {
background: white;
padding: 25px;
border-radius: 10px;
margin: 20px 0;
text-align: center;
}
.flow-step {
display: inline-block;
background: var(--secondary);
color: white;
padding: 10px 20px;
border-radius: 8px;
margin: 5px;
font-size: 0.9em;
}
.flow-arrow { display: inline-block; color: var(--accent); font-size: 1.5em; margin: 0 8px; vertical-align: middle; }
.alert { padding: 15px 20px; border-radius: 8px; margin: 15px 0; border-right: 4px solid; }
.alert-info { background: #dbeafe; border-color: var(--secondary); color: #1e40af; }
.alert-success { background: #d1fae5; border-color: var(--success); color: #065f46; }
.alert-warning { background: #fef3c7; border-color: var(--accent); color: #92400e; }
.footer { background: var(--primary); color: white; padding: 25px; text-align: center; margin-top: 40px; }
.footer p { margin: 5px 0; }
.highlight { background: linear-gradient(120deg, #fef3c7 0%, #fef3c7 100%); padding: 2px 6px; border-radius: 4px; font-weight: bold; }
.toc { background: white; padding: 20px; border-radius: 10px; margin-bottom: 25px; border: 2px solid var(--border); }
.toc h3 { color: var(--primary); margin-bottom: 15px; }
.toc ol { padding-right: 25px; }
.toc li { padding: 6px 0; }
.toc a { color: var(--secondary); text-decoration: none; transition: color 0.2s; }
.toc a:hover { color: var(--primary); text-decoration: underline; }
.file-change { background: #f0f9ff; border-right: 4px solid var(--secondary); padding: 15px; margin: 10px 0; border-radius: 8px; }
.file-change .path { font-family: 'Consolas', monospace; color: var(--primary); font-weight: bold; direction: ltr; display: inline-block; }
.badge-new { background: var(--success); color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.75em; margin-right: 8px; }
.badge-modify { background: var(--warning); color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.75em; margin-right: 8px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📎 پیاده‌سازی استخراج محتوا با ماژول xFileService</h1>
<div class="subtitle">یکپارچه‌سازی XFileProvider در XAiServiceBase برای پردازش فایل‌های ضمیمه</div>
</div>
<div class="meta-bar">
<div class="meta-item">👨‍💻 <strong>توسعه‌دهنده:</strong> هادی خزاعی اصل</div>
<div class="meta-item">🏢 <strong>شرکت:</strong> فن آوران ساحر علم</div>
<div class="meta-item">📅 <strong>تاریخ تهیه مستند:</strong> چهارشنبه ۸ مهر ۱۴۰۵</div>
<div class="meta-item">📦 <strong>پروژه:</strong> xAiApi</div>
</div>
<div class="content">
<div class="toc">
<h3>📑 فهرست مطالب</h3>
<ol>
<li><a href="#strategy">استراتژی یکپارچه‌سازی</a></li>
<li><a href="#step1">گام ۱: به‌روزرسانی Controller برای آپلود فایل</a></li>
<li><a href="#step2">گام ۲: تزریق IXFileProvider به XAiServiceBase</a></li>
<li><a href="#step3">گام ۳: پیاده‌سازی منطق استخراج محتوا از فایل</a></li>
<li><a href="#step4">گام ۴: الحاق محتوا به ChatMessage و ذخیره Metadata</a></li>
<li><a href="#step5">گام ۵: به‌روزرسانی Extension مدل‌ها</a></li>
<li><a href="#flow">جریان کامل داده‌ها</a></li>
<li><a href="#summary">خلاصه تغییرات</a></li>
</ol>
</div>
<!-- Section 1: Strategy -->
<div class="section" id="strategy">
<h2>🎯 گام ۱: استراتژی یکپارچه‌سازی</h2>
<p>
به جای مدیریت مستقیم <code>IFormFile</code> در لایه سرویس هوش مصنوعی، از معماری تمیز (Clean Architecture) پیروی می‌کنیم:
</p>
<div class="arch-grid">
<div class="arch-card">
<h4>۱. لایه ارائه (Controller)</h4>
<ul>
<li>دریافت <code>IFormFileCollection</code></li>
<li>فراخوانی <code>IXFileProvider.Upload</code></li>
<li>دریافت لیست <code>XFileDto</code></li>
</ul>
</div>
<div class="arch-card">
<h4>۲. لایه سرویس (XAiServiceBase)</h4>
<ul>
<li>دریافت <code>IEnumerable&lt;XFileDto&gt;</code></li>
<li>فراخوانی <code>IXFileProvider.GetFileDescriptor</code></li>
<li>خواندن Stream و تبدیل به <code>AIContent</code></li>
</ul>
</div>
<div class="arch-card">
<h4>۳. لایه داده (Metadata)</h4>
<ul>
<li>ذخیره <code>FileId</code> در فیلد <code>MetaDatas</code> پیام</li>
<li>حفظ رابطه بدون نیاز به جدول Join جدید</li>
</ul>
</div>
</div>
</div>
<!-- Step 1 -->
<div class="section" id="step1">
<h2>🎮 گام ۲: به‌روزرسانی Controller برای آپلود فایل</h2>
<p>ابتدا باید فایل‌ها را از طریق <code>XFileProvider</code> آپلود کنیم تا <code>XFileDto</code> دریافت شود.</p>
<div class="file-change">
<span class="badge-modify">MODIFY</span>
<span class="path">xAiApi/Controllers/XAiServiceControllerBase.cs</span>
</div>
<pre>// ۱. افزودن وابستگی به Constructor
private readonly IXFileProvider _fileProvider;
protected XAiServiceControllerBase(
// ... پارامترهای قبلی
IXFileProvider fileProvider // ✅ جدید
) : base(...)
{
// ...
_fileProvider = fileProvider;
}
// ۲. اصلاح متد Ask
[HttpPost("Ask")]
[Consumes("multipart/form-data")]
public async Task&lt;ActionResult&lt;string&gt;&gt; Ask(
[FromForm] XAiResponseRequest request,
[FromForm] IFormFileCollection files,
CancellationToken cancellationToken = default
)
{
try
{
if (!request.IsValid()) XException.InvalidArgs.Throw();
var userInfo = await GetUserInfo();
var connectionId = GetConnectionId();
// ✅ آپلود فایل‌ها از طریق ماژول xFileService
IEnumerable&lt;XFileDto&gt; uploadedFiles = null;
if (files != null && files.Any())
{
uploadedFiles = await _fileProvider.Upload(
files: files,
userInfo: userInfo,
connectionId: connectionId,
cancellationToken: cancellationToken
);
}
// ✅ ارسال XFileDto به سرویس هوش مصنوعی
var result = await aiService.AskAsync(
prompt: request.Prompt,
ownerId: userInfo.UserId,
projectId: request.ProjectId,
conversationId: request.ConversationId,
connectionId: connectionId,
attachedFiles: uploadedFiles, // ✅ تغییر نوع پارامتر
cancellationToken: cancellationToken
);
return Ok(result);
}
catch (Exception ex)
{
return GetExceptionActionResult(ex);
}
}</pre>
<div class="alert alert-info">
<strong>💡 نکته:</strong> همین تغییر باید برای متد <code>AskStream</code> نیز اعمال شود.
</div>
</div>
<!-- Step 2 -->
<div class="section" id="step2">
<h2>⚙️ گام ۳: تزریق IXFileProvider به XAiServiceBase</h2>
<div class="file-change">
<span class="badge-modify">MODIFY</span>
<span class="path">xAiApi/Providers/XAIServiceBase.cs</span>
</div>
<pre>// ۱. افزودن فیلد و تزریق در Constructor
private readonly IXFileProvider _fileProvider;
protected XAIServiceBase(
IXAiDataProvider dataProvider,
ILogger&lt;XAIServiceBase&gt; logger,
XAiApiConfiguration configuration,
XValidationProvider validationProvider,
IXFileProvider fileProvider, // ✅ جدید
string model = null
)
{
this.dataProvider = dataProvider;
this.logger = logger;
this.configuration = configuration;
this.validationProvider = validationProvider;
this._fileProvider = fileProvider; // ✅ مقداردهی
Descriptor = configuration.GetModel(model);
Options = new ChatOptions();
}
// ۲. به‌روزرسانی امضای متد AskAsync در Interface و Implementation
public async Task&lt;XAiMessageDto&gt; AskAsync(
string prompt,
string ownerId,
Guid projectId,
Guid conversationId,
string connectionId = null,
IEnumerable&lt;XFileDto&gt; attachedFiles = null, // ✅ تغییر از IFormFileCollection
CancellationToken cancellationToken = default
)
{
// ... (کدهای اعتبارسنجی و دریافت Project/Conversation)
// ✅ پردازش فایل‌های ضمیمه
var fileContents = await ProcessAttachedFilesAsync(attachedFiles, cancellationToken);
// ... (ساخت promptMessage)
// ✅ ذخیره ارجاع فایل‌ها در MetaDatas پیام
if (attachedFiles != null && attachedFiles.Any())
{
var fileIds = attachedFiles.Select(f => f.Id).ToList();
promptMessage.MetaDatas = new Dictionary&lt;string, object&gt;
{
{ "AttachedFileIds", fileIds }
}.ToJSON();
}
promptMessage = await dataProvider.AddMessage(...);
// ✅ الحاق محتوا به ChatMessage
var promptChatMessage = promptMessage.ToChatMessages(fileContents);
var answer = await AskLLMAsync(history: history, prompt: promptChatMessage, cancellationToken: cancellationToken);
// ... (ذخیره پاسخ و بازگشت نتیجه)
}</pre>
</div>
<!-- Step 3 -->
<div class="section" id="step3">
<h2>🔍 گام ۴: پیاده‌سازی منطق استخراج محتوا از فایل</h2>
<p>این متد کمکی درون <code>XAiServiceBase</code> مسئول خواندن فایل از طریق <code>XFileProvider</code> و تبدیل آن به فرمت قابل فهم برای LLM است.</p>
<div class="file-change">
<span class="badge-new">NEW METHOD</span>
<span class="path">xAiApi/Providers/XAIServiceBase.cs</span>
</div>
<pre>/// &lt;summary&gt;
/// پردازش فایل‌های ضمیمه و تبدیل به AIContent
/// &lt;/summary&gt;
private async Task&lt;IList&lt;AIContent&gt;&gt; ProcessAttachedFilesAsync(
IEnumerable&lt;XFileDto&gt; files,
CancellationToken cancellationToken = default
)
{
var contents = new List&lt;AIContent&gt;();
if (files == null || !files.Any())
{
return contents;
}
foreach (var file in files)
{
try
{
// ✅ دریافت استریم فایل از ماژول xFileService
var descriptor = await _fileProvider.GetFileDescriptor(
id: file.Id,
cancellationToken: cancellationToken
);
if (descriptor == null || descriptor.Stream == null)
{
logger.LogWarning("فایل {FileName} یافت نشد یا قابل خواندن نیست.", file.FileName);
continue;
}
// ✅ تشخیص نوع فایل و پردازش مناسب
if (file.Type == XFileType.Image || descriptor.MIMEType.StartsWith("image/"))
{
// برای مدل‌های Vision: ارسال به صورت DataContent
using var memoryStream = new MemoryStream();
await descriptor.Stream.CopyToAsync(memoryStream, cancellationToken);
contents.Add(new DataContent(memoryStream.ToArray(), descriptor.MIMEType));
}
else
{
// برای فایل‌های متنی: خواندن محتوا و الحاق به Prompt
using var reader = new StreamReader(descriptor.Stream);
var textContent = await reader.ReadToEndAsync(cancellationToken);
// قالب‌بندی برای درک بهتر مدل از منبع متن
var formattedText = $"[File: {file.FileName} (Type: {descriptor.MIMEType})]\n{textContent}\n[/File]";
contents.Add(new TextContent(formattedText));
}
}
catch (Exception ex)
{
logger.LogError(ex, "خطا در پردازش فایل ضمیمه: {FileName}", file.FileName);
}
}
return contents;
}</pre>
<div class="alert alert-success">
<strong>✅ مزیت:</strong> با استفاده از <code>GetFileDescriptor</code>، ماژول هوش مصنوعی نیازی به دانستن جزئیات سیستم فایل (File System) ندارد و کاملاً از <code>xFileService</code> انتزاع یافته است.
</div>
</div>
<!-- Step 4 -->
<div class="section" id="step4">
<h2>🔌 گام ۵: به‌روزرسانی Extension مدل‌ها</h2>
<p>متد <code>ToChatMessages</code> باید بتواند محتوای استخراج شده از فایل‌ها را در کنار متن اصلی پیام قرار دهد.</p>
<div class="file-change">
<span class="badge-modify">MODIFY</span>
<span class="path">xAiModels/Extensions/XAiModelsExtensions.cs</span>
</div>
<pre>public static ChatMessage ToChatMessages(
this XAiMessageDto source,
IList&lt;AIContent&gt; additionalContents = null // ✅ پارامتر جدید
)
{
ChatMessage result = null;
if (!source.IsNullOrDefault())
{
var contents = new List&lt;AIContent&gt;();
// ۱. افزودن متن اصلی پیام (Prompt کاربر)
if (!string.IsNullOrWhiteSpace(source.Content))
{
contents.Add(new TextContent(source.Content));
}
// ۲. ✅ افزودن محتوای استخراج شده از فایل‌ها
if (additionalContents != null && additionalContents.Any())
{
contents.AddRange(additionalContents);
}
result = new ChatMessage
{
AuthorName = source.Role == XAiChatRole.User && !source.Owner.IsNullOrDefault()
? source.Owner.GetFullname()
: string.Empty,
Role = source.Role.ToChatRole(),
MessageId = source.Id.ToString(),
Contents = contents // ✅ لیست ترکیبی از متن و فایل
};
}
return result;
}</pre>
</div>
<!-- Step 5 -->
<div class="section" id="step5">
<h2>🔗 گام ۶: ثبت وابستگی‌ها (Dependency Injection)</h2>
<p>اطمینان حاصل کنید که <code>IXFileProvider</code> در کانتینر DI ثبت شده است (که بر اساس فایل‌های ارائه شده، قبلاً در <code>xFileService.DI.XDIHelperExtension</code> انجام شده است). فقط باید اطمینان حاصل کنیم که در <code>xAiApi</code> قابل تزریق است.</p>
<div class="file-change">
<span class="badge-modify">MODIFY</span>
<span class="path">xAiApi/Startup.cs</span>
</div>
<pre>public void ConfigureServices(IServiceCollection services)
{
// ... (سایر ثبت‌ها)
// ✅ اطمینان از ثبت سرویس فایل (اگر قبلاً در ماژول xFileService ثبت نشده، اینجا فراخوانی شود)
// services.AddXFileService&lt;XAiApiDbContext&gt;(xDataService.Constants.XRepositoryType.EF);
// ✅ به‌روزرسانی ثبت سرویس‌های AI برای تزریق IXFileProvider
// نکته: چون XAIServiceBase کلاس پایه است، باید در کلاس‌های مشتق شده (مثل XDefaultAiService) تزریق شود.
// مثال برای XDefaultAiService:
// services.AddScoped&lt;IXDefaultAiService&gt;(sp =&gt; new XDefaultAiService(
// sp.GetRequiredService&lt;IXAiDataProvider&gt;(),
// sp.GetRequiredService&lt;ILogger&lt;XDefaultAiService&gt;&gt;(),
// sp.GetRequiredService&lt;XAiApiConfiguration&gt;(),
// sp.GetRequiredService&lt;XValidationProvider&gt;(),
// sp.GetRequiredService&lt;IXFileProvider&gt;() // ✅ تزریق جدید
// ));
}</pre>
<div class="alert alert-warning">
<strong>⚠️ توجه:</strong> اگر <code>XAiServiceBase</code> را مستقیماً ثبت نمی‌کنید و از کلاس‌های مشتق شده استفاده می‌کنید، باید Constructor آن کلاس‌ها را نیز برای پذیرش <code>IXFileProvider</code> و پاس دادن آن به <code>base(...)</code> به‌روزرسانی کنید.
</div>
</div>
<!-- Flow -->
<div class="section" id="flow">
<h2>🔄 گام ۷: جریان کامل پردازش</h2>
<div class="flow-diagram">
<span class="flow-step">📤 کلاینت: ارسال Form (Prompt + Files)</span>
<span class="flow-arrow">→</span>
<span class="flow-step">🎮 Controller: فراخوانی IXFileProvider.Upload</span>
<span class="flow-arrow">→</span>
<span class="flow-step">💾 xFileService: ذخیره فیزیکی و ثبت در DB</span>
<span class="flow-arrow">→</span>
<span class="flow-step">⚙️ XAiServiceBase: دریافت List&lt;XFileDto&gt;</span>
<span class="flow-arrow">→</span>
<span class="flow-step">🔍 XAiServiceBase: فراخوانی GetFileDescriptor</span>
<span class="flow-arrow">→</span>
<span class="flow-step">📝 تبدیل Stream به TextContent/DataContent</span>
<span class="flow-arrow">→</span>
<span class="flow-step">🤖 ارسال ChatMessage (Text + Files) به LLM</span>
<span class="flow-arrow">→</span>
<span class="flow-step">💾 ذخیره Message با FileIds در MetaDatas</span>
</div>
</div>
<!-- Summary -->
<div class="section" id="summary">
<h2>📋 گام ۸: خلاصه تغییرات</h2>
<table>
<tr>
<th>ردیف</th>
<th>فایل / ماژول</th>
<th>نوع تغییر</th>
<th>توضیح</th>
</tr>
<tr>
<td>۱</td>
<td><code>xAiApi/Controllers/XAiServiceControllerBase.cs</code></td>
<td><span class="badge-modify">MODIFY</span></td>
<td>افزودن <code>IXFileProvider</code> و فراخوانی <code>Upload</code> قبل از سرویس AI</td>
</tr>
<tr>
<td>۲</td>
<td><code>xAiApi/Interfaces/IXAiServiceBase.cs</code></td>
<td><span class="badge-modify">MODIFY</span></td>
<td>تغییر پارامتر <code>files</code> از <code>IFormFileCollection</code> به <code>IEnumerable&lt;XFileDto&gt;</code></td>
</tr>
<tr>
<td>۳</td>
<td><code>xAiApi/Providers/XAIServiceBase.cs</code></td>
<td><span class="badge-modify">MODIFY</span></td>
<td>تزریق <code>IXFileProvider</code> و افزودن متد <code>ProcessAttachedFilesAsync</code></td>
</tr>
<tr>
<td>۴</td>
<td><code>xAiModels/Extensions/XAiModelsExtensions.cs</code></td>
<td><span class="badge-modify">MODIFY</span></td>
<td>پشتیبانی <code>ToChatMessages</code> از <code>additionalContents</code></td>
</tr>
<tr>
<td>۵</td>
<td><code>xAiApi/Providers/XDefaultAiService.cs</code> (و سایر مشتق‌ها)</td>
<td><span class="badge-modify">MODIFY</span></td>
<td>به‌روزرسانی Constructor برای پاس دادن <code>IXFileProvider</code> به کلاس پایه</td>
</tr>
</table>
<div class="alert alert-success">
<strong>✅ دستاوردهای این طراحی:</strong>
<ul style="padding-right: 25px; margin-top: 10px;">
<li>🛡️ <strong>جداسازی مسئولیت‌ها:</strong> ماژول AI دیگر درگیر آپلود یا مدیریت فایل فیزیکی نیست.</li>
<li>♻️ <strong>استفاده مجدد:</strong> از تمام قابلیت‌های <code>xFileService</code> (مانند Thumbnail، References، و Storage) بهره می‌بریم.</li>
<li>🔗 <strong>ردیابی‌پذیری:</strong> با ذخیره <code>FileId</code> در <code>MetaDatas</code>، همیشه می‌توانیم بفهمیم کدام فایل‌ها به کدام پیام متصل بوده‌اند.</li>
<li>🎨 <strong>پشتیبانی چندوجهی (Multi-modal):</strong> آماده‌سازی برای ارسال تصاویر به صورت <code>DataContent</code> به مدل‌های Vision.</li>
</ul>
</div>
</div>
</div>
<div class="footer">
<p><strong>👨‍💻 توسعه‌دهنده:</strong> هادی خزاعی اصل</p>
<p><strong>🏢 شرکت:</strong> فن آوران ساحر علم</p>
<p><strong>📅 تاریخ تهیه مستند:</strong> چهارشنبه ۸ مهر ۱۴۰۵</p>
<p style="margin-top: 15px; opacity: 0.8; font-size: 0.9em;">
📎 پیاده‌سازی استخراج محتوا با ماژول xFileService - تمامی حقوق محفوظ است
</p>
</div>
</div>
</body>
</html>