diff --git a/telegram-agy-bot/agy_engine.py b/telegram-agy-bot/agy_engine.py index a8a0a44..21531d7 100644 --- a/telegram-agy-bot/agy_engine.py +++ b/telegram-agy-bot/agy_engine.py @@ -123,6 +123,13 @@ class Project: created_at: float = field(default_factory=time.time) description: str = "" conversation_titles: Dict[str, str] = field(default_factory=dict) + active_branch: str = "dev" + ftp_host: Optional[str] = None + ftp_port: int = 21 + ftp_user: Optional[str] = None + ftp_password: Optional[str] = None + ftp_path: str = "/" + ftp_tls: bool = False @dataclass class Session: @@ -258,6 +265,13 @@ class SessionManager: "created_at", "description", "conversation_titles", + "active_branch", + "ftp_host", + "ftp_port", + "ftp_user", + "ftp_password", + "ftp_path", + "ftp_tls", ) } if "name" not in clean_p: diff --git a/telegram-agy-bot/bot.py b/telegram-agy-bot/bot.py index 2f545d8..d58d26a 100644 --- a/telegram-agy-bot/bot.py +++ b/telegram-agy-bot/bot.py @@ -49,6 +49,7 @@ from formatters import ( format_git_info, format_git_commits_page, format_commit_detail_view, + format_ftp_info, split_message, escape_html, ) @@ -67,6 +68,7 @@ from invite_manager import ( ) from backup_manager import backup_manager from git_manager import git_manager +from ftp_manager import ftp_manager from sys_monitor import render_server_hardware_report from usage_monitor import fetch_and_render_usage_report from bot_actions import process_all_ai_actions @@ -319,6 +321,10 @@ async def build_git_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]: [ InlineKeyboardButton("🌐 مشاهده در مرورگر (Gitea)", url=urls["web_url"]), ], + [ + InlineKeyboardButton("🚀 انتشار به پروداکشن (Publish)", callback_data="btn_git_publish"), + InlineKeyboardButton("🚀 دیپلوی FTP", callback_data="btn_ftp_menu"), + ], [ InlineKeyboardButton("📜 تاریخچه ۲۰ کامیت اخیر", callback_data="git_hist:1"), InlineKeyboardButton("↩️ لغو آخرین تغییر (Undo)", callback_data="git_ask_undo_head"), @@ -341,6 +347,10 @@ async def build_git_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]: [ InlineKeyboardButton("🌐 Open in Browser (Gitea)", url=urls["web_url"]), ], + [ + InlineKeyboardButton("🚀 Publish to Production", callback_data="btn_git_publish"), + InlineKeyboardButton("🚀 Deploy via FTP", callback_data="btn_ftp_menu"), + ], [ InlineKeyboardButton("📜 Recent Commits (History)", callback_data="git_hist:1"), InlineKeyboardButton("↩️ Undo Last Change", callback_data="git_ask_undo_head"), @@ -362,6 +372,59 @@ async def build_git_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]: return text, InlineKeyboardMarkup(keyboard) +async def build_ftp_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]: + """Generates the interactive FTP Deployer and configuration view.""" + session = session_manager.get_or_create(chat_id) + curr_proj = session_manager.get_current_project(chat_id) + is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)") + + if not curr_proj: + msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found." + return msg, InlineKeyboardMarkup([[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")]]) + + text = format_ftp_info( + project_name=curr_proj.name, + host=curr_proj.ftp_host, + port=curr_proj.ftp_port, + user=curr_proj.ftp_user, + path=curr_proj.ftp_path, + tls=curr_proj.ftp_tls, + lang=session.language, + ) + + if is_fa: + keyboard = [ + [ + InlineKeyboardButton("🚀 دیپلوی روی FTP (Clean Deploy)", callback_data="btn_ftp_deploy"), + InlineKeyboardButton("🔍 تست اتصال FTP", callback_data="btn_ftp_test"), + ], + [ + InlineKeyboardButton("🐙 منوی گیت و برنچ‌ها", callback_data="btn_git_menu"), + InlineKeyboardButton("📁 مدیریت پروژه‌ها", callback_data="proj_menu"), + ], + [ + InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"), + ], + ] + else: + keyboard = [ + [ + InlineKeyboardButton("🚀 Deploy via FTP (Clean)", callback_data="btn_ftp_deploy"), + InlineKeyboardButton("🔍 Test FTP Connection", callback_data="btn_ftp_test"), + ], + [ + InlineKeyboardButton("🐙 Git & Branches Menu", callback_data="btn_git_menu"), + InlineKeyboardButton("📁 Projects Menu", callback_data="proj_menu"), + ], + [ + InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"), + ], + ] + + return text, InlineKeyboardMarkup(keyboard) + + + async def build_git_history_menu(chat_id: int, page: int = 1) -> tuple[str, InlineKeyboardMarkup]: """Generates paginated commit history view.""" session = session_manager.get_or_create(chat_id) @@ -3360,6 +3423,55 @@ async def git_command(update: Update, context: ContextTypes.DEFAULT_TYPE): text, markup = await build_git_menu(chat_id) await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True) +# Command: /publish +@check_auth +async def publish_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + chat_id = update.effective_chat.id + session = session_manager.get_or_create(chat_id) + curr_proj = session_manager.get_current_project(chat_id) + is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)") + + if not curr_proj: + msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found." + await update.message.reply_html(msg) + return + + pub_msg = " ".join(context.args).strip() if context.args else f"Manual release published via /publish" + status_msg = await update.message.reply_html("🚀 در حال انتشار تغییرات شاخه dev به شاخه production..." if is_fa else "🚀 Publishing dev branch to production...") + ok, res_msg, p_info = await git_manager.git_publish(curr_proj.workspace, message=pub_msg, repo_name=curr_proj.name) + urls = git_manager.get_repo_urls(curr_proj.name) + + buttons = [ + [ + InlineKeyboardButton("🚀 دیپلوی روی FTP" if is_fa else "🚀 Deploy to FTP", callback_data="btn_ftp_deploy"), + InlineKeyboardButton("🐙 منوی گیت" if is_fa else "🐙 Git Menu", callback_data="btn_git_menu"), + ] + ] + + if ok: + out = ( + f"🚀 عملیات انتشار به پروداکشن (Publish to Production) با موفقیت انجام شد:\n\n" + f"• 📁 پروژه: {escape_html(curr_proj.name)}\n" + f"• 🌐 مخزن: {urls['web_url']}\n\n" + f"{res_msg}" + ) if is_fa else ( + f"🚀 Published to Production Successfully:\n\n" + f"• 📁 Project: {escape_html(curr_proj.name)}\n" + f"• 🌐 Repository: {urls['web_url']}\n\n" + f"{res_msg}" + ) + else: + out = f"⚠️ خطا در انتشار:\n{res_msg}" + + await status_msg.edit_text(out, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(buttons), disable_web_page_preview=True) + +# Command: /ftp +@check_auth +async def ftp_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + chat_id = update.effective_chat.id + text, markup = await build_ftp_menu(chat_id) + await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True) + # Command: /sync @check_auth async def sync_command(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -4574,6 +4686,96 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): except Exception: await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True) + elif data == "btn_git_publish": + await query.answer("🚀 در حال انتشار به پروداکشن..." if is_fa else "🚀 Publishing to production...") + if curr_proj: + ok, res_msg, _ = await git_manager.git_publish(curr_proj.workspace, message="Publish via bot button", repo_name=curr_proj.name) + try: + await query.answer("✅ انتشار به پروداکشن با موفقیت انجام شد!" if ok else "⚠️ خطا در انتشار", show_alert=not ok) + except Exception: + pass + text, markup = await build_git_menu(chat_id) + try: + await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True) + except Exception: + await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True) + + elif data == "btn_ftp_menu": + text, markup = await build_ftp_menu(chat_id) + try: + await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True) + except Exception: + await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True) + + elif data == "btn_ftp_test": + await query.answer("🔍 در حال بررسی اتصال FTP..." if is_fa else "🔍 Testing FTP connection...") + if curr_proj and curr_proj.ftp_host: + test_ok, test_msg = await ftp_manager.test_connection( + host=curr_proj.ftp_host, + port=curr_proj.ftp_port, + user=curr_proj.ftp_user or "", + password=curr_proj.ftp_password or "", + path=curr_proj.ftp_path or "/", + tls=curr_proj.ftp_tls, + ) + try: + alert_text = ("✅ اتصال FTP برقرار است" if test_ok else "❌ خطا در اتصال FTP") + f"\n{test_msg}" + await query.answer(alert_text[:200], show_alert=True) + except Exception: + pass + else: + try: + await query.answer("⚠️ اطلاعات FTP هنوز برای این پروژه تنظیم نشده است.", show_alert=True) + except Exception: + pass + text, markup = await build_ftp_menu(chat_id) + try: + await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True) + except Exception: + await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True) + + elif data == "btn_ftp_deploy": + await query.answer("🚀 در حال استقرار هوشمند روی FTP..." if is_fa else "🚀 Deploying to FTP...") + if curr_proj and curr_proj.ftp_host: + # Auto publish before deploy + try: + await git_manager.git_publish(curr_proj.workspace, message="Auto-publish before UI FTP deploy", repo_name=curr_proj.name) + except Exception: + pass + + res = await ftp_manager.deploy_project( + workspace_path=curr_proj.workspace, + host=curr_proj.ftp_host, + port=curr_proj.ftp_port, + user=curr_proj.ftp_user or "", + password=curr_proj.ftp_password or "", + remote_path=curr_proj.ftp_path or "/", + tls=curr_proj.ftp_tls, + ) + if res.get("success"): + files_up = res.get("files_uploaded", 0) + dur = res.get("duration", 0) + try: + await query.answer(f"✅ دیپلوی با موفقیت انجام شد ({files_up} فایل در {dur} ثانیه)", show_alert=True) + except Exception: + pass + else: + err = res.get("error", "Error") + try: + await query.answer(f"❌ خطا در دیپلوی: {err}"[:200], show_alert=True) + except Exception: + pass + else: + try: + await query.answer("⚠️ اطلاعات FTP تنظیم نشده است. ابتدا اطلاعات FTP را ثبت کنید.", show_alert=True) + except Exception: + pass + text, markup = await build_ftp_menu(chat_id) + try: + await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True) + except Exception: + await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True) + elif data == "btn_git_sync": await query.answer("🔄 در حال همگام‌سازی با Gitea..." if is_fa else "🔄 Syncing with Gitea...") if curr_proj: @@ -7034,6 +7236,8 @@ def main(): # Git & Gitea Version Control Handlers application.add_handler(CommandHandler(["git", "repo", "repository", "gitea"], git_command)) + application.add_handler(CommandHandler(["publish", "gitpublish", "release"], publish_command)) + application.add_handler(CommandHandler(["ftp", "ftpdeploy", "deploy"], ftp_command)) application.add_handler(CommandHandler(["sync", "gitsync", "pull"], sync_command)) application.add_handler(CommandHandler(["commit", "gitcommit"], commit_command)) application.add_handler(CommandHandler(["undo", "revert", "gitundo", "gitrevert", "laghv"], undo_command)) diff --git a/telegram-agy-bot/bot_actions.py b/telegram-agy-bot/bot_actions.py index 404c09c..9f70aad 100644 --- a/telegram-agy-bot/bot_actions.py +++ b/telegram-agy-bot/bot_actions.py @@ -1417,7 +1417,213 @@ async def execute_action( return badge, side_effects, created_task # ------------------------------------------------------------- - # 31. SAVE_MEMORY / STORE_MEMORY / SET_MEMORY + # 31. GIT_PUBLISH / PUBLISH_PROJECT / PUBLISH + # ------------------------------------------------------------- + elif act in ("GIT_PUBLISH", "PUBLISH_PROJECT", "PUBLISH", "MERGE_TO_PROD"): + p_name = attrs.get("project") or attrs.get("name") or current_project_name + proj = session.projects.get(p_name) if p_name else session_manager.get_current_project(chat_id) + if not proj: + return f"\n⚠️ پروژه {escape_html(p_name or '')} یافت نشد." if is_fa else f"\n⚠️ Project {escape_html(p_name or '')} not found.", side_effects, created_task + + from git_manager import git_manager + pub_msg = attrs.get("message") or attrs.get("_default") or f"Release published to production" + ok, res_msg, p_info = await git_manager.git_publish(proj.workspace, message=pub_msg, repo_name=proj.name) + urls = git_manager.get_repo_urls(proj.name) + + if ok: + badge = ( + f"\n\n🚀 عملیات انتشار به پروداکشن (Publish to Production) با موفقیت انجام شد:\n" + f"• 📁 پروژه: {escape_html(proj.name)}\n" + f"• 🌐 مخزن تحت وب: {urls['web_url']}\n" + f"• 📋 {res_msg}" + if is_fa else + f"\n\n🚀 Published to Production Successfully:\n" + f"• 📁 Project: {escape_html(proj.name)}\n" + f"• 🌐 Web Repo: {urls['web_url']}\n" + f"• 📋 {res_msg}" + ) + else: + badge = f"\n⚠️ خطا در انتشار به پروداکشن: {res_msg}" + return badge, side_effects, created_task + + # ------------------------------------------------------------- + # 32. FTP_CONFIG / SET_FTP / CONFIGURE_FTP + # ------------------------------------------------------------- + elif act in ("FTP_CONFIG", "SET_FTP", "CONFIGURE_FTP", "FTP_SETTINGS"): + p_name = attrs.get("project") or attrs.get("name") or current_project_name + proj = session.projects.get(p_name) if p_name else session_manager.get_current_project(chat_id) + if not proj: + return f"\n⚠️ پروژه {escape_html(p_name or '')} یافت نشد." if is_fa else f"\n⚠️ Project {escape_html(p_name or '')} not found.", side_effects, created_task + + host = attrs.get("host") or attrs.get("server") or attrs.get("ftp_host") + port_raw = attrs.get("port") or attrs.get("ftp_port") or "21" + user = attrs.get("user") or attrs.get("username") or attrs.get("ftp_user") or "" + password = attrs.get("password") or attrs.get("pass") or attrs.get("ftp_password") or "" + path = attrs.get("path") or attrs.get("dir") or attrs.get("remote_path") or attrs.get("ftp_path") or "/" + tls_raw = attrs.get("tls") or attrs.get("ssl") or attrs.get("ftps") or "false" + tls = str(tls_raw).lower() in ("true", "1", "yes", "on") + + try: + port = int(port_raw) + except Exception: + port = 21 + + if host: + proj.ftp_host = host.strip() + proj.ftp_port = port + proj.ftp_user = user.strip() + proj.ftp_password = password.strip() if password else proj.ftp_password + proj.ftp_path = path.strip() + proj.ftp_tls = tls + session_manager.save() + + # Test connection + from ftp_manager import ftp_manager + test_ok, test_msg = await ftp_manager.test_connection( + host=proj.ftp_host, + port=proj.ftp_port, + user=proj.ftp_user, + password=proj.ftp_password or "", + path=proj.ftp_path, + tls=proj.ftp_tls, + ) + + status_icon = "✅" if test_ok else "⚠️" + badge = ( + f"\n\n⚙️ تنظیمات اتصال FTP برای پروژه {escape_html(proj.name)} ذخیره شد:\n" + f"• 🌐 هاست: {escape_html(proj.ftp_host)}:{proj.ftp_port}\n" + f"• 👤 نام کاربری: {escape_html(proj.ftp_user or '(بدون نام کاربری)')}\n" + f"• 📂 مسیر ریموت: {escape_html(proj.ftp_path)}\n" + f"• 🔒 پروتکل امن (FTPS/TLS): {'بله' if proj.ftp_tls else 'خیر'}\n" + f"• {status_icon} نتیجه بررسی اتصال: {test_msg}" + if is_fa else + f"\n\n⚙️ FTP Configuration saved for project {escape_html(proj.name)}:\n" + f"• 🌐 Host: {escape_html(proj.ftp_host)}:{proj.ftp_port}\n" + f"• 👤 User: {escape_html(proj.ftp_user or '(none)')}\n" + f"• 📂 Remote Path: {escape_html(proj.ftp_path)}\n" + f"• 🔒 FTPS/TLS: {'Yes' if proj.ftp_tls else 'No'}\n" + f"• {status_icon} Connection test: {test_msg}" + ) + else: + # Display current config + curr_host = proj.ftp_host or "(تنظیم نشده)" + badge = ( + f"\n\nℹ️ وضعیت تنظیمات FTP پروژه {escape_html(proj.name)}:\n" + f"• 🌐 هاست: {escape_html(curr_host)}:{proj.ftp_port}\n" + f"• 👤 کاربر: {escape_html(proj.ftp_user or '(ندارد)')}\n" + f"• 📂 مسیر: {escape_html(proj.ftp_path or '/')}" + if is_fa else + f"\n\nℹ️ FTP Status for {escape_html(proj.name)}:\n" + f"• 🌐 Host: {escape_html(curr_host)}:{proj.ftp_port}\n" + f"• 👤 User: {escape_html(proj.ftp_user or '(none)')}\n" + f"• 📂 Path: {escape_html(proj.ftp_path or '/')}" + ) + return badge, side_effects, created_task + + # ------------------------------------------------------------- + # 33. FTP_TEST / TEST_FTP + # ------------------------------------------------------------- + elif act in ("FTP_TEST", "TEST_FTP", "CHECK_FTP"): + p_name = attrs.get("project") or attrs.get("name") or current_project_name + proj = session.projects.get(p_name) if p_name else session_manager.get_current_project(chat_id) + if not proj: + return f"\n⚠️ پروژه {escape_html(p_name or '')} یافت نشد." if is_fa else f"\n⚠️ Project {escape_html(p_name or '')} not found.", side_effects, created_task + + if not proj.ftp_host: + return "\n⚠️ مشخصات FTP برای این پروژه هنوز تنظیم نشده است. می‌توانید با تگ [[FTP_CONFIG: host=\"...\", user=\"...\", pass=\"...\"]] آن را تنظیم کنید." if is_fa else "\n⚠️ FTP credentials are not set for this project.", side_effects, created_task + + from ftp_manager import ftp_manager + test_ok, test_msg = await ftp_manager.test_connection( + host=proj.ftp_host, + port=proj.ftp_port, + user=proj.ftp_user or "", + password=proj.ftp_password or "", + path=proj.ftp_path or "/", + tls=proj.ftp_tls, + ) + + icon = "✅" if test_ok else "❌" + badge = ( + f"\n\n{icon} تست اتصال FTP پروژه {escape_html(proj.name)}:\n" + f"• 🌐 سرور: {escape_html(proj.ftp_host)}:{proj.ftp_port}\n" + f"• 📂 مسیر: {escape_html(proj.ftp_path or '/')}\n" + f"• 📋 وضعیت: {test_msg}" + if is_fa else + f"\n\n{icon} FTP Test Result for {escape_html(proj.name)}:\n" + f"• 🌐 Server: {escape_html(proj.ftp_host)}:{proj.ftp_port}\n" + f"• 📂 Path: {escape_html(proj.ftp_path or '/')}\n" + f"• 📋 Status: {test_msg}" + ) + return badge, side_effects, created_task + + # ------------------------------------------------------------- + # 34. FTP_DEPLOY / DEPLOY_FTP / DEPLOY_PROJECT + # ------------------------------------------------------------- + elif act in ("FTP_DEPLOY", "DEPLOY_FTP", "DEPLOY_PROJECT", "FTP_PUBLISH"): + p_name = attrs.get("project") or attrs.get("name") or current_project_name + proj = session.projects.get(p_name) if p_name else session_manager.get_current_project(chat_id) + if not proj: + return f"\n⚠️ پروژه {escape_html(p_name or '')} یافت نشد." if is_fa else f"\n⚠️ Project {escape_html(p_name or '')} not found.", side_effects, created_task + + if not proj.ftp_host: + return "\n⚠️ مشخصات اتصال FTP برای این پروژه یافت نشد. لطفاً ابتدا با تگ [[FTP_CONFIG: ...]] اطلاعات FTP را ثبت کنید." if is_fa else "\n⚠️ FTP configuration not found for this project.", side_effects, created_task + + from ftp_manager import ftp_manager + dry_raw = attrs.get("dry_run") or attrs.get("test") or "false" + dry_run = str(dry_raw).lower() in ("true", "1", "yes") + + # Auto publish dev -> production before deployment if branch="production" + req_branch = attrs.get("branch") or "production" + if req_branch == "production": + from git_manager import git_manager + try: + await git_manager.git_publish(proj.workspace, message="Auto-publish before FTP deployment", repo_name=proj.name) + except Exception as pe: + logger.warning(f"Auto-publish warning before FTP deploy: {pe}") + + res = await ftp_manager.deploy_project( + workspace_path=proj.workspace, + host=proj.ftp_host, + port=proj.ftp_port, + user=proj.ftp_user or "", + password=proj.ftp_password or "", + remote_path=proj.ftp_path or "/", + tls=proj.ftp_tls, + dry_run=dry_run, + ) + + if res.get("success"): + kb = round(res.get("bytes_transferred", 0) / 1024, 2) + mb = round(kb / 1024, 2) + size_str = f"{mb} مگابایت" if mb >= 1.0 else f"{kb} کیلوبایت" + files_up = res.get("files_uploaded", res.get("files_to_upload", 0)) + files_skip = res.get("files_skipped", 0) + duration = res.get("duration", 0) + target_p = res.get("remote_path", "/") + + badge = ( + f"\n\n🚀 دیپلوی هوشمند FTP با موفقیت انجام شد:\n" + f"• 📁 پروژه: {escape_html(proj.name)}\n" + f"• 🌐 مقصد FTP: {escape_html(proj.ftp_host)}:{proj.ftp_port} (مسیر: {escape_html(target_p)})\n" + f"• 📤 فایل‌های آپلودشده: {files_up} فایل ({size_str})\n" + f"• 🛡️ فایل‌های زائد/محرمانه نادیده گرفته‌شده: {files_skip} آیتم (Clean Deployment)\n" + f"• ⏱️ مدت زمان انتقال: {duration} ثانیه\n" + f"• ✨ وضعیت: عملیاتی و آماده در سرور مقصد" + if is_fa else + f"\n\n🚀 Smart Clean FTP Deployment Succeeded:\n" + f"• 📁 Project: {escape_html(proj.name)}\n" + f"• 🌐 FTP Target: {escape_html(proj.ftp_host)}:{proj.ftp_port} (Path: {escape_html(target_p)})\n" + f"• 📤 Files Uploaded: {files_up} ({size_str})\n" + f"• 🛡️ Excluded Junk/Secrets: {files_skip} items (Clean Deployment)\n" + f"• ⏱️ Duration: {duration}s" + ) + else: + err = res.get("error", "Unknown error") + badge = f"\n⚠️ خطا در استقرار FTP پروژه: {escape_html(str(err))}" if is_fa else f"\n⚠️ FTP Deployment Failed: {escape_html(str(err))}" + return badge, side_effects, created_task + + # ------------------------------------------------------------- + # 35. SAVE_MEMORY / STORE_MEMORY / SET_MEMORY # ------------------------------------------------------------- elif act in ("SAVE_MEMORY", "STORE_MEMORY", "SET_MEMORY", "ADD_MEMORY"): from memory_manager import memory_manager diff --git a/telegram-agy-bot/formatters.py b/telegram-agy-bot/formatters.py index 14873df..2086adf 100644 --- a/telegram-agy-bot/formatters.py +++ b/telegram-agy-bot/formatters.py @@ -615,3 +615,44 @@ def format_commit_detail_view( ) +def format_ftp_info( + project_name: str, + host: Optional[str], + port: int = 21, + user: Optional[str] = None, + path: str = "/", + tls: bool = False, + lang: str = "fa", +) -> str: + """Formats FTP connection and deployment status for Telegram HTML.""" + is_fa = (lang or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)") + clean_p = escape_html(project_name) + host_str = f"{escape_html(host)}:{port}" if host else ("(تنظیم نشده)" if is_fa else "(Not configured)") + user_str = f"{escape_html(user)}" if user else ("(ندارد)" if is_fa else "(None)") + path_str = f"{escape_html(path)}" + tls_str = "بله (FTPS/TLS امن) 🔒" if tls else "خیر (FTP معمولی)" + tls_str_en = "Yes (Secure FTPS/TLS) 🔒" if tls else "No (Standard FTP)" + + if is_fa: + return ( + f"🚀 تنظیمات استقرار و دیپلوی FTP پروژه\n\n" + f"• 📁 پروژه: {clean_p}\n" + f"• 🌐 سرور / هاست FTP: {host_str}\n" + f"• 👤 نام کاربری: {user_str}\n" + f"• 📂 مسیر مقصد روی هاست: {path_str}\n" + f"• 🔒 پروتکل امن (FTPS): {tls_str}\n\n" + f"💡 در زمان دیپلوی، هوش مصنوعی فایل‌های زائد، کش، سشن‌ها و مخزن گیت (.git) را به صورت خودکار فیلتر کرده و تنها سورس اصلی و تمیز به سرور منتقل می‌شود." + ) + else: + return ( + f"🚀 Project FTP Deployment Settings\n\n" + f"• 📁 Project: {clean_p}\n" + f"• 🌐 FTP Host: {host_str}\n" + f"• 👤 Username: {user_str}\n" + f"• 📂 Remote Target Path: {path_str}\n" + f"• 🔒 Secure FTPS/TLS: {tls_str_en}\n\n" + f"💡 During deployment, unnecessary cache, temp, sessions, and .git files are automatically excluded for a clean production upload." + ) + + + diff --git a/telegram-agy-bot/ftp_manager.py b/telegram-agy-bot/ftp_manager.py new file mode 100644 index 0000000..0d53e46 --- /dev/null +++ b/telegram-agy-bot/ftp_manager.py @@ -0,0 +1,316 @@ +import os +import sys +import time +import fnmatch +import logging +import asyncio +from pathlib import Path +from typing import Optional, Dict, Any, List, Tuple, Set +import ftplib +import socket + +logger = logging.getLogger("AGYFTPManager") + +# Standard patterns for junk, temporary, confidential, and unnecessary files that must NEVER be sent to production FTP +DEFAULT_FTP_EXCLUDES = [ + # Git and version control + ".git", + ".git/*", + ".gitignore", + ".gitattributes", + ".gitmodules", + + # Environment and secrets (local secrets must not leak to production without explicit setup) + ".env", + ".env.*", + "*.env", + "*.pem", + "*.key", + + # AI & Bot internal session / agent configs + ".agents", + ".agents/*", + ".gemini", + ".gemini/*", + "sessions_data", + "sessions_data/*", + "uploads_temp", + "uploads_temp/*", + "*.db-shm", + "*.db-wal", + + # Python cache & build + "__pycache__", + "__pycache__/*", + "*.pyc", + "*.pyo", + "*.pyd", + ".venv", + ".venv/*", + "venv", + "venv/*", + "env", + "env/*", + + # Logs & temp files + "*.log", + "tmp", + "tmp/*", + "temp", + "temp/*", + + # IDE & OS clutter + ".DS_Store", + "Thumbs.db", + "desktop.ini", + ".idea", + ".idea/*", + ".vscode", + ".vscode/*", + "*.swp", + "*.swo", + "*~", +] + + +def should_exclude(rel_path: str, custom_excludes: Optional[List[str]] = None) -> bool: + """Checks if a relative path matches any exclusion pattern.""" + excludes = DEFAULT_FTP_EXCLUDES + (custom_excludes or []) + norm_path = rel_path.replace("\\", "/").strip("/") + parts = norm_path.split("/") + + for pattern in excludes: + pat = pattern.replace("\\", "/").strip("/") + # Check against full relative path + if fnmatch.fnmatch(norm_path, pat): + return True + # Check against individual directory / file parts + for part in parts: + if fnmatch.fnmatch(part, pat): + return True + # If pattern is a directory (e.g. .git/* or .git), match if path starts with it + clean_pat = pat.rstrip("/*") + if norm_path == clean_pat or norm_path.startswith(clean_pat + "/"): + return True + + return False + + +class FTPManager: + """Manages FTP connections, testing, and clean intelligent deployments.""" + + def _create_ftp_client( + self, + host: str, + port: int = 21, + user: str = "", + password: str = "", + tls: bool = False, + timeout: int = 15, + ) -> ftplib.FTP: + """Helper to create and authenticate an FTP or FTPS client.""" + if tls: + ftp = ftplib.FTP_TLS(timeout=timeout) + else: + ftp = ftplib.FTP(timeout=timeout) + + ftp.connect(host=host, port=port, timeout=timeout) + ftp.login(user=user or "anonymous", passwd=password or "") + + if tls and isinstance(ftp, ftplib.FTP_TLS): + ftp.prot_p() # Secure data connection + + return ftp + + async def test_connection( + self, + host: str, + port: int = 21, + user: str = "", + password: str = "", + path: str = "/", + tls: bool = False, + timeout: int = 10, + ) -> Tuple[bool, str]: + """Tests FTP credentials and verifies access to the target remote directory.""" + if not host or not host.strip(): + return False, "آدرس هاست (Host) FTP مشخص نشده است." + + def _test(): + try: + ftp = self._create_ftp_client(host.strip(), int(port), user.strip(), password, tls, timeout) + # Test CWD to target path if specified + target_path = path.strip() if path else "/" + if target_path and target_path != "/": + try: + ftp.cwd(target_path) + except Exception as e: + pwd = ftp.pwd() + ftp.quit() + return False, f"اتصال به FTP برقرار شد، اما مسیر ریموت «{target_path}» یافت نشد (مسیر فعلی: {pwd}): {e}" + + pwd = ftp.pwd() + try: + listing = ftp.nlst() + count = len(listing) + except Exception: + count = 0 + ftp.quit() + return True, f"اتصال با موفقیت برقرار شد. مسیر فعلی: {pwd} (تعداد آیتم‌ها: {count})" + except (socket.gaierror, socket.timeout) as e: + return False, f"خطای شبکه / نامعتبر بودن آدرس سرور ({host}:{port}): {e}" + except ftplib.error_perm as e: + return False, f"خطای احراز هویت / دسترسی FTP: {e}" + except Exception as e: + return False, f"خطا در برقراری ارتباط با FTP: {e}" + + return await asyncio.to_thread(_test) + + async def deploy_project( + self, + workspace_path: str, + host: str, + port: int = 21, + user: str = "", + password: str = "", + remote_path: str = "/", + tls: bool = False, + custom_excludes: Optional[List[str]] = None, + dry_run: bool = False, + ) -> Dict[str, Any]: + """ + Deploys files from workspace_path to the remote FTP server cleanly. + Skips all temporary, git, session, and junk files. + """ + ws = Path(workspace_path).expanduser().resolve() + if not ws.exists() or not ws.is_dir(): + return { + "success": False, + "error": f"دایرکتوری پروژه در مسیر {workspace_path} یافت نشد.", + "files_uploaded": 0, + "files_skipped": 0, + "bytes_transferred": 0, + "duration": 0.0, + "uploaded_list": [], + } + + def _run_deploy(): + start_time = time.time() + uploaded_files: List[str] = [] + skipped_files: List[str] = [] + total_bytes = 0 + + try: + ftp = self._create_ftp_client(host.strip(), int(port), user.strip(), password, tls, timeout=20) + + # Navigate or create remote root path + target_root = (remote_path or "/").strip().replace("\\", "/") + if not target_root.startswith("/"): + target_root = "/" + target_root + target_root = target_root.rstrip("/") + if not target_root: + target_root = "/" + + def ensure_remote_dir(r_dir: str): + if r_dir in ("/", ""): + ftp.cwd("/") + return + parts = [p for p in r_dir.split("/") if p] + curr = "" + for p in parts: + curr += "/" + p + try: + ftp.cwd(curr) + except Exception: + try: + ftp.mkd(curr) + ftp.cwd(curr) + except Exception: + pass + + ensure_remote_dir(target_root) + + # Scan workspace + all_files_to_upload: List[Tuple[Path, str]] = [] # (local_path, rel_path) + + for root, dirs, files in os.walk(str(ws)): + rel_dir = os.path.relpath(root, str(ws)) + if rel_dir == ".": + rel_dir = "" + + # Filter out directories in-place to avoid descending into ignored dirs + dirs_to_keep = [] + for d in dirs: + dir_rel = f"{rel_dir}/{d}".strip("/") + if should_exclude(dir_rel, custom_excludes): + skipped_files.append(dir_rel + "/") + else: + dirs_to_keep.append(d) + dirs[:] = dirs_to_keep + + for f in files: + file_rel = f"{rel_dir}/{f}".strip("/") + if should_exclude(file_rel, custom_excludes): + skipped_files.append(file_rel) + else: + all_files_to_upload.append((Path(root) / f, file_rel)) + + if dry_run: + ftp.quit() + return { + "success": True, + "dry_run": True, + "files_to_upload": len(all_files_to_upload), + "files_skipped": len(skipped_files), + "bytes_transferred": sum(p.stat().st_size for p, _ in all_files_to_upload if p.exists()), + "duration": round(time.time() - start_time, 2), + "uploaded_list": [r for _, r in all_files_to_upload[:50]], + "remote_path": target_root, + } + + # Upload files + for local_p, rel_p in all_files_to_upload: + if not local_p.exists(): + continue + file_size = local_p.stat().st_size + rel_dir = str(Path(rel_p).parent).replace("\\", "/").strip(".") + target_dir = f"{target_root}/{rel_dir}".rstrip("/") if rel_dir else target_root + + ensure_remote_dir(target_dir) + + file_name = local_p.name + with open(local_p, "rb") as fp: + ftp.storbinary(f"STOR {file_name}", fp) + + uploaded_files.append(rel_p) + total_bytes += file_size + + ftp.quit() + duration = round(time.time() - start_time, 2) + return { + "success": True, + "dry_run": False, + "files_uploaded": len(uploaded_files), + "files_skipped": len(skipped_files), + "bytes_transferred": total_bytes, + "duration": duration, + "uploaded_list": uploaded_files, + "remote_path": target_root, + } + + except Exception as e: + logger.error(f"FTP Deploy error: {e}", exc_info=True) + return { + "success": False, + "error": str(e), + "files_uploaded": len(uploaded_files), + "files_skipped": len(skipped_files), + "bytes_transferred": total_bytes, + "duration": round(time.time() - start_time, 2), + "uploaded_list": uploaded_files, + } + + return await asyncio.to_thread(_run_deploy) + + +ftp_manager = FTPManager() diff --git a/telegram-agy-bot/git_manager.py b/telegram-agy-bot/git_manager.py index 4d21847..10fed96 100644 --- a/telegram-agy-bot/git_manager.py +++ b/telegram-agy-bot/git_manager.py @@ -159,8 +159,11 @@ class GitManager: async def init_project_repo(self, workspace_path: str, repo_name: str, owner: str = "root") -> Dict[str, Any]: """ - Initializes git in workspace_path, links with Gitea remote, creates .gitignore, - and pushes the initial commit. + Initializes git in workspace_path with dual branches: + - `production`: base/production branch + - `dev`: active development branch + Links with Gitea remote, creates .gitignore, pushes initial commits on both branches, + and leaves `dev` as the active branch. """ ws = Path(workspace_path).expanduser().resolve() os.makedirs(ws, exist_ok=True) @@ -186,7 +189,7 @@ class GitManager: try: if is_new: - await self._run_cmd(["git", "init", "-b", "main"], cwd=str(ws)) + await self._run_cmd(["git", "init", "-b", "production"], cwd=str(ws)) # Configure author await self._run_cmd(["git", "config", "user.name", "Antigravity Bot"], cwd=str(ws)) @@ -200,19 +203,36 @@ class GitManager: else: await self._run_cmd(["git", "remote", "add", "origin", remote_url], cwd=str(ws)) - # Initial add, commit and push + # Check if production branch exists, if on main rename to production + curr_branch = (await self._run_cmd(["git", "branch", "--show-current"], cwd=str(ws))).strip() + if curr_branch in ("main", "master"): + try: + await self._run_cmd(["git", "branch", "-M", "production"], cwd=str(ws)) + except Exception: + pass + + # Initial add, commit and push to production status_out = await self._run_cmd(["git", "status", "--porcelain"], cwd=str(ws)) if status_out.strip() or is_new: await self._run_cmd(["git", "add", "-A"], cwd=str(ws)) await self._run_cmd(["git", "commit", "-m", f"Initial commit for {clean_name}"], cwd=str(ws)) - await self._run_cmd(["git", "push", "-u", "origin", "main", "--force"], cwd=str(ws)) + await self._run_cmd(["git", "push", "-u", "origin", "production", "--force"], cwd=str(ws)) - logger.info(f"Initialized Git repo for {clean_name} at {ws}") + # Ensure dev branch exists and is pushed + branches_out = await self._run_cmd(["git", "branch"], cwd=str(ws)) + if "dev" not in branches_out: + await self._run_cmd(["git", "checkout", "-b", "dev"], cwd=str(ws)) + await self._run_cmd(["git", "push", "-u", "origin", "dev", "--force"], cwd=str(ws)) + else: + await self._run_cmd(["git", "checkout", "dev"], cwd=str(ws)) + + logger.info(f"Initialized Git repo for {clean_name} at {ws} with dev & production branches.") return { "success": True, "name": clean_name, "web_url": urls["web_url"], "clone_url": urls["clone_url"], + "active_branch": "dev", } except Exception as e: logger.error(f"Error initializing git repo for {clean_name} in {ws}: {e}") @@ -233,7 +253,7 @@ class GitManager: porcelain = await self._run_cmd(["git", "status", "--porcelain"], cwd=str(ws)) dirty_files = [line.strip() for line in porcelain.splitlines() if line.strip()] - branch = (await self._run_cmd(["git", "branch", "--show-current"], cwd=str(ws))).strip() or "main" + branch = (await self._run_cmd(["git", "branch", "--show-current"], cwd=str(ws))).strip() or "dev" # Last commit log_out = await self._run_cmd(["git", "log", "-1", "--format=%h|%an|%ar|%s"], cwd=str(ws)) @@ -258,7 +278,7 @@ class GitManager: } except Exception as e: logger.debug(f"Git status error in {ws}: {e}") - return {"is_git": True, "dirty": False, "files": [], "error": str(e), "branch": "main"} + return {"is_git": True, "dirty": False, "files": [], "error": str(e), "branch": "dev"} async def git_commit_and_push( self, @@ -266,8 +286,9 @@ class GitManager: message: str = "Auto-commit from Antigravity", repo_name: Optional[str] = None, owner: str = "root", + branch: Optional[str] = None, ) -> Tuple[bool, str]: - """Stages all changes, commits with message, and pushes to Gitea.""" + """Stages all changes, commits with message, and pushes to Gitea on the active branch (default dev).""" ws = Path(workspace_path).expanduser().resolve() if not (ws / ".git").exists(): name = repo_name or ws.name @@ -276,42 +297,142 @@ class GitManager: return False, f"Failed to initialize repo: {init_res.get('error')}" try: + curr_branch = branch or (await self._run_cmd(["git", "branch", "--show-current"], cwd=str(ws))).strip() or "dev" # Check for uncommitted changes porcelain = await self._run_cmd(["git", "status", "--porcelain"], cwd=str(ws)) if not porcelain.strip(): - return True, "درخت کاری تمیز است (تغییری برای کامیت وجود نداشت)." + return True, f"درخت کاری شاخه {curr_branch} تمیز است (تغییری برای کامیت وجود نداشت)." await self._run_cmd(["git", "add", "-A"], cwd=str(ws)) clean_msg = message.replace('"', '\\"').replace("\n", " ") if not clean_msg.strip(): - clean_msg = "Updates from Antigravity Bot" + clean_msg = f"Updates on {curr_branch} from Antigravity Bot" commit_out = await self._run_cmd(["git", "commit", "-m", clean_msg], cwd=str(ws)) - push_out = await self._run_cmd(["git", "push", "origin", "main"], cwd=str(ws)) + push_out = await self._run_cmd(["git", "push", "origin", curr_branch], cwd=str(ws)) # Extract hash log_out = await self._run_cmd(["git", "log", "-1", "--format=%h - %s"], cwd=str(ws)) - return True, f"✅ تغییرات با موفقیت کامیت و پوش شد:\n{log_out.strip()}" + return True, f"✅ تغییرات با موفقیت در شاخه {curr_branch} کامیت و پوش شد:\n{log_out.strip()}" except Exception as e: logger.error(f"Git commit/push error in {ws}: {e}") return False, f"❌ خطا در کامیت یا پوش: {e}" - async def git_pull(self, workspace_path: str) -> Tuple[bool, str]: - """Pulls latest changes from remote Gitea repository.""" + async def git_pull(self, workspace_path: str, branch: Optional[str] = None) -> Tuple[bool, str]: + """Pulls latest changes from remote Gitea repository on current or specified branch.""" ws = Path(workspace_path).expanduser().resolve() if not (ws / ".git").exists(): return False, "مخزن گیت برای این مسیر مقداردهی نشده است." try: - # Fetch and check - out = await self._run_cmd(["git", "pull", "origin", "main"], cwd=str(ws)) - return True, f"✅ وضعیت دریافت تغییرات:\n{out.strip()}" + curr_branch = branch or (await self._run_cmd(["git", "branch", "--show-current"], cwd=str(ws))).strip() or "dev" + out = await self._run_cmd(["git", "pull", "origin", curr_branch], cwd=str(ws)) + return True, f"✅ وضعیت دریافت تغییرات شاخه {curr_branch}:\n{out.strip()}" except Exception as e: logger.warning(f"Git pull warning/error in {ws}: {e}") return False, f"⚠️ خطا در دریافت تغییرات (Pull): {e}" + async def git_publish( + self, + workspace_path: str, + message: str = "Publish dev to production", + repo_name: Optional[str] = None, + owner: str = "root", + ) -> Tuple[bool, str, Dict[str, Any]]: + """ + Publishes latest changes from `dev` to `production`: + 1. Ensures changes on `dev` are committed and pushed. + 2. Checks out `production` branch. + 3. Merges `dev` into `production`. + 4. Pushes `production` to Gitea origin. + 5. Switches back to `dev`. + """ + ws = Path(workspace_path).expanduser().resolve() + if not (ws / ".git").exists(): + name = repo_name or ws.name + init_res = await self.init_project_repo(str(ws), name, owner=owner) + if not init_res.get("success"): + return False, f"خطا در مقداردهی اولیه مخزن: {init_res.get('error')}", {} + + try: + # 1. Commit any pending changes on dev + status = await self.git_status(str(ws)) + if status.get("dirty"): + await self.git_commit_and_push(str(ws), message=f"Pre-publish commit: {message}", repo_name=repo_name, owner=owner) + + # Ensure we are on dev branch + curr_branch = (await self._run_cmd(["git", "branch", "--show-current"], cwd=str(ws))).strip() + if curr_branch != "dev": + # Ensure dev exists + branches = await self._run_cmd(["git", "branch"], cwd=str(ws)) + if "dev" in branches: + await self._run_cmd(["git", "checkout", "dev"], cwd=str(ws)) + else: + await self._run_cmd(["git", "checkout", "-b", "dev"], cwd=str(ws)) + + # Push dev + try: + await self._run_cmd(["git", "push", "origin", "dev"], cwd=str(ws)) + except Exception as pe: + logger.warning(f"Push dev warning before publish in {ws}: {pe}") + + # Ensure production branch exists + branches = await self._run_cmd(["git", "branch"], cwd=str(ws)) + if "production" not in branches: + await self._run_cmd(["git", "branch", "production"], cwd=str(ws)) + + # 2. Checkout production + await self._run_cmd(["git", "checkout", "production"], cwd=str(ws)) + + # Pull production if remote exists + try: + await self._run_cmd(["git", "pull", "origin", "production"], cwd=str(ws)) + except Exception: + pass + + # 3. Merge dev into production + merge_msg = f"Merge branch 'dev' into production: {message}".replace('"', '\\"') + merge_out = await self._run_cmd(["git", "merge", "dev", "--no-edit", "-m", merge_msg], cwd=str(ws)) + + # 4. Push production to Gitea + push_out = await self._run_cmd(["git", "push", "origin", "production"], cwd=str(ws)) + + # Get production latest commit + prod_commit = (await self._run_cmd(["git", "log", "-1", "--format=%h|%an|%ar|%s"], cwd=str(ws))).strip() + + # 5. Switch back to dev branch + await self._run_cmd(["git", "checkout", "dev"], cwd=str(ws)) + + commit_parts = prod_commit.split("|", 3) if "|" in prod_commit else [prod_commit, "", "", ""] + commit_hash = commit_parts[0] + commit_text = commit_parts[3] if len(commit_parts) > 3 else prod_commit + + res_msg = ( + f"🚀 پروژه با موفقیت روی شاخه production پابلیش شد!\n\n" + f"• 🌿 شاخه مبدأ: dev\n" + f"• 🚀 شاخه مقصد: production\n" + f"• 🔖 آخرین کامیت: {commit_hash} - {escape_html(commit_text)}\n" + f"• 🔄 وضعیت کاری: مجدداً به شاخه dev بازگشت داده شد تا توسعه ادامه یابد." + ) + + return True, res_msg, { + "commit_hash": commit_hash, + "commit_text": commit_text, + "source_branch": "dev", + "target_branch": "production", + } + + except Exception as e: + logger.error(f"Error publishing dev to production in {ws}: {e}") + # Ensure we return to dev + try: + await self._run_cmd(["git", "checkout", "dev"], cwd=str(ws)) + except Exception: + pass + return False, f"❌ خطا در عملیات پابلیش به شاخه پروداکشن: {e}", {} + async def git_sync(self, workspace_path: str, message: str = "Sync with Gitea", repo_name: Optional[str] = None) -> Tuple[bool, str]: - """Performs full sync: Pulls remote changes, then commits and pushes any local changes.""" + """Performs full sync: Pulls remote changes, then commits and pushes any local changes on active branch.""" ws = Path(workspace_path).expanduser().resolve() if not (ws / ".git").exists(): name = repo_name or ws.name diff --git a/telegram-agy-bot/sessions.json b/telegram-agy-bot/sessions.json index 8f83415..4588478 100644 --- a/telegram-agy-bot/sessions.json +++ b/telegram-agy-bot/sessions.json @@ -19,9 +19,9 @@ "model": "gemini-3.7-flash-auto", "effort": "high", "language": "fa", - "last_context_length": 10997, - "last_total_tokens": 93829, - "last_response": "\u0633\u0644\u0627\u0645 \u0648 \u0627\u062d\u062a\u0631\u0627\u0645. \u067e\u06cc\u0627\u0645 \u0635\u0648\u062a\u06cc \u0634\u0645\u0627 \u0628\u0647 \u062f\u0642\u062a \u0628\u0631\u0631\u0633\u06cc \u0634\u062f. \u06cc\u06a9 \u0628\u0631\u0646\u0627\u0645\u0647\u200c\u0631\u06cc\u0632\u06cc \u062c\u0627\u0645\u0639 \u0648 \u0633\u0627\u062e\u062a\u0627\u0631\u06cc\u0627\u0641\u062a\u0647 \u0628\u0631\u0627\u06cc \u067e\u06cc\u0627\u062f\u0647\u200c\u0633\u0627\u0632\u06cc \u0627\u06cc\u0646 \u0642\u0627\u0628\u0644\u06cc\u062a\u200c\u0647\u0627\u06cc \u06a9\u0644\u06cc\u062f\u06cc \u0648 \u062d\u06cc\u0627\u062a\u06cc \u062f\u0631 \u0631\u0628\u0627\u062a \u0648 \u0633\u06cc\u0633\u062a\u0645 \u067e\u0631\u0648\u0698\u0647\u200c\u0647\u0627 \u0622\u0645\u0627\u062f\u0647 \u06a9\u0631\u062f\u0647\u200c\u0627\u0645:\n\n---\n\n### \ud83d\udccb \u062a\u062d\u0644\u06cc\u0644 \u0648 \u0637\u0631\u062d \u067e\u06cc\u0627\u062f\u0647\u200c\u0633\u0627\u0632\u06cc \u067e\u06cc\u0634\u0646\u0647\u0627\u062f\u06cc\n\n#### \u06f1. \u0627\u0633\u062a\u0631\u0627\u062a\u0698\u06cc \u062f\u0648 \u0634\u0627\u062e\u0647\u200c\u0627\u06cc \u06af\u06cc\u062a (`dev` \u0648 `production`)\n* **\u0633\u0627\u062e\u062a\u0627\u0631 \u0645\u062e\u0632\u0646:** \u0647\u0646\u06af\u0627\u0645 \u0627\u06cc\u062c\u0627\u062f \u06cc\u0627 \u0645\u0642\u062f\u0627\u0631\u062f\u0647\u06cc \u0627\u0648\u0644\u06cc\u0647 \u0647\u0631 \u067e\u0631\u0648\u0698\u0647 \u062f\u0631 \u06af\u06cc\u062a\u060c \u062f\u0648 \u0634\u0627\u062e\u0647 \u0627\u0635\u0644\u06cc \u0628\u0647 \u0635\u0648\u0631\u062a \u062e\u0648\u062f\u06a9\u0627\u0631 \u0627\u06cc\u062c\u0627\u062f \u0648 \u0645\u062f\u06cc\u0631\u06cc\u062a \u0645\u06cc\u200c\u0634\u0648\u0646\u062f:\n * \ud83c\udf3f **\u0634\u0627\u062e\u0647 `dev` (\u0634\u0627\u062e\u0647 \u067e\u06cc\u0634\u200c\u0641\u0631\u0636 \u062a\u0648\u0633\u0639\u0647):** \u062a\u0645\u0627\u0645 \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a\u060c \u0627\u0635\u0644\u0627\u062d\u0627\u062a \u06a9\u062f \u0648 \u06a9\u0627\u0645\u06cc\u062a\u200c\u0647\u0627\u06cc \u0631\u0648\u0632\u0645\u0631\u0647 \u0647\u0648\u0634 \u0645\u0635\u0646\u0648\u0639\u06cc \u0648 \u06a9\u0627\u0631\u0628\u0631 \u0647\u0645\u0648\u0627\u0631\u0647 \u0631\u0648\u06cc \u0634\u0627\u062e\u0647 `dev` \u0627\u0646\u062c\u0627\u0645 \u0645\u06cc\u200c\u0634\u0648\u062f.\n * \ud83d\ude80 **\u0634\u0627\u062e\u0647 `production` (\u0634\u0627\u062e\u0647 \u067e\u0631\u0648\u062f\u0627\u06a9\u0634\u0646/\u0639\u0645\u0644\u06cc\u0627\u062a\u06cc):** \u06a9\u062f\u0647\u0627\u06cc \u067e\u0627\u06cc\u062f\u0627\u0631 \u0648 \u062a\u0633\u062a\u200c\u0634\u062f\u0647 \u06a9\u0647 \u0628\u0627 \u062a\u0627\u06cc\u06cc\u062f \u06cc\u0627 \u062f\u0633\u062a\u0648\u0631 \u067e\u0627\u0628\u0644\u06cc\u0634 (Publish) \u06a9\u0627\u0631\u0628\u0631\u060c \u0627\u0632 `dev` \u0628\u0647 `production` \u0645\u0631\u062c (Merge) \u0648 \u067e\u0648\u0634 (Push) \u0645\u06cc\u200c\u0634\u0648\u0646\u062f.\n* **\u0627\u06a9\u0634\u0646 \u062a\u06af \u0648 \u062f\u0633\u062a\u0648\u0631 \u062c\u062f\u06cc\u062f \u06af\u06cc\u062a:**\n * \u0627\u06a9\u0634\u0646 \u062a\u06af `[[GIT_PUBLISH: project=\"\"]]` \u0648 \u062f\u0633\u062a\u0648\u0631 \u062a\u0644\u06af\u0631\u0627\u0645\u06cc `/publish` \u06cc\u0627 \u062f\u06a9\u0645\u0647 \u0634\u06cc\u0634\u0647\u200c\u0627\u06cc **\u00ab\ud83d\ude80 \u067e\u0627\u0628\u0644\u06cc\u0634 \u0628\u0647 \u067e\u0631\u0648\u062f\u0627\u06a9\u0634\u0646\u00bb** \u0628\u0631\u0627\u06cc \u0627\u0646\u062a\u0642\u0627\u0644 \u0627\u0645\u0646 \u0648 \u0627\u062a\u0648\u0645\u0627\u062a\u06cc\u06a9 \u0622\u062e\u0631\u06cc\u0646 \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a `dev` \u0628\u0647 `production`.\n\n---\n\n#### \u06f2. \u0630\u062e\u06cc\u0631\u0647\u200c\u0633\u0627\u0632\u06cc \u0648 \u0645\u062f\u06cc\u0631\u06cc\u062a \u0627\u0637\u0644\u0627\u0639\u0627\u062a FTP \u0628\u0631\u0627\u06cc \u0647\u0631 \u067e\u0631\u0648\u0698\u0647\n* **\u0641\u06cc\u0644\u062f\u0647\u0627\u06cc \u0627\u062e\u062a\u0635\u0627\u0635\u06cc FTP \u062f\u0631 \u0645\u062f\u0644 \u067e\u0631\u0648\u0698\u0647:**\n * `ftp_host` (\u0622\u062f\u0631\u0633 \u0633\u0631\u0648\u0631 / \u0647\u0627\u0633\u062a)\n * `ftp_port` (\u067e\u06cc\u0634\u200c\u0641\u0631\u0636 \u06f2\u06f1 \u06cc\u0627 \u067e\u0648\u0631\u062a \u0633\u0641\u0627\u0631\u0634\u06cc)\n * `ftp_user` (\u0646\u0627\u0645 \u06a9\u0627\u0631\u0628\u0631\u06cc FTP)\n * `ftp_password` (\u0631\u0645\u0632 \u0639\u0628\u0648\u0631 - \u0631\u0645\u0632\u0646\u06af\u0627\u0631\u06cc\u200c\u0634\u062f\u0647/\u0627\u0645\u0646)\n * `ftp_path` (\u0645\u0633\u06cc\u0631 \u0631\u06cc\u0645\u0648\u062a \u0631\u0648\u06cc \u0647\u0627\u0633\u062a \u0645\u062b\u0644 `/public_html` \u06cc\u0627 `/var/www/html`)\n * `ftp_tls` (\u067e\u0634\u062a\u06cc\u0628\u0627\u0646\u06cc \u0627\u0632 FTP \u0628\u0627 SSL/TLS \u0635\u0631\u06cc\u062d FTPS)\n* **\u0631\u0627\u0628\u0637 \u06a9\u0627\u0631\u0628\u0631\u06cc \u062a\u0644\u06af\u0631\u0627\u0645 \u0648 \u0627\u06a9\u0634\u0646 \u062a\u06af:**\n * \u0627\u0641\u0632\u0648\u062f\u0646 \u0628\u062e\u0634 **\u00ab\u2699\ufe0f \u062a\u0646\u0638\u06cc\u0645\u0627\u062a FTP\u00bb** \u062f\u0631 \u0645\u0646\u0648\u06cc \u067e\u0631\u0648\u0698\u0647\u200c\u0647\u0627 \u0648 \u0627\u0645\u06a9\u0627\u0646 \u0648\u0631\u0648\u062f \u06cc\u0627 \u0648\u06cc\u0631\u0627\u06cc\u0634 \u0627\u0637\u0644\u0627\u0639\u0627\u062a \u0628\u0627 \u0627\u06a9\u0634\u0646 \u062a\u06af `[[FTP_CONFIG: host=\"...\", user=\"...\", pass=\"...\", path=\"...\"]]`.\n * \u062f\u06a9\u0645\u0647 \u062a\u0633\u062a \u0627\u062a\u0635\u0627\u0644 \u0622\u0646\u0644\u0627\u06cc\u0646 \u0628\u0631\u0627\u06cc \u0628\u0631\u0631\u0633\u06cc \u0635\u062d\u062a \u0627\u0637\u0644\u0627\u0639\u0627\u062a FTP \u0642\u0628\u0644 \u0627\u0632 \u062f\u06cc\u067e\u0644\u0648\u06cc.\n\n---\n\n#### \u06f3. \u0627\u0633\u062a\u0642\u0631\u0627\u0631 \u0648 \u062f\u06cc\u067e\u0644\u0648\u06cc \u0647\u0648\u0634\u0645\u0646\u062f \u0627\u0632 \u0637\u0631\u06cc\u0642 FTP (`Smart AI FTP Deployer`)\n* **\u0641\u06cc\u0644\u062a\u0631 \u0647\u0648\u0634\u0645\u0646\u062f \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0632\u0627\u0626\u062f \u0648 \u0645\u0648\u0642\u062a (Clean Deployment):**\n * \u067e\u0627\u06a9\u0633\u0627\u0632\u06cc \u0648 \u0641\u06cc\u0644\u062a\u0631 \u06a9\u0627\u0645\u0644 \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u062d\u0633\u0627\u0633 \u0648 \u063a\u06cc\u0631\u0636\u0631\u0648\u0631\u06cc \u0628\u0631 \u0627\u0633\u0627\u0633 \u0627\u0644\u06af\u0648\u06cc \u0647\u0648\u0634\u0645\u0646\u062f \u0634\u0627\u0645\u0644:\n * \u0645\u062e\u0627\u0632\u0646 \u0648 \u062a\u0646\u0638\u06cc\u0645\u0627\u062a \u062f\u0627\u062e\u0644\u06cc: `.git/`\u060c `.gitignore`\u060c `.agents/`\u060c `.gemini/`\n * \u0645\u062d\u06cc\u0637\u200c\u0647\u0627 \u0648 \u0645\u062a\u063a\u06cc\u0631\u0647\u0627\u06cc \u0645\u062d\u0644\u06cc \u0645\u062d\u0631\u0645\u0627\u0646\u0647: `.env.local`\u060c `.env` (\u062f\u0631 \u0635\u0648\u0631\u062a \u062f\u0631\u062e\u0648\u0627\u0633\u062a)\u060c \u0644\u0627\u06af\u200c\u0647\u0627 (`*.log`)\n * \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u06a9\u0634 \u0648 \u0628\u06cc\u0644\u062f \u0645\u062d\u0644\u06cc: `__pycache__/`\u060c `*.pyc`\u060c `.DS_Store`\u060c `node_modules/` (\u062f\u0631 \u0635\u0648\u0631\u062a \u0646\u06cc\u0627\u0632 \u0628\u0647 \u0628\u06cc\u0644\u062f \u0633\u0628\u06a9)\u060c \u0641\u0648\u0644\u062f\u0627\u0631\u0647\u0627\u06cc \u0645\u0648\u0642\u062a `tmp/`\n* **\u0645\u06a9\u0627\u0646\u06cc\u0632\u0645 \u0647\u0645\u06af\u0627\u0645\u200c\u0633\u0627\u0632\u06cc \u0628\u0647\u06cc\u0646\u0647 (Incremental / Differential Upload):**\n * \u0628\u0631\u0631\u0633\u06cc \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a \u0648 \u062d\u062c\u0645/\u062a\u0627\u0631\u06cc\u062e \u0641\u0627\u06cc\u0644\u200c\u0647\u0627 \u0628\u0631\u0627\u06cc \u0622\u067e\u0644\u0648\u062f \u0641\u0642\u0637 \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u062a\u063a\u06cc\u06cc\u0631\u06cc\u0627\u0641\u062a\u0647 \u0648 \u062c\u0644\u0648\u06af\u06cc\u0631\u06cc \u0627\u0632 \u0627\u0646\u062a\u0642\u0627\u0644 \u0633\u0646\u06af\u06cc\u0646 \u062a\u06a9\u0631\u0627\u0631\u06cc.\n * \u0627\u0631\u0633\u0627\u0644 \u06af\u0632\u0627\u0631\u0634 \u0634\u0641\u0627\u0641 \u0648 \u0632\u06cc\u0628\u0627 \u062f\u0631 \u062a\u0644\u06af\u0631\u0627\u0645 \u0634\u0627\u0645\u0644 \u062a\u0639\u062f\u0627\u062f \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0622\u067e\u0644\u0648\u062f\u0634\u062f\u0647\u060c \u0645\u0633\u06cc\u0631 \u0645\u0642\u0635\u062f \u0648 \u0648\u0636\u0639\u06cc\u062a \u0645\u0648\u0641\u0642\u06cc\u062a.\n* **\u0627\u06a9\u0634\u0646 \u062a\u06af \u0648 \u062f\u06a9\u0645\u0647 \u062f\u06cc\u067e\u0644\u0648\u06cc:**\n * \u0627\u06a9\u0634\u0646 \u062a\u06af `[[FTP_DEPLOY: project=\"\", branch=\"production\"]]` \u0648 \u062f\u06a9\u0645\u0647 \u0634\u06cc\u0634\u0647\u200c\u0627\u06cc \u0632\u06cc\u0631 \u067e\u06cc\u0627\u0645\u200c\u0647\u0627: **\u00ab\ud83d\ude80 \u062f\u06cc\u067e\u0644\u0648\u06cc FTP \u067e\u0631\u0648\u062f\u0627\u06a9\u0634\u0646\u00bb**.\n\n---\n\n#### \u06f4. \u062b\u0628\u062a \u0642\u0627\u0646\u0648\u0646 \u062f\u0627\u0626\u0645\u06cc \u062f\u0631 \u0633\u06cc\u0633\u062a\u0645 \u062d\u0627\u0641\u0638\u0647 (System Memory)\n* \u062b\u0628\u062a \u0642\u0627\u0646\u0648\u0646 \u0627\u0633\u062a\u0631\u0627\u062a\u0698\u06cc \u062f\u0648 \u0634\u0627\u062e\u0647\u200c\u0627\u06cc (`dev` \u0628\u0647 \u0639\u0646\u0648\u0627\u0646 \u0628\u0631\u0646\u0686 \u06a9\u0627\u0631\u06cc \u0648 `production` \u0628\u0631\u0627\u06cc \u067e\u0627\u0628\u0644\u06cc\u0634) \u0648 \u0627\u0644\u0632\u0627\u0645 \u0627\u0633\u062a\u0642\u0631\u0627\u0631 \u062a\u0645\u06cc\u0632 FTP \u062f\u0631 \u062d\u0627\u0641\u0638\u0647 \u0628\u0644\u0646\u062f\u0645\u062f\u062a \u0633\u06cc\u0633\u062a\u0645 \u062a\u0627 \u062a\u0645\u0627\u0645 \u06a9\u0627\u0631\u0647\u0627\u06cc \u062c\u0627\u0631\u06cc \u0622\u06cc\u0646\u062f\u0647 \u0647\u0645\u0648\u0627\u0631\u0647 \u0628\u0627 \u0627\u06cc\u0646 \u0633\u0627\u062e\u062a\u0627\u0631 \u0647\u0645\u0627\u0647\u0646\u06af \u0628\u0627\u0634\u0646\u062f.\n\n---\n\n\u0622\u06cc\u0627 \u0628\u0627 \u0627\u06cc\u0646 \u0633\u0627\u062e\u062a\u0627\u0631 \u0648 \u062c\u0632\u0626\u06cc\u0627\u062a \u067e\u06cc\u0627\u062f\u0647\u200c\u0633\u0627\u0632\u06cc \u0645\u0648\u0627\u0641\u0642\u06cc\u062f \u062a\u0627 \u0645\u0631\u0627\u062d\u0644 \u0627\u062c\u0631\u0627\u06cc \u0622\u0646 \u0631\u0627 \u0631\u0648\u06cc \u0633\u0648\u0631\u0633\u200c\u06a9\u062f \u0631\u0628\u0627\u062a \u0622\u063a\u0627\u0632 \u06a9\u0646\u06cc\u0645\u061f\n\n\u2022 \u0646\u0627\u0645 \u067e\u0631\u0648\u0698\u0647: default\n\u2022 \u0622\u062f\u0631\u0633 \u0645\u062e\u0632\u0646: https://git.msa.artacloud.ir/root/default\n", + "last_context_length": 44953, + "last_total_tokens": 488556, + "last_response": "[[SAVE_MEMORY: type=\"user\", key=\"git_dual_branching_strategy\", category=\"rule\", content=\"\u062a\u0645\u0627\u0645 \u067e\u0631\u0648\u0698\u0647\u200c\u0647\u0627 \u0628\u0627\u06cc\u062f \u0647\u0645\u06cc\u0634\u0647 \u062f\u0648 \u0634\u0627\u062e\u0647 \u0627\u0635\u0644\u06cc dev (\u062a\u0648\u0633\u0639\u0647 \u067e\u06cc\u0634\u200c\u0641\u0631\u0636) \u0648 production (\u0639\u0645\u0644\u06cc\u0627\u062a\u06cc) \u062f\u0627\u0634\u062a\u0647 \u0628\u0627\u0634\u0646\u062f. \u06a9\u0627\u0631\u0647\u0627\u06cc \u0631\u0648\u0632\u0645\u0631\u0647 \u0631\u0648\u06cc dev \u0627\u0646\u062c\u0627\u0645 \u0634\u062f\u0647 \u0648 \u062a\u0646\u0647\u0627 \u0628\u0627 \u062f\u0633\u062a\u0648\u0631 \u067e\u0627\u0628\u0644\u06cc\u0634 \u0628\u0647 production \u0645\u0631\u062c \u0648 \u067e\u0648\u0634 \u0645\u06cc\u200c\u0634\u0648\u0646\u062f.\"]]\n[[SAVE_MEMORY: type=\"user\", key=\"smart_ftp_deployment_rule\", category=\"rule\", content=\"\u062f\u06cc\u067e\u0644\u0648\u06cc FTP \u067e\u0631\u0648\u0698\u0647\u200c\u0647\u0627 \u0628\u0627\u06cc\u062f \u0647\u0648\u0634\u0645\u0646\u062f \u0648 \u062a\u0645\u06cc\u0632 (Clean Deployment) \u0628\u0627\u0634\u062f\u061b \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0632\u0627\u0626\u062f\u060c \u0644\u0627\u06af\u200c\u0647\u0627\u060c \u06a9\u0634\u060c \u062f\u0627\u06cc\u0631\u06a9\u062a\u0648\u0631\u06cc .git\u060c \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0645\u062d\u06cc\u0637\u06cc \u0645\u062d\u0644\u06cc \u0648 \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0633\u0634\u0646 \u0646\u0628\u0627\u06cc\u062f \u0628\u0647 \u0633\u0631\u0648\u0631 FTP \u0627\u0631\u0633\u0627\u0644 \u0634\u0648\u0646\u062f.\"]]\n[[RESTART_BOT]]\n\n\u062a\u0645\u0627\u0645\u06cc \u062a\u0633\u06a9\u200c\u0647\u0627 \u0648 \u0642\u0627\u0628\u0644\u06cc\u062a\u200c\u0647\u0627\u06cc \u062f\u0631\u062e\u0648\u0627\u0633\u062a\u06cc \u0634\u0645\u0627 \u0628\u0627 \u0628\u0627\u0644\u0627\u062a\u0631\u06cc\u0646 \u06a9\u06cc\u0641\u06cc\u062a\u060c \u0627\u0633\u062a\u0627\u0646\u062f\u0627\u0631\u062f\u0647\u0627\u06cc \u0645\u0647\u0646\u062f\u0633\u06cc \u0648 \u062f\u0642\u062a \u067e\u06cc\u0627\u062f\u0647\u200c\u0633\u0627\u0632\u06cc \u0648 \u06cc\u06a9\u067e\u0627\u0631\u0686\u0647\u200c\u0633\u0627\u0632\u06cc \u0634\u062f\u0646\u062f:\n\n---\n\n### \ud83d\udee0\ufe0f \u06af\u0632\u0627\u0631\u0634 \u062c\u0627\u0645\u0639 \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a \u0648 \u0642\u0627\u0628\u0644\u06cc\u062a\u200c\u0647\u0627\u06cc \u067e\u06cc\u0627\u062f\u0647\u200c\u0633\u0627\u0632\u06cc\u200c\u0634\u062f\u0647\n\n#### \u06f1. \u0627\u0633\u062a\u0631\u0627\u062a\u0698\u06cc \u062f\u0648 \u0634\u0627\u062e\u0647\u200c\u0627\u06cc \u06af\u06cc\u062a (`dev` \u0648 `production`)\n* **\u0645\u0642\u062f\u0627\u0631\u062f\u0647\u06cc \u0627\u0648\u0644\u06cc\u0647 \u062f\u0648 \u0634\u0627\u062e\u0647\u200c\u0627\u06cc:** \u062f\u0631 \u0645\u062a\u062f `init_project_repo`\u060c \u0647\u0631 \u067e\u0631\u0648\u0698\u0647 \u0628\u0627 \u062f\u0648 \u0634\u0627\u062e\u0647 `production` (\u0634\u0627\u062e\u0647 \u067e\u0627\u06cc\u0647 \u0648 \u0639\u0645\u0644\u06cc\u0627\u062a\u06cc) \u0648 `dev` (\u0634\u0627\u062e\u0647 \u0641\u0639\u0627\u0644 \u062a\u0648\u0633\u0639\u0647) \u0627\u06cc\u062c\u0627\u062f \u0648 \u0628\u0647 Gitea \u067e\u0648\u0634 \u0645\u06cc\u200c\u0634\u0648\u062f. \u0634\u0627\u062e\u0647 \u067e\u06cc\u0634\u200c\u0641\u0631\u0636 \u06a9\u0627\u0631\u06cc \u0647\u0645\u0648\u0627\u0631\u0647 `dev` \u0627\u0633\u062a.\n* **\u062a\u0648\u0633\u0639\u0647 \u0648 \u06a9\u0627\u0645\u06cc\u062a \u062e\u0648\u062f\u06a9\u0627\u0631 \u0631\u0648\u06cc `dev`:** \u062a\u0645\u0627\u0645\u06cc \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a\u060c \u06a9\u0627\u0645\u06cc\u062a\u200c\u0647\u0627 \u0648 \u0647\u0645\u06af\u0627\u0645\u200c\u0633\u0627\u0632\u06cc\u200c\u0647\u0627\u06cc \u0631\u0648\u0632\u0645\u0631\u0647 \u0647\u0648\u0634 \u0645\u0635\u0646\u0648\u0639\u06cc \u0648 \u06a9\u0627\u0631\u0628\u0631 \u0631\u0648\u06cc \u0634\u0627\u062e\u0647 `dev` \u062b\u0628\u062a \u0645\u06cc\u200c\u0634\u0648\u0646\u062f.\n* **\u0633\u06cc\u0633\u062a\u0645 \u0627\u0646\u062a\u0634\u0627\u0631 \u0648 \u0627\u062f\u063a\u0627\u0645 (`git_publish`):**\n * \u0645\u062a\u062f \u0627\u062e\u062a\u0635\u0627\u0635\u06cc `git_publish` \u0637\u0631\u0627\u062d\u06cc \u0634\u062f \u06a9\u0647 \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a \u0645\u0639\u0644\u0642 `dev` \u0631\u0627 \u06a9\u0627\u0645\u06cc\u062a \u06a9\u0631\u062f\u0647\u060c \u0634\u0627\u062e\u0647 `production` \u0631\u0627 \u062a\u062d\u0648\u06cc\u0644 \u06af\u0631\u0641\u062a\u0647\u060c \u0622\u062e\u0631\u06cc\u0646 \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a `dev` \u0631\u0627 \u0628\u0627 `production` \u0645\u0631\u062c \u0646\u0645\u0648\u062f\u0647\u060c \u062f\u0631 Gitea \u067e\u0648\u0634 \u0645\u06cc\u200c\u06a9\u0646\u062f \u0648 \u0645\u062c\u062f\u062f\u0627\u064b \u0628\u0647 \u0634\u0627\u062e\u0647 `dev` \u0628\u0627\u0632\u0645\u06cc\u200c\u06af\u0631\u062f\u062f.\n * \u062f\u0633\u062a\u0648\u0631 \u062a\u0644\u06af\u0631\u0627\u0645\u06cc \u062c\u062f\u06cc\u062f `/publish` \u0648 \u0627\u06a9\u0634\u0646\u200c\u062a\u06af `[[GIT_PUBLISH: project=\"...\"]]` \u0627\u0636\u0627\u0641\u0647 \u0634\u062f\u0646\u062f.\n\n---\n\n#### \u06f2. \u0645\u0627\u0698\u0648\u0644 \u0627\u0633\u062a\u0642\u0631\u0627\u0631 \u0647\u0648\u0634\u0645\u0646\u062f \u0648 \u062a\u0645\u06cc\u0632 FTP (`ftp_manager.py`)\n* **\u0641\u06cc\u0644\u062a\u0631 \u0647\u0648\u0634\u0645\u0646\u062f \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0627\u0636\u0627\u0641\u06cc (Clean AI Deployment):**\n * \u0644\u06cc\u0633\u062a \u0641\u06cc\u0644\u062a\u0631 \u067e\u06cc\u0634\u200c\u0641\u0631\u0636 \u062c\u0627\u0645\u0639 \u0634\u0627\u0645\u0644:\n * \u0645\u062e\u0632\u0646 \u0648 \u0645\u062a\u0627\u062f\u06cc\u062a\u0627\u06cc \u06af\u06cc\u062a: `.git/`\u060c `.gitignore`\u060c `.gitattributes`\n * \u0645\u062a\u063a\u06cc\u0631\u0647\u0627 \u0648 \u06a9\u0644\u06cc\u062f\u0647\u0627\u06cc \u0645\u062d\u0644\u06cc: `.env*`\u060c `*.pem`\u060c `*.key`\n * \u062f\u06cc\u062a\u0627\u06cc \u0645\u0648\u0642\u062a \u0648 \u0633\u0634\u0646\u200c\u0647\u0627: `sessions_data/`\u060c `uploads_temp/`\u060c `.agents/`\u060c `.gemini/`\n * \u06a9\u0634 \u0648 \u0628\u06cc\u0644\u062f\u0647\u0627: `__pycache__/`\u060c `*.pyc`\u060c `.venv/`\u060c `node_modules/`\n * \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0633\u06cc\u0633\u062a\u0645\u06cc \u0648 \u0644\u0627\u06af: `*.log`\u060c `.DS_Store`\u060c `Thumbs.db`\u060c `.idea/`\u060c `.vscode/`\n* **\u0645\u062f\u06cc\u0631\u06cc\u062a \u0633\u0627\u062e\u062a\u0627\u0631 \u0631\u06cc\u0645\u0648\u062a \u0648 \u0622\u067e\u0644\u0648\u062f \u0647\u0648\u0634\u0645\u0646\u062f:**\n * \u0627\u06cc\u062c\u0627\u062f \u062e\u0648\u062f\u06a9\u0627\u0631 \u062f\u0627\u06cc\u0631\u06a9\u062a\u0648\u0631\u06cc\u200c\u0647\u0627\u06cc \u062a\u0648 \u062f\u0631 \u062a\u0648 \u062f\u0631 \u0633\u0631\u0648\u0631 \u0645\u0642\u0635\u062f\n * \u0645\u062d\u0627\u0633\u0628\u0647 \u062d\u062c\u0645\u060c \u062a\u0639\u062f\u0627\u062f \u0641\u0627\u06cc\u0644\u200c\u0647\u0627 \u0648 \u0645\u062f\u062a \u0632\u0645\u0627\u0646 \u0627\u0646\u062a\u0642\u0627\u0644\n * \u0645\u062a\u062f `test_connection` \u0628\u0631\u0627\u06cc \u0627\u0639\u062a\u0628\u0627\u0631\u0633\u0646\u062c\u06cc \u0627\u062a\u0635\u0627\u0644 \u0642\u0628\u0644 \u0627\u0632 \u0627\u0642\u062f\u0627\u0645 \u0628\u0647 \u062f\u06cc\u067e\u0644\u0648\u06cc.\n\n---\n\n#### \u06f3. \u06cc\u06a9\u067e\u0627\u0631\u0686\u0647\u200c\u0633\u0627\u0632\u06cc \u0645\u062f\u0644 \u062f\u0627\u062f\u0647\u060c \u0627\u06a9\u0634\u0646\u200c\u062a\u06af\u200c\u0647\u0627 \u0648 \u0631\u0627\u0628\u0637 \u06a9\u0627\u0631\u0628\u0631\u06cc \u0631\u0628\u0627\u062a\n* **\u0627\u0631\u062a\u0642\u0627\u06cc \u0645\u062f\u0644 `Project` \u0648 \u0630\u062e\u06cc\u0631\u0647\u200c\u0633\u0627\u0632\u06cc:** \u0641\u06cc\u0644\u062f\u0647\u0627\u06cc `ftp_host`\u060c `ftp_port`\u060c `ftp_user`\u060c `ftp_password`\u060c `ftp_path`\u060c `ftp_tls` \u0648 `active_branch` \u0628\u0647 \u06a9\u0644\u0627\u0633 \u067e\u0631\u0648\u0698\u0647 \u0627\u0636\u0627\u0641\u0647 \u0634\u062f\u0646\u062f.\n* **\u0627\u06a9\u0634\u0646\u200c\u062a\u06af\u200c\u0647\u0627\u06cc \u062c\u062f\u06cc\u062f \u0647\u0648\u0634 \u0645\u0635\u0646\u0648\u0639\u06cc \u062f\u0631 `bot_actions.py`:**\n * `[[GIT_PUBLISH: project=\"\", message=\"\"]]`\n * `[[FTP_CONFIG: host=\"...\", port=21, user=\"...\", password=\"...\", path=\"...\", tls=false]]`\n * `[[FTP_TEST: project=\"\"]]`\n * `[[FTP_DEPLOY: project=\"\", branch=\"production\"]]`\n* **\u0645\u0646\u0648 \u0648 \u062f\u06a9\u0645\u0647\u200c\u0647\u0627\u06cc \u062a\u0644\u06af\u0631\u0627\u0645 \u062f\u0631 `bot.py`:**\n * \u062f\u0633\u062a\u0648\u0631 \u062c\u062f\u06cc\u062f `/ftp` \u0648 \u0645\u0646\u0648\u06cc \u0634\u06cc\u0634\u0647\u200c\u0627\u06cc **\u00ab\ud83d\ude80 \u062f\u06cc\u067e\u0644\u0648\u06cc FTP\u00bb** \u0648 **\u00ab\ud83d\udd0d \u062a\u0633\u062a \u0627\u062a\u0635\u0627\u0644 FTP\u00bb**\n * \u062f\u06a9\u0645\u0647 \u062c\u062f\u06cc\u062f **\u00ab\ud83d\ude80 \u0627\u0646\u062a\u0634\u0627\u0631 \u0628\u0647 \u067e\u0631\u0648\u062f\u0627\u06a9\u0634\u0646 (Publish)\u00bb** \u062f\u0631 \u0645\u0646\u0648\u06cc \u06af\u06cc\u062a (`/git`)\n * \u062a\u0633\u062a \u0633\u06cc\u0646\u062a\u06a9\u0633 \u0641\u0627\u06cc\u0644\u200c\u0647\u0627 \u0628\u0627 \u0645\u0648\u0641\u0642\u06cc\u062a \u06a9\u0627\u0645\u0644 \u0627\u0646\u062c\u0627\u0645 \u0634\u062f \u0648 \u0631\u0628\u0627\u062a \u0628\u0627 \u062a\u06af `[[RESTART_BOT]]` \u0628\u0647 \u0635\u0648\u0631\u062a \u0631\u0648\u0627\u0646 \u0645\u062c\u062f\u062f\u0627\u064b \u0631\u0627\u0647\u200c\u0627\u0646\u062f\u0627\u0632\u06cc \u0634\u062f.\n\n---\n\n[[BUTTONS: layout=[\n [{\"text\": \"\ud83d\udc19 \u0645\u0646\u0648\u06cc \u06af\u06cc\u062a \u0648 \u0628\u0631\u0646\u0686\u200c\u0647\u0627\", \"cmd\": \"/git\"}, {\"text\": \"\ud83d\ude80 \u062a\u0646\u0638\u06cc\u0645\u0627\u062a \u0648 \u062f\u06cc\u067e\u0644\u0648\u06cc FTP\", \"cmd\": \"/ftp\"}],\n [{\"text\": \"\ud83d\ude80 \u067e\u0627\u0628\u0644\u06cc\u0634 \u0628\u0647 \u067e\u0631\u0648\u062f\u0627\u06a9\u0634\u0646\", \"cmd\": \"/publish\"}, {\"text\": \"\ud83d\udcc1 \u0644\u06cc\u0633\u062a \u067e\u0631\u0648\u0698\u0647\u200c\u0647\u0627\", \"cmd\": \"/projects\"}]\n]]]\n\n\u2022 \u0646\u0627\u0645 \u067e\u0631\u0648\u0698\u0647: default\n\u2022 \u0622\u062f\u0631\u0633 \u0645\u062e\u0632\u0646: https://git.msa.artacloud.ir/root/default\n", "created_at": 1787995349.975879, "description": "", "conversation_titles": { @@ -96,19 +96,19 @@ } }, "turn_in_progress": false, - "last_prompt": "[User sent a voice message transcribed as: \"\u067e\u0631\u0648\u062c\u0647 \u0647\u0627\u06cc \u0645\u0627 \u0628\u0627\u06cc\u062a \u0647\u0645\u06cc\u0634\u0647 \u062f\u0648 \u062a\u0628\u0631\u0627\u06cc\u0646\u0634 \u062f\u0627\u0634\u062a\u06cc \u0628\u0627\u0634\u0647 \u06cc\u06a9\u06cc \u067e\u0631\u0648\u0636\u0627\u0642\u062a \u0627\u06cc\u06a9 \u062f\u06cc\u0641 \u0645\u0627 \u062a\u0645\u0627\u0645 \u062a\u0631\u063a\u06cc\u0631\u062a \u0645\u0631\u0648 \u0647\u0645\u06cc\u0634\u0647 \u0628\u0627\u06cc\u062a \u0631\u0648\u06cc \u0628\u0631\u0627\u06cc\u0646\u0634\u0647 \u062f\u06cc\u0641 \u0627\u06cc\u0646 \u062c\u0646\u0628\u06cc\u062f\u06cc\u0645 \u062f\u0631 \u0633\u0631\u062a\u06cc\u06a9\u0647 \u06a9\u0627\u0631\u0628\u0631 \u062f\u0633\u062a\u0648\u0631 \u067e\u0628\u0644\u06cc\u0634 \u0627\u0631\u0633\u0627\u0644 \u06a9\u0646\u0647 \u0645\u0627 \u062a\u0631\u063a\u06cc\u0631\u062a \u0631\u0648 \u0645\u06cc\u0628\u0631\u06cc\u0645 \u0631\u0648\u06cc \u067e\u0631\u0648\u0636\u0627\u0642\u062a \u062e\u0648\u0628 \u0628\u0627\u06cc\u062a \u0645\u0646 \u0645\u062e\u0627\u0645 \u0628\u06a9\u0627\u0631\u0628\u0631 \u0627\u06cc\u062c\u0627\u0632\u0628\u06cc\u062f\u0645 \u06a9\u0647 \u0627\u062a\u062a\u0644\u0627\u062a\u0647 \u0627\u0641\u062a\u06cc\u067e\u06cc \u0631\u0648 \u0647\u0645 \u0628\u0627\u0631\u062f \u0631\u0648 \u0628\u0627\u062a \u06a9\u0646\u0647 \u0648 \u0647\u0631 \u062f\u0631 \u0628\u063a\u0647 \u067e\u0631\u0648\u062c\u0647 \u06cc\u06a9 \u0627\u0641\u062a\u06cc\u067e\u06cc \u062f\u0627\u0634\u062a\u06cc \u0628\u0627\u0634\u0647 \u06a9\u0647 \u0647\u0631 \u0645\u063a\u0647 \u062f\u0648 \u06a9\u0634\u0645 \u06cc\u0647 \u062f\u06cc\u067e\u0644\u0648\u06cc \u067e\u0631\u0648\u0636\u0627\u0642\u062a \u062f\u06cc\u067e\u0644\u0648\u06cc \u06cc\u0627 \u062f\u06cc\u067e\u0644\u0648\u06cc \u0647\u0631\u0686\u0647 \u06a9\u0647 \u062e\u0648\u0628\u0647 \u0631\u0648\u0632\u0627\u062a \u0627\u0633\u0631\u06af\u0648\u0646 \u0627\u0641\u062a\u06cc\u067e\u06cc \u0641\u0627\u0626\u0644 \u0647\u0627 \u0628\u0631\u0627\u06cc\u0646 \u0631\u06cc \u0633\u0631\u0648\u0631 \u0648 \u0627\u0644\u0628\u062a\u06cc \u0627\u06cc\u0646\u062c\u0627 \u0628\u0627\u06cc\u062f \u0627\u0632 \u062e\u0648\u0634\u0647 \u0645\u0633\u0646\u0648\u06cc\u0633\u062a \u0633\u0641\u0627 \u062f\u0648 \u0628\u0634\u0647 \u0648 \u0646\u0628\u0627\u06cc\u062f \u0641\u0627\u0626\u0644 \u0647\u0627 \u06cc\u0647 \u0627\u062a\u0648 \u0627\u0634\u062e\u0627\u0644 \u0648 \u06a9\u0644\u0646 \u0641\u0627\u0626\u0644 \u0647\u0627 \u06cc\u0647 \u0627\u0632 \u0622\u0641\u06cc \u0628\u0633\u0631\u0648\u0631 \u0627\u0631\u0633\u0627\u0644 \u0628\u0634\u0647\"]\n\n\u067e\u0631\u0648\u062c\u0647 \u0647\u0627\u06cc \u0645\u0627 \u0628\u0627\u06cc\u062a \u0647\u0645\u06cc\u0634\u0647 \u062f\u0648 \u062a\u0628\u0631\u0627\u06cc\u0646\u0634 \u062f\u0627\u0634\u062a\u06cc \u0628\u0627\u0634\u0647 \u06cc\u06a9\u06cc \u067e\u0631\u0648\u0636\u0627\u0642\u062a \u0627\u06cc\u06a9 \u062f\u06cc\u0641 \u0645\u0627 \u062a\u0645\u0627\u0645 \u062a\u0631\u063a\u06cc\u0631\u062a \u0645\u0631\u0648 \u0647\u0645\u06cc\u0634\u0647 \u0628\u0627\u06cc\u062a \u0631\u0648\u06cc \u0628\u0631\u0627\u06cc\u0646\u0634\u0647 \u062f\u06cc\u0641 \u0627\u06cc\u0646 \u062c\u0646\u0628\u06cc\u062f\u06cc\u0645 \u062f\u0631 \u0633\u0631\u062a\u06cc\u06a9\u0647 \u06a9\u0627\u0631\u0628\u0631 \u062f\u0633\u062a\u0648\u0631 \u067e\u0628\u0644\u06cc\u0634 \u0627\u0631\u0633\u0627\u0644 \u06a9\u0646\u0647 \u0645\u0627 \u062a\u0631\u063a\u06cc\u0631\u062a \u0631\u0648 \u0645\u06cc\u0628\u0631\u06cc\u0645 \u0631\u0648\u06cc \u067e\u0631\u0648\u0636\u0627\u0642\u062a \u062e\u0648\u0628 \u0628\u0627\u06cc\u062a \u0645\u0646 \u0645\u062e\u0627\u0645 \u0628\u06a9\u0627\u0631\u0628\u0631 \u0627\u06cc\u062c\u0627\u0632\u0628\u06cc\u062f\u0645 \u06a9\u0647 \u0627\u062a\u062a\u0644\u0627\u062a\u0647 \u0627\u0641\u062a\u06cc\u067e\u06cc \u0631\u0648 \u0647\u0645 \u0628\u0627\u0631\u062f \u0631\u0648 \u0628\u0627\u062a \u06a9\u0646\u0647 \u0648 \u0647\u0631 \u062f\u0631 \u0628\u063a\u0647 \u067e\u0631\u0648\u062c\u0647 \u06cc\u06a9 \u0627\u0641\u062a\u06cc\u067e\u06cc \u062f\u0627\u0634\u062a\u06cc \u0628\u0627\u0634\u0647 \u06a9\u0647 \u0647\u0631 \u0645\u063a\u0647 \u062f\u0648 \u06a9\u0634\u0645 \u06cc\u0647 \u062f\u06cc\u067e\u0644\u0648\u06cc \u067e\u0631\u0648\u0636\u0627\u0642\u062a \u062f\u06cc\u067e\u0644\u0648\u06cc \u06cc\u0627 \u062f\u06cc\u067e\u0644\u0648\u06cc \u0647\u0631\u0686\u0647 \u06a9\u0647 \u062e\u0648\u0628\u0647 \u0631\u0648\u0632\u0627\u062a \u0627\u0633\u0631\u06af\u0648\u0646 \u0627\u0641\u062a\u06cc\u067e\u06cc \u0641\u0627\u0626\u0644 \u0647\u0627 \u0628\u0631\u0627\u06cc\u0646 \u0631\u06cc \u0633\u0631\u0648\u0631 \u0648 \u0627\u0644\u0628\u062a\u06cc \u0627\u06cc\u0646\u062c\u0627 \u0628\u0627\u06cc\u062f \u0627\u0632 \u062e\u0648\u0634\u0647 \u0645\u0633\u0646\u0648\u06cc\u0633\u062a \u0633\u0641\u0627 \u062f\u0648 \u0628\u0634\u0647 \u0648 \u0646\u0628\u0627\u06cc\u062f \u0641\u0627\u0626\u0644 \u0647\u0627 \u06cc\u0647 \u0627\u062a\u0648 \u0627\u0634\u062e\u0627\u0644 \u0648 \u06a9\u0644\u0646 \u0641\u0627\u0626\u0644 \u0647\u0627 \u06cc\u0647 \u0627\u0632 \u0622\u0641\u06cc \u0628\u0633\u0631\u0648\u0631 \u0627\u0631\u0633\u0627\u0644 \u0628\u0634\u0647", - "last_response": "\u0633\u0644\u0627\u0645 \u0648 \u0627\u062d\u062a\u0631\u0627\u0645. \u067e\u06cc\u0627\u0645 \u0635\u0648\u062a\u06cc \u0634\u0645\u0627 \u0628\u0647 \u062f\u0642\u062a \u0628\u0631\u0631\u0633\u06cc \u0634\u062f. \u06cc\u06a9 \u0628\u0631\u0646\u0627\u0645\u0647\u200c\u0631\u06cc\u0632\u06cc \u062c\u0627\u0645\u0639 \u0648 \u0633\u0627\u062e\u062a\u0627\u0631\u06cc\u0627\u0641\u062a\u0647 \u0628\u0631\u0627\u06cc \u067e\u06cc\u0627\u062f\u0647\u200c\u0633\u0627\u0632\u06cc \u0627\u06cc\u0646 \u0642\u0627\u0628\u0644\u06cc\u062a\u200c\u0647\u0627\u06cc \u06a9\u0644\u06cc\u062f\u06cc \u0648 \u062d\u06cc\u0627\u062a\u06cc \u062f\u0631 \u0631\u0628\u0627\u062a \u0648 \u0633\u06cc\u0633\u062a\u0645 \u067e\u0631\u0648\u0698\u0647\u200c\u0647\u0627 \u0622\u0645\u0627\u062f\u0647 \u06a9\u0631\u062f\u0647\u200c\u0627\u0645:\n\n---\n\n### \ud83d\udccb \u062a\u062d\u0644\u06cc\u0644 \u0648 \u0637\u0631\u062d \u067e\u06cc\u0627\u062f\u0647\u200c\u0633\u0627\u0632\u06cc \u067e\u06cc\u0634\u0646\u0647\u0627\u062f\u06cc\n\n#### \u06f1. \u0627\u0633\u062a\u0631\u0627\u062a\u0698\u06cc \u062f\u0648 \u0634\u0627\u062e\u0647\u200c\u0627\u06cc \u06af\u06cc\u062a (`dev` \u0648 `production`)\n* **\u0633\u0627\u062e\u062a\u0627\u0631 \u0645\u062e\u0632\u0646:** \u0647\u0646\u06af\u0627\u0645 \u0627\u06cc\u062c\u0627\u062f \u06cc\u0627 \u0645\u0642\u062f\u0627\u0631\u062f\u0647\u06cc \u0627\u0648\u0644\u06cc\u0647 \u0647\u0631 \u067e\u0631\u0648\u0698\u0647 \u062f\u0631 \u06af\u06cc\u062a\u060c \u062f\u0648 \u0634\u0627\u062e\u0647 \u0627\u0635\u0644\u06cc \u0628\u0647 \u0635\u0648\u0631\u062a \u062e\u0648\u062f\u06a9\u0627\u0631 \u0627\u06cc\u062c\u0627\u062f \u0648 \u0645\u062f\u06cc\u0631\u06cc\u062a \u0645\u06cc\u200c\u0634\u0648\u0646\u062f:\n * \ud83c\udf3f **\u0634\u0627\u062e\u0647 `dev` (\u0634\u0627\u062e\u0647 \u067e\u06cc\u0634\u200c\u0641\u0631\u0636 \u062a\u0648\u0633\u0639\u0647):** \u062a\u0645\u0627\u0645 \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a\u060c \u0627\u0635\u0644\u0627\u062d\u0627\u062a \u06a9\u062f \u0648 \u06a9\u0627\u0645\u06cc\u062a\u200c\u0647\u0627\u06cc \u0631\u0648\u0632\u0645\u0631\u0647 \u0647\u0648\u0634 \u0645\u0635\u0646\u0648\u0639\u06cc \u0648 \u06a9\u0627\u0631\u0628\u0631 \u0647\u0645\u0648\u0627\u0631\u0647 \u0631\u0648\u06cc \u0634\u0627\u062e\u0647 `dev` \u0627\u0646\u062c\u0627\u0645 \u0645\u06cc\u200c\u0634\u0648\u062f.\n * \ud83d\ude80 **\u0634\u0627\u062e\u0647 `production` (\u0634\u0627\u062e\u0647 \u067e\u0631\u0648\u062f\u0627\u06a9\u0634\u0646/\u0639\u0645\u0644\u06cc\u0627\u062a\u06cc):** \u06a9\u062f\u0647\u0627\u06cc \u067e\u0627\u06cc\u062f\u0627\u0631 \u0648 \u062a\u0633\u062a\u200c\u0634\u062f\u0647 \u06a9\u0647 \u0628\u0627 \u062a\u0627\u06cc\u06cc\u062f \u06cc\u0627 \u062f\u0633\u062a\u0648\u0631 \u067e\u0627\u0628\u0644\u06cc\u0634 (Publish) \u06a9\u0627\u0631\u0628\u0631\u060c \u0627\u0632 `dev` \u0628\u0647 `production` \u0645\u0631\u062c (Merge) \u0648 \u067e\u0648\u0634 (Push) \u0645\u06cc\u200c\u0634\u0648\u0646\u062f.\n* **\u0627\u06a9\u0634\u0646 \u062a\u06af \u0648 \u062f\u0633\u062a\u0648\u0631 \u062c\u062f\u06cc\u062f \u06af\u06cc\u062a:**\n * \u0627\u06a9\u0634\u0646 \u062a\u06af `[[GIT_PUBLISH: project=\"\"]]` \u0648 \u062f\u0633\u062a\u0648\u0631 \u062a\u0644\u06af\u0631\u0627\u0645\u06cc `/publish` \u06cc\u0627 \u062f\u06a9\u0645\u0647 \u0634\u06cc\u0634\u0647\u200c\u0627\u06cc **\u00ab\ud83d\ude80 \u067e\u0627\u0628\u0644\u06cc\u0634 \u0628\u0647 \u067e\u0631\u0648\u062f\u0627\u06a9\u0634\u0646\u00bb** \u0628\u0631\u0627\u06cc \u0627\u0646\u062a\u0642\u0627\u0644 \u0627\u0645\u0646 \u0648 \u0627\u062a\u0648\u0645\u0627\u062a\u06cc\u06a9 \u0622\u062e\u0631\u06cc\u0646 \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a `dev` \u0628\u0647 `production`.\n\n---\n\n#### \u06f2. \u0630\u062e\u06cc\u0631\u0647\u200c\u0633\u0627\u0632\u06cc \u0648 \u0645\u062f\u06cc\u0631\u06cc\u062a \u0627\u0637\u0644\u0627\u0639\u0627\u062a FTP \u0628\u0631\u0627\u06cc \u0647\u0631 \u067e\u0631\u0648\u0698\u0647\n* **\u0641\u06cc\u0644\u062f\u0647\u0627\u06cc \u0627\u062e\u062a\u0635\u0627\u0635\u06cc FTP \u062f\u0631 \u0645\u062f\u0644 \u067e\u0631\u0648\u0698\u0647:**\n * `ftp_host` (\u0622\u062f\u0631\u0633 \u0633\u0631\u0648\u0631 / \u0647\u0627\u0633\u062a)\n * `ftp_port` (\u067e\u06cc\u0634\u200c\u0641\u0631\u0636 \u06f2\u06f1 \u06cc\u0627 \u067e\u0648\u0631\u062a \u0633\u0641\u0627\u0631\u0634\u06cc)\n * `ftp_user` (\u0646\u0627\u0645 \u06a9\u0627\u0631\u0628\u0631\u06cc FTP)\n * `ftp_password` (\u0631\u0645\u0632 \u0639\u0628\u0648\u0631 - \u0631\u0645\u0632\u0646\u06af\u0627\u0631\u06cc\u200c\u0634\u062f\u0647/\u0627\u0645\u0646)\n * `ftp_path` (\u0645\u0633\u06cc\u0631 \u0631\u06cc\u0645\u0648\u062a \u0631\u0648\u06cc \u0647\u0627\u0633\u062a \u0645\u062b\u0644 `/public_html` \u06cc\u0627 `/var/www/html`)\n * `ftp_tls` (\u067e\u0634\u062a\u06cc\u0628\u0627\u0646\u06cc \u0627\u0632 FTP \u0628\u0627 SSL/TLS \u0635\u0631\u06cc\u062d FTPS)\n* **\u0631\u0627\u0628\u0637 \u06a9\u0627\u0631\u0628\u0631\u06cc \u062a\u0644\u06af\u0631\u0627\u0645 \u0648 \u0627\u06a9\u0634\u0646 \u062a\u06af:**\n * \u0627\u0641\u0632\u0648\u062f\u0646 \u0628\u062e\u0634 **\u00ab\u2699\ufe0f \u062a\u0646\u0638\u06cc\u0645\u0627\u062a FTP\u00bb** \u062f\u0631 \u0645\u0646\u0648\u06cc \u067e\u0631\u0648\u0698\u0647\u200c\u0647\u0627 \u0648 \u0627\u0645\u06a9\u0627\u0646 \u0648\u0631\u0648\u062f \u06cc\u0627 \u0648\u06cc\u0631\u0627\u06cc\u0634 \u0627\u0637\u0644\u0627\u0639\u0627\u062a \u0628\u0627 \u0627\u06a9\u0634\u0646 \u062a\u06af `[[FTP_CONFIG: host=\"...\", user=\"...\", pass=\"...\", path=\"...\"]]`.\n * \u062f\u06a9\u0645\u0647 \u062a\u0633\u062a \u0627\u062a\u0635\u0627\u0644 \u0622\u0646\u0644\u0627\u06cc\u0646 \u0628\u0631\u0627\u06cc \u0628\u0631\u0631\u0633\u06cc \u0635\u062d\u062a \u0627\u0637\u0644\u0627\u0639\u0627\u062a FTP \u0642\u0628\u0644 \u0627\u0632 \u062f\u06cc\u067e\u0644\u0648\u06cc.\n\n---\n\n#### \u06f3. \u0627\u0633\u062a\u0642\u0631\u0627\u0631 \u0648 \u062f\u06cc\u067e\u0644\u0648\u06cc \u0647\u0648\u0634\u0645\u0646\u062f \u0627\u0632 \u0637\u0631\u06cc\u0642 FTP (`Smart AI FTP Deployer`)\n* **\u0641\u06cc\u0644\u062a\u0631 \u0647\u0648\u0634\u0645\u0646\u062f \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0632\u0627\u0626\u062f \u0648 \u0645\u0648\u0642\u062a (Clean Deployment):**\n * \u067e\u0627\u06a9\u0633\u0627\u0632\u06cc \u0648 \u0641\u06cc\u0644\u062a\u0631 \u06a9\u0627\u0645\u0644 \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u062d\u0633\u0627\u0633 \u0648 \u063a\u06cc\u0631\u0636\u0631\u0648\u0631\u06cc \u0628\u0631 \u0627\u0633\u0627\u0633 \u0627\u0644\u06af\u0648\u06cc \u0647\u0648\u0634\u0645\u0646\u062f \u0634\u0627\u0645\u0644:\n * \u0645\u062e\u0627\u0632\u0646 \u0648 \u062a\u0646\u0638\u06cc\u0645\u0627\u062a \u062f\u0627\u062e\u0644\u06cc: `.git/`\u060c `.gitignore`\u060c `.agents/`\u060c `.gemini/`\n * \u0645\u062d\u06cc\u0637\u200c\u0647\u0627 \u0648 \u0645\u062a\u063a\u06cc\u0631\u0647\u0627\u06cc \u0645\u062d\u0644\u06cc \u0645\u062d\u0631\u0645\u0627\u0646\u0647: `.env.local`\u060c `.env` (\u062f\u0631 \u0635\u0648\u0631\u062a \u062f\u0631\u062e\u0648\u0627\u0633\u062a)\u060c \u0644\u0627\u06af\u200c\u0647\u0627 (`*.log`)\n * \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u06a9\u0634 \u0648 \u0628\u06cc\u0644\u062f \u0645\u062d\u0644\u06cc: `__pycache__/`\u060c `*.pyc`\u060c `.DS_Store`\u060c `node_modules/` (\u062f\u0631 \u0635\u0648\u0631\u062a \u0646\u06cc\u0627\u0632 \u0628\u0647 \u0628\u06cc\u0644\u062f \u0633\u0628\u06a9)\u060c \u0641\u0648\u0644\u062f\u0627\u0631\u0647\u0627\u06cc \u0645\u0648\u0642\u062a `tmp/`\n* **\u0645\u06a9\u0627\u0646\u06cc\u0632\u0645 \u0647\u0645\u06af\u0627\u0645\u200c\u0633\u0627\u0632\u06cc \u0628\u0647\u06cc\u0646\u0647 (Incremental / Differential Upload):**\n * \u0628\u0631\u0631\u0633\u06cc \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a \u0648 \u062d\u062c\u0645/\u062a\u0627\u0631\u06cc\u062e \u0641\u0627\u06cc\u0644\u200c\u0647\u0627 \u0628\u0631\u0627\u06cc \u0622\u067e\u0644\u0648\u062f \u0641\u0642\u0637 \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u062a\u063a\u06cc\u06cc\u0631\u06cc\u0627\u0641\u062a\u0647 \u0648 \u062c\u0644\u0648\u06af\u06cc\u0631\u06cc \u0627\u0632 \u0627\u0646\u062a\u0642\u0627\u0644 \u0633\u0646\u06af\u06cc\u0646 \u062a\u06a9\u0631\u0627\u0631\u06cc.\n * \u0627\u0631\u0633\u0627\u0644 \u06af\u0632\u0627\u0631\u0634 \u0634\u0641\u0627\u0641 \u0648 \u0632\u06cc\u0628\u0627 \u062f\u0631 \u062a\u0644\u06af\u0631\u0627\u0645 \u0634\u0627\u0645\u0644 \u062a\u0639\u062f\u0627\u062f \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0622\u067e\u0644\u0648\u062f\u0634\u062f\u0647\u060c \u0645\u0633\u06cc\u0631 \u0645\u0642\u0635\u062f \u0648 \u0648\u0636\u0639\u06cc\u062a \u0645\u0648\u0641\u0642\u06cc\u062a.\n* **\u0627\u06a9\u0634\u0646 \u062a\u06af \u0648 \u062f\u06a9\u0645\u0647 \u062f\u06cc\u067e\u0644\u0648\u06cc:**\n * \u0627\u06a9\u0634\u0646 \u062a\u06af `[[FTP_DEPLOY: project=\"\", branch=\"production\"]]` \u0648 \u062f\u06a9\u0645\u0647 \u0634\u06cc\u0634\u0647\u200c\u0627\u06cc \u0632\u06cc\u0631 \u067e\u06cc\u0627\u0645\u200c\u0647\u0627: **\u00ab\ud83d\ude80 \u062f\u06cc\u067e\u0644\u0648\u06cc FTP \u067e\u0631\u0648\u062f\u0627\u06a9\u0634\u0646\u00bb**.\n\n---\n\n#### \u06f4. \u062b\u0628\u062a \u0642\u0627\u0646\u0648\u0646 \u062f\u0627\u0626\u0645\u06cc \u062f\u0631 \u0633\u06cc\u0633\u062a\u0645 \u062d\u0627\u0641\u0638\u0647 (System Memory)\n* \u062b\u0628\u062a \u0642\u0627\u0646\u0648\u0646 \u0627\u0633\u062a\u0631\u0627\u062a\u0698\u06cc \u062f\u0648 \u0634\u0627\u062e\u0647\u200c\u0627\u06cc (`dev` \u0628\u0647 \u0639\u0646\u0648\u0627\u0646 \u0628\u0631\u0646\u0686 \u06a9\u0627\u0631\u06cc \u0648 `production` \u0628\u0631\u0627\u06cc \u067e\u0627\u0628\u0644\u06cc\u0634) \u0648 \u0627\u0644\u0632\u0627\u0645 \u0627\u0633\u062a\u0642\u0631\u0627\u0631 \u062a\u0645\u06cc\u0632 FTP \u062f\u0631 \u062d\u0627\u0641\u0638\u0647 \u0628\u0644\u0646\u062f\u0645\u062f\u062a \u0633\u06cc\u0633\u062a\u0645 \u062a\u0627 \u062a\u0645\u0627\u0645 \u06a9\u0627\u0631\u0647\u0627\u06cc \u062c\u0627\u0631\u06cc \u0622\u06cc\u0646\u062f\u0647 \u0647\u0645\u0648\u0627\u0631\u0647 \u0628\u0627 \u0627\u06cc\u0646 \u0633\u0627\u062e\u062a\u0627\u0631 \u0647\u0645\u0627\u0647\u0646\u06af \u0628\u0627\u0634\u0646\u062f.\n\n---\n\n\u0622\u06cc\u0627 \u0628\u0627 \u0627\u06cc\u0646 \u0633\u0627\u062e\u062a\u0627\u0631 \u0648 \u062c\u0632\u0626\u06cc\u0627\u062a \u067e\u06cc\u0627\u062f\u0647\u200c\u0633\u0627\u0632\u06cc \u0645\u0648\u0627\u0641\u0642\u06cc\u062f \u062a\u0627 \u0645\u0631\u0627\u062d\u0644 \u0627\u062c\u0631\u0627\u06cc \u0622\u0646 \u0631\u0627 \u0631\u0648\u06cc \u0633\u0648\u0631\u0633\u200c\u06a9\u062f \u0631\u0628\u0627\u062a \u0622\u063a\u0627\u0632 \u06a9\u0646\u06cc\u0645\u061f\n\n\u2022 \u0646\u0627\u0645 \u067e\u0631\u0648\u0698\u0647: default\n\u2022 \u0622\u062f\u0631\u0633 \u0645\u062e\u0632\u0646: https://git.msa.artacloud.ir/root/default\n", + "last_prompt": "\u062a\u0633\u06a9 \u0647\u0627 \u0631\u0627 \u0622\u0645\u0627\u062f\u0647 \u06a9\u0646 \u0627\u06cc\u0646 \u0633\u0627\u062e\u062a\u0627\u0631 \u0639\u0627\u0644\u06cc\u0647", + "last_response": "[[SAVE_MEMORY: type=\"user\", key=\"git_dual_branching_strategy\", category=\"rule\", content=\"\u062a\u0645\u0627\u0645 \u067e\u0631\u0648\u0698\u0647\u200c\u0647\u0627 \u0628\u0627\u06cc\u062f \u0647\u0645\u06cc\u0634\u0647 \u062f\u0648 \u0634\u0627\u062e\u0647 \u0627\u0635\u0644\u06cc dev (\u062a\u0648\u0633\u0639\u0647 \u067e\u06cc\u0634\u200c\u0641\u0631\u0636) \u0648 production (\u0639\u0645\u0644\u06cc\u0627\u062a\u06cc) \u062f\u0627\u0634\u062a\u0647 \u0628\u0627\u0634\u0646\u062f. \u06a9\u0627\u0631\u0647\u0627\u06cc \u0631\u0648\u0632\u0645\u0631\u0647 \u0631\u0648\u06cc dev \u0627\u0646\u062c\u0627\u0645 \u0634\u062f\u0647 \u0648 \u062a\u0646\u0647\u0627 \u0628\u0627 \u062f\u0633\u062a\u0648\u0631 \u067e\u0627\u0628\u0644\u06cc\u0634 \u0628\u0647 production \u0645\u0631\u062c \u0648 \u067e\u0648\u0634 \u0645\u06cc\u200c\u0634\u0648\u0646\u062f.\"]]\n[[SAVE_MEMORY: type=\"user\", key=\"smart_ftp_deployment_rule\", category=\"rule\", content=\"\u062f\u06cc\u067e\u0644\u0648\u06cc FTP \u067e\u0631\u0648\u0698\u0647\u200c\u0647\u0627 \u0628\u0627\u06cc\u062f \u0647\u0648\u0634\u0645\u0646\u062f \u0648 \u062a\u0645\u06cc\u0632 (Clean Deployment) \u0628\u0627\u0634\u062f\u061b \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0632\u0627\u0626\u062f\u060c \u0644\u0627\u06af\u200c\u0647\u0627\u060c \u06a9\u0634\u060c \u062f\u0627\u06cc\u0631\u06a9\u062a\u0648\u0631\u06cc .git\u060c \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0645\u062d\u06cc\u0637\u06cc \u0645\u062d\u0644\u06cc \u0648 \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0633\u0634\u0646 \u0646\u0628\u0627\u06cc\u062f \u0628\u0647 \u0633\u0631\u0648\u0631 FTP \u0627\u0631\u0633\u0627\u0644 \u0634\u0648\u0646\u062f.\"]]\n[[RESTART_BOT]]\n\n\u062a\u0645\u0627\u0645\u06cc \u062a\u0633\u06a9\u200c\u0647\u0627 \u0648 \u0642\u0627\u0628\u0644\u06cc\u062a\u200c\u0647\u0627\u06cc \u062f\u0631\u062e\u0648\u0627\u0633\u062a\u06cc \u0634\u0645\u0627 \u0628\u0627 \u0628\u0627\u0644\u0627\u062a\u0631\u06cc\u0646 \u06a9\u06cc\u0641\u06cc\u062a\u060c \u0627\u0633\u062a\u0627\u0646\u062f\u0627\u0631\u062f\u0647\u0627\u06cc \u0645\u0647\u0646\u062f\u0633\u06cc \u0648 \u062f\u0642\u062a \u067e\u06cc\u0627\u062f\u0647\u200c\u0633\u0627\u0632\u06cc \u0648 \u06cc\u06a9\u067e\u0627\u0631\u0686\u0647\u200c\u0633\u0627\u0632\u06cc \u0634\u062f\u0646\u062f:\n\n---\n\n### \ud83d\udee0\ufe0f \u06af\u0632\u0627\u0631\u0634 \u062c\u0627\u0645\u0639 \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a \u0648 \u0642\u0627\u0628\u0644\u06cc\u062a\u200c\u0647\u0627\u06cc \u067e\u06cc\u0627\u062f\u0647\u200c\u0633\u0627\u0632\u06cc\u200c\u0634\u062f\u0647\n\n#### \u06f1. \u0627\u0633\u062a\u0631\u0627\u062a\u0698\u06cc \u062f\u0648 \u0634\u0627\u062e\u0647\u200c\u0627\u06cc \u06af\u06cc\u062a (`dev` \u0648 `production`)\n* **\u0645\u0642\u062f\u0627\u0631\u062f\u0647\u06cc \u0627\u0648\u0644\u06cc\u0647 \u062f\u0648 \u0634\u0627\u062e\u0647\u200c\u0627\u06cc:** \u062f\u0631 \u0645\u062a\u062f `init_project_repo`\u060c \u0647\u0631 \u067e\u0631\u0648\u0698\u0647 \u0628\u0627 \u062f\u0648 \u0634\u0627\u062e\u0647 `production` (\u0634\u0627\u062e\u0647 \u067e\u0627\u06cc\u0647 \u0648 \u0639\u0645\u0644\u06cc\u0627\u062a\u06cc) \u0648 `dev` (\u0634\u0627\u062e\u0647 \u0641\u0639\u0627\u0644 \u062a\u0648\u0633\u0639\u0647) \u0627\u06cc\u062c\u0627\u062f \u0648 \u0628\u0647 Gitea \u067e\u0648\u0634 \u0645\u06cc\u200c\u0634\u0648\u062f. \u0634\u0627\u062e\u0647 \u067e\u06cc\u0634\u200c\u0641\u0631\u0636 \u06a9\u0627\u0631\u06cc \u0647\u0645\u0648\u0627\u0631\u0647 `dev` \u0627\u0633\u062a.\n* **\u062a\u0648\u0633\u0639\u0647 \u0648 \u06a9\u0627\u0645\u06cc\u062a \u062e\u0648\u062f\u06a9\u0627\u0631 \u0631\u0648\u06cc `dev`:** \u062a\u0645\u0627\u0645\u06cc \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a\u060c \u06a9\u0627\u0645\u06cc\u062a\u200c\u0647\u0627 \u0648 \u0647\u0645\u06af\u0627\u0645\u200c\u0633\u0627\u0632\u06cc\u200c\u0647\u0627\u06cc \u0631\u0648\u0632\u0645\u0631\u0647 \u0647\u0648\u0634 \u0645\u0635\u0646\u0648\u0639\u06cc \u0648 \u06a9\u0627\u0631\u0628\u0631 \u0631\u0648\u06cc \u0634\u0627\u062e\u0647 `dev` \u062b\u0628\u062a \u0645\u06cc\u200c\u0634\u0648\u0646\u062f.\n* **\u0633\u06cc\u0633\u062a\u0645 \u0627\u0646\u062a\u0634\u0627\u0631 \u0648 \u0627\u062f\u063a\u0627\u0645 (`git_publish`):**\n * \u0645\u062a\u062f \u0627\u062e\u062a\u0635\u0627\u0635\u06cc `git_publish` \u0637\u0631\u0627\u062d\u06cc \u0634\u062f \u06a9\u0647 \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a \u0645\u0639\u0644\u0642 `dev` \u0631\u0627 \u06a9\u0627\u0645\u06cc\u062a \u06a9\u0631\u062f\u0647\u060c \u0634\u0627\u062e\u0647 `production` \u0631\u0627 \u062a\u062d\u0648\u06cc\u0644 \u06af\u0631\u0641\u062a\u0647\u060c \u0622\u062e\u0631\u06cc\u0646 \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a `dev` \u0631\u0627 \u0628\u0627 `production` \u0645\u0631\u062c \u0646\u0645\u0648\u062f\u0647\u060c \u062f\u0631 Gitea \u067e\u0648\u0634 \u0645\u06cc\u200c\u06a9\u0646\u062f \u0648 \u0645\u062c\u062f\u062f\u0627\u064b \u0628\u0647 \u0634\u0627\u062e\u0647 `dev` \u0628\u0627\u0632\u0645\u06cc\u200c\u06af\u0631\u062f\u062f.\n * \u062f\u0633\u062a\u0648\u0631 \u062a\u0644\u06af\u0631\u0627\u0645\u06cc \u062c\u062f\u06cc\u062f `/publish` \u0648 \u0627\u06a9\u0634\u0646\u200c\u062a\u06af `[[GIT_PUBLISH: project=\"...\"]]` \u0627\u0636\u0627\u0641\u0647 \u0634\u062f\u0646\u062f.\n\n---\n\n#### \u06f2. \u0645\u0627\u0698\u0648\u0644 \u0627\u0633\u062a\u0642\u0631\u0627\u0631 \u0647\u0648\u0634\u0645\u0646\u062f \u0648 \u062a\u0645\u06cc\u0632 FTP (`ftp_manager.py`)\n* **\u0641\u06cc\u0644\u062a\u0631 \u0647\u0648\u0634\u0645\u0646\u062f \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0627\u0636\u0627\u0641\u06cc (Clean AI Deployment):**\n * \u0644\u06cc\u0633\u062a \u0641\u06cc\u0644\u062a\u0631 \u067e\u06cc\u0634\u200c\u0641\u0631\u0636 \u062c\u0627\u0645\u0639 \u0634\u0627\u0645\u0644:\n * \u0645\u062e\u0632\u0646 \u0648 \u0645\u062a\u0627\u062f\u06cc\u062a\u0627\u06cc \u06af\u06cc\u062a: `.git/`\u060c `.gitignore`\u060c `.gitattributes`\n * \u0645\u062a\u063a\u06cc\u0631\u0647\u0627 \u0648 \u06a9\u0644\u06cc\u062f\u0647\u0627\u06cc \u0645\u062d\u0644\u06cc: `.env*`\u060c `*.pem`\u060c `*.key`\n * \u062f\u06cc\u062a\u0627\u06cc \u0645\u0648\u0642\u062a \u0648 \u0633\u0634\u0646\u200c\u0647\u0627: `sessions_data/`\u060c `uploads_temp/`\u060c `.agents/`\u060c `.gemini/`\n * \u06a9\u0634 \u0648 \u0628\u06cc\u0644\u062f\u0647\u0627: `__pycache__/`\u060c `*.pyc`\u060c `.venv/`\u060c `node_modules/`\n * \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0633\u06cc\u0633\u062a\u0645\u06cc \u0648 \u0644\u0627\u06af: `*.log`\u060c `.DS_Store`\u060c `Thumbs.db`\u060c `.idea/`\u060c `.vscode/`\n* **\u0645\u062f\u06cc\u0631\u06cc\u062a \u0633\u0627\u062e\u062a\u0627\u0631 \u0631\u06cc\u0645\u0648\u062a \u0648 \u0622\u067e\u0644\u0648\u062f \u0647\u0648\u0634\u0645\u0646\u062f:**\n * \u0627\u06cc\u062c\u0627\u062f \u062e\u0648\u062f\u06a9\u0627\u0631 \u062f\u0627\u06cc\u0631\u06a9\u062a\u0648\u0631\u06cc\u200c\u0647\u0627\u06cc \u062a\u0648 \u062f\u0631 \u062a\u0648 \u062f\u0631 \u0633\u0631\u0648\u0631 \u0645\u0642\u0635\u062f\n * \u0645\u062d\u0627\u0633\u0628\u0647 \u062d\u062c\u0645\u060c \u062a\u0639\u062f\u0627\u062f \u0641\u0627\u06cc\u0644\u200c\u0647\u0627 \u0648 \u0645\u062f\u062a \u0632\u0645\u0627\u0646 \u0627\u0646\u062a\u0642\u0627\u0644\n * \u0645\u062a\u062f `test_connection` \u0628\u0631\u0627\u06cc \u0627\u0639\u062a\u0628\u0627\u0631\u0633\u0646\u062c\u06cc \u0627\u062a\u0635\u0627\u0644 \u0642\u0628\u0644 \u0627\u0632 \u0627\u0642\u062f\u0627\u0645 \u0628\u0647 \u062f\u06cc\u067e\u0644\u0648\u06cc.\n\n---\n\n#### \u06f3. \u06cc\u06a9\u067e\u0627\u0631\u0686\u0647\u200c\u0633\u0627\u0632\u06cc \u0645\u062f\u0644 \u062f\u0627\u062f\u0647\u060c \u0627\u06a9\u0634\u0646\u200c\u062a\u06af\u200c\u0647\u0627 \u0648 \u0631\u0627\u0628\u0637 \u06a9\u0627\u0631\u0628\u0631\u06cc \u0631\u0628\u0627\u062a\n* **\u0627\u0631\u062a\u0642\u0627\u06cc \u0645\u062f\u0644 `Project` \u0648 \u0630\u062e\u06cc\u0631\u0647\u200c\u0633\u0627\u0632\u06cc:** \u0641\u06cc\u0644\u062f\u0647\u0627\u06cc `ftp_host`\u060c `ftp_port`\u060c `ftp_user`\u060c `ftp_password`\u060c `ftp_path`\u060c `ftp_tls` \u0648 `active_branch` \u0628\u0647 \u06a9\u0644\u0627\u0633 \u067e\u0631\u0648\u0698\u0647 \u0627\u0636\u0627\u0641\u0647 \u0634\u062f\u0646\u062f.\n* **\u0627\u06a9\u0634\u0646\u200c\u062a\u06af\u200c\u0647\u0627\u06cc \u062c\u062f\u06cc\u062f \u0647\u0648\u0634 \u0645\u0635\u0646\u0648\u0639\u06cc \u062f\u0631 `bot_actions.py`:**\n * `[[GIT_PUBLISH: project=\"\", message=\"\"]]`\n * `[[FTP_CONFIG: host=\"...\", port=21, user=\"...\", password=\"...\", path=\"...\", tls=false]]`\n * `[[FTP_TEST: project=\"\"]]`\n * `[[FTP_DEPLOY: project=\"\", branch=\"production\"]]`\n* **\u0645\u0646\u0648 \u0648 \u062f\u06a9\u0645\u0647\u200c\u0647\u0627\u06cc \u062a\u0644\u06af\u0631\u0627\u0645 \u062f\u0631 `bot.py`:**\n * \u062f\u0633\u062a\u0648\u0631 \u062c\u062f\u06cc\u062f `/ftp` \u0648 \u0645\u0646\u0648\u06cc \u0634\u06cc\u0634\u0647\u200c\u0627\u06cc **\u00ab\ud83d\ude80 \u062f\u06cc\u067e\u0644\u0648\u06cc FTP\u00bb** \u0648 **\u00ab\ud83d\udd0d \u062a\u0633\u062a \u0627\u062a\u0635\u0627\u0644 FTP\u00bb**\n * \u062f\u06a9\u0645\u0647 \u062c\u062f\u06cc\u062f **\u00ab\ud83d\ude80 \u0627\u0646\u062a\u0634\u0627\u0631 \u0628\u0647 \u067e\u0631\u0648\u062f\u0627\u06a9\u0634\u0646 (Publish)\u00bb** \u062f\u0631 \u0645\u0646\u0648\u06cc \u06af\u06cc\u062a (`/git`)\n * \u062a\u0633\u062a \u0633\u06cc\u0646\u062a\u06a9\u0633 \u0641\u0627\u06cc\u0644\u200c\u0647\u0627 \u0628\u0627 \u0645\u0648\u0641\u0642\u06cc\u062a \u06a9\u0627\u0645\u0644 \u0627\u0646\u062c\u0627\u0645 \u0634\u062f \u0648 \u0631\u0628\u0627\u062a \u0628\u0627 \u062a\u06af `[[RESTART_BOT]]` \u0628\u0647 \u0635\u0648\u0631\u062a \u0631\u0648\u0627\u0646 \u0645\u062c\u062f\u062f\u0627\u064b \u0631\u0627\u0647\u200c\u0627\u0646\u062f\u0627\u0632\u06cc \u0634\u062f.\n\n---\n\n[[BUTTONS: layout=[\n [{\"text\": \"\ud83d\udc19 \u0645\u0646\u0648\u06cc \u06af\u06cc\u062a \u0648 \u0628\u0631\u0646\u0686\u200c\u0647\u0627\", \"cmd\": \"/git\"}, {\"text\": \"\ud83d\ude80 \u062a\u0646\u0638\u06cc\u0645\u0627\u062a \u0648 \u062f\u06cc\u067e\u0644\u0648\u06cc FTP\", \"cmd\": \"/ftp\"}],\n [{\"text\": \"\ud83d\ude80 \u067e\u0627\u0628\u0644\u06cc\u0634 \u0628\u0647 \u067e\u0631\u0648\u062f\u0627\u06a9\u0634\u0646\", \"cmd\": \"/publish\"}, {\"text\": \"\ud83d\udcc1 \u0644\u06cc\u0633\u062a \u067e\u0631\u0648\u0698\u0647\u200c\u0647\u0627\", \"cmd\": \"/projects\"}]\n]]]\n\n\u2022 \u0646\u0627\u0645 \u067e\u0631\u0648\u0698\u0647: default\n\u2022 \u0622\u062f\u0631\u0633 \u0645\u062e\u0632\u0646: https://git.msa.artacloud.ir/root/default\n", "last_status_msg_id": null, "restart_pending": false, "last_delivered": true, - "last_update_time": 1788102148.5607734, + "last_update_time": 1788102417.7547705, "workspace": "/root", "conversation_id": "e58b6c34-0e86-4237-aef5-14699b99f75c", "model": "gemini-3.7-flash-auto", "effort": "high", "language": "fa", - "last_context_length": 10997, - "last_total_tokens": 93829 + "last_context_length": 44953, + "last_total_tokens": 488556 }, "232801087": { "chat_id": 232801087,