AI Update: فاز ۱ و ۲ نقشه راه را روی ربات تلگرام پیادهسازی کن
This commit is contained in:
+131
-1
@@ -320,6 +320,7 @@ async def build_git_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("📜 تاریخچه ۲۰ کامیت اخیر", callback_data="git_hist:1"),
|
||||
InlineKeyboardButton("↩️ لغو آخرین تغییر (Undo)", callback_data="git_ask_undo_head"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("🔄 همگامسازی گیت (Sync)", callback_data="btn_git_sync"),
|
||||
@@ -341,6 +342,7 @@ async def build_git_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("📜 Recent Commits (History)", callback_data="git_hist:1"),
|
||||
InlineKeyboardButton("↩️ Undo Last Change", callback_data="git_ask_undo_head"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("🔄 Git Sync", callback_data="btn_git_sync"),
|
||||
@@ -454,13 +456,15 @@ async def build_git_commit_detail_menu(chat_id: int, commit_hash: str) -> tuple[
|
||||
if not is_curr:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton("🔀 سوییچ به این کامیت (Checkout)" if is_fa else "🔀 Switch to this commit (Checkout)", callback_data=f"git_co:{short_h}"),
|
||||
InlineKeyboardButton("↩️ لغو این کامیت (Revert)" if is_fa else "↩️ Revert This Commit", callback_data=f"git_ask_revert:{short_h}"),
|
||||
])
|
||||
keyboard.append([
|
||||
InlineKeyboardButton("⏪ بازگردانی پروژه به این کامیت (Reset)" if is_fa else "⏪ Restore Project to this Commit (Reset)", callback_data=f"git_ask_reset:{short_h}"),
|
||||
])
|
||||
else:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton("✅ این کامیت هماکنون فعال است" if is_fa else "✅ Currently Active Commit", callback_data=f"git_view:{short_h}"),
|
||||
InlineKeyboardButton("↩️ لغو تغییرات این کامیت (Revert)" if is_fa else "↩️ Revert This Commit", callback_data=f"git_ask_revert:{short_h}"),
|
||||
InlineKeyboardButton("✅ کامیت فعال فعلی" if is_fa else "✅ Currently Active Commit", callback_data=f"git_view:{short_h}"),
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
@@ -2089,6 +2093,11 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
f"• <code>/delproject <name></code> - حذف یک پروژه\n"
|
||||
f"• <code>/renameproject <قدیم> <جدید></code> - تغییر نام پروژه\n"
|
||||
f"• <code>/backup [نام]</code> - تهیه فایل Zip و ارسال بکاپ پروژه در پارتهای ۵۰ مگابایتی\n\n"
|
||||
f"<b>🐙 کنترل نسخه و لغو تغییرات (Git & Undo):</b>\n"
|
||||
f"• <code>/git</code> یا <code>/repo</code> - داشبورد مدیریت گیت و مشاهده وضعیت مخزن Gitea\n"
|
||||
f"• <code>/sync</code> - همگامسازی سریع با مخزن (Pull & Push)\n"
|
||||
f"• <code>/commit [پیام]</code> - ثبت و ارسال دستی کامیت\n"
|
||||
f"• <code>/undo [hash]</code> یا <code>/revert</code> - لغو امن آخرین تغییر (HEAD) یا یک کامیت مشخص از گذشته\n\n"
|
||||
f"<b>⏰ زمانبندی تسکها (Scheduled Tasks & Cron):</b>\n"
|
||||
f"• <code>/tasks</code> یا <code>/schedule</code> - کنترلپنل تعاملی تسکهای زمانبندی شده\n"
|
||||
f"• <code>/schedule <زمان> | <پرامپت/دستور></code> - ایجاد و زمانبندی تسک جدید\n"
|
||||
@@ -3343,6 +3352,50 @@ async def commit_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
)
|
||||
await status_msg.edit_text(out, parse_mode=constants.ParseMode.HTML, disable_web_page_preview=True)
|
||||
|
||||
# Command: /undo or /revert
|
||||
@check_auth
|
||||
async def undo_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
|
||||
|
||||
commit_target = context.args[0].strip() if context.args else "HEAD"
|
||||
status_msg = await update.message.reply_html("↩️ <i>در حال لغو تغییرات (Git Revert)...</i>" if is_fa else "↩️ <i>Reverting commit...</i>")
|
||||
|
||||
ok, res_msg, extra = await git_manager.git_revert(curr_proj.workspace, commit_target=commit_target)
|
||||
urls = git_manager.get_repo_urls(curr_proj.name)
|
||||
|
||||
buttons = [
|
||||
[
|
||||
InlineKeyboardButton("📜 تاریخچه کامیتها" if is_fa else "📜 Commit History", callback_data="git_hist:1"),
|
||||
InlineKeyboardButton("🐙 منوی گیت" if is_fa else "🐙 Git Menu", callback_data="btn_git_menu"),
|
||||
]
|
||||
]
|
||||
|
||||
if is_fa:
|
||||
out = (
|
||||
f"↩️ <b>گزارش لغو تغییرات (Git Undo / Revert):</b>\n\n"
|
||||
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||||
f"• 🎯 <b>کامیت هدف:</b> <code>{escape_html(commit_target)}</code>\n"
|
||||
f"• 🌐 <b>مخزن:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
|
||||
f"{res_msg}"
|
||||
)
|
||||
else:
|
||||
out = (
|
||||
f"↩️ <b>Git Undo / Revert Report:</b>\n\n"
|
||||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||||
f"• 🎯 <b>Target Commit:</b> <code>{escape_html(commit_target)}</code>\n"
|
||||
f"• 🌐 <b>Repository:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
|
||||
f"{res_msg}"
|
||||
)
|
||||
await status_msg.edit_text(out, parse_mode=constants.ParseMode.HTML, disable_web_page_preview=True, reply_markup=InlineKeyboardMarkup(buttons))
|
||||
|
||||
# Command: /model
|
||||
@check_auth
|
||||
async def model_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
@@ -4566,6 +4619,82 @@ 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 == "git_ask_undo_head":
|
||||
confirm_text = (
|
||||
f"⚠️ <b>تأیید لغو آخرین تغییرات (Git Revert HEAD)</b>\n\n"
|
||||
f"آیا مطمئن هستید که میخواهید آخرین کامیت ثبتشده در پروژه <code>{escape_html(curr_proj.name if curr_proj else '')}</code> را لغو کنید؟\n\n"
|
||||
f"<i>یک کامیت معکوس ایجاد خواهد شد و تاریخچهٔ پروژه با امنیت کامل حفظ میشود.</i>"
|
||||
if is_fa else
|
||||
f"⚠️ <b>Confirm Undo Last Commit (Git Revert HEAD)</b>\n\n"
|
||||
f"Are you sure you want to revert the latest commit on project <code>{escape_html(curr_proj.name if curr_proj else '')}</code>?\n\n"
|
||||
f"<i>A safe reverse commit will be created preserving your Git history.</i>"
|
||||
)
|
||||
confirm_markup = InlineKeyboardMarkup([
|
||||
[
|
||||
InlineKeyboardButton("↩️ بله، لغو شود (Revert)" if is_fa else "↩️ Yes, Revert HEAD", callback_data="git_do_undo_head"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("❌ انصراف و بازگشت" if is_fa else "❌ Cancel", callback_data="btn_git_menu"),
|
||||
],
|
||||
])
|
||||
try:
|
||||
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=confirm_markup)
|
||||
except Exception:
|
||||
await query.message.reply_html(confirm_text, reply_markup=confirm_markup)
|
||||
|
||||
elif data == "git_do_undo_head":
|
||||
await query.answer("↩️ در حال لغو آخرین تغییرات..." if is_fa else "↩️ Reverting latest commit...")
|
||||
if curr_proj:
|
||||
ok, res_msg, extra = await git_manager.git_revert(curr_proj.workspace, commit_target="HEAD")
|
||||
if ok:
|
||||
await query.answer("✅ آخرین تغییر با موفقیت لغو شد." if is_fa else "✅ Successfully reverted latest commit.", show_alert=True)
|
||||
else:
|
||||
await query.answer("❌ " + res_msg, show_alert=True)
|
||||
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.startswith("git_ask_revert:"):
|
||||
commit_hash = data.split(":", 1)[1]
|
||||
confirm_text = (
|
||||
f"⚠️ <b>تأیید لغو کامیت (Git Revert)</b>\n\n"
|
||||
f"آیا میخواهید تغییرات اعمالشده در کامیت <code>{escape_html(commit_hash)}</code> را در پروژه <code>{escape_html(curr_proj.name if curr_proj else '')}</code> لغو کنید؟\n\n"
|
||||
f"<i>تغییرات این کامیت بهصورت یک کامیت معکوس جدید ثبت خواهد شد.</i>"
|
||||
if is_fa else
|
||||
f"⚠️ <b>Confirm Git Revert</b>\n\n"
|
||||
f"Are you sure you want to revert commit <code>{escape_html(commit_hash)}</code> on project <code>{escape_html(curr_proj.name if curr_proj else '')}</code>?\n\n"
|
||||
f"<i>Changes from this commit will be inverted in a new commit.</i>"
|
||||
)
|
||||
confirm_markup = InlineKeyboardMarkup([
|
||||
[
|
||||
InlineKeyboardButton("↩️ بله، لغو شود" if is_fa else "↩️ Yes, Revert", callback_data=f"git_do_revert:{commit_hash}"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("❌ انصراف و بازگشت" if is_fa else "❌ Cancel", callback_data=f"git_view:{commit_hash}"),
|
||||
],
|
||||
])
|
||||
try:
|
||||
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=confirm_markup)
|
||||
except Exception:
|
||||
await query.message.reply_html(confirm_text, reply_markup=confirm_markup)
|
||||
|
||||
elif data.startswith("git_do_revert:"):
|
||||
commit_hash = data.split(":", 1)[1]
|
||||
await query.answer("↩️ در حال لغو کامیت..." if is_fa else "↩️ Reverting commit...")
|
||||
if curr_proj:
|
||||
ok, res_msg, extra = await git_manager.git_revert(curr_proj.workspace, commit_target=commit_hash)
|
||||
if ok:
|
||||
await query.answer("✅ کامیت با موفقیت لغو شد." if is_fa else "✅ Commit reverted successfully.", show_alert=True)
|
||||
else:
|
||||
await query.answer("❌ " + res_msg, show_alert=True)
|
||||
text, markup = await build_git_commit_detail_menu(chat_id, commit_hash)
|
||||
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.startswith("aibtn:"):
|
||||
btn_id = data.split(":", 1)[1].strip()
|
||||
from bot_actions import get_ai_button_payload
|
||||
@@ -6812,6 +6941,7 @@ def main():
|
||||
application.add_handler(CommandHandler(["git", "repo", "repository", "gitea"], git_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))
|
||||
|
||||
# Scheduled Tasks Handlers
|
||||
application.add_handler(CommandHandler(["tasks", "task", "schedule", "schedules", "cron", "timer"], tasks_command))
|
||||
|
||||
@@ -1362,7 +1362,36 @@ async def execute_action(
|
||||
return badge, side_effects, created_task
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 30. SAVE_MEMORY / STORE_MEMORY / SET_MEMORY
|
||||
# 30. GIT_REVERT / GIT_UNDO
|
||||
# -------------------------------------------------------------
|
||||
elif act in ("GIT_REVERT", "REVERT_GIT", "GIT_UNDO", "UNDO_GIT", "UNDO"):
|
||||
p_name = attrs.get("project") or attrs.get("name") or current_project_name
|
||||
proj = session.projects.get(p_name) if p_name else curr_proj
|
||||
if not proj:
|
||||
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
|
||||
|
||||
target = attrs.get("commit") or attrs.get("target") or attrs.get("_default") or "HEAD"
|
||||
from git_manager import git_manager
|
||||
ok, msg, extra = await git_manager.git_revert(proj.workspace, commit_target=target)
|
||||
|
||||
if ok:
|
||||
badge = (
|
||||
f"\n\n↩️ <b>عملیات لغو تغییرات (Git Revert) با موفقیت انجام شد:</b>\n"
|
||||
f"• 📁 <b>پروژه:</b> <code>{escape_html(proj.name)}</code>\n"
|
||||
f"• 🎯 <b>کامیت هدف:</b> <code>{escape_html(target)}</code>\n"
|
||||
f"• 📋 {msg}"
|
||||
if is_fa else
|
||||
f"\n\n↩️ <b>Git Revert Succeeded:</b>\n"
|
||||
f"• 📁 <b>Project:</b> <code>{escape_html(proj.name)}</code>\n"
|
||||
f"• 🎯 <b>Target Commit:</b> <code>{escape_html(target)}</code>\n"
|
||||
f"• 📋 {msg}"
|
||||
)
|
||||
else:
|
||||
badge = f"\n⚠️ خطا در لغو کامیت: {msg}"
|
||||
return badge, side_effects, created_task
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 31. SAVE_MEMORY / STORE_MEMORY / SET_MEMORY
|
||||
# -------------------------------------------------------------
|
||||
elif act in ("SAVE_MEMORY", "STORE_MEMORY", "SET_MEMORY", "ADD_MEMORY"):
|
||||
from memory_manager import memory_manager
|
||||
|
||||
@@ -486,6 +486,112 @@ class GitManager:
|
||||
logger.error(f"Error resetting to {commit_hash} in {ws}: {e}")
|
||||
return False, f"خطا در بازگردانی به کامیت {commit_hash}: {e}"
|
||||
|
||||
async def git_revert(
|
||||
self,
|
||||
workspace_path: str,
|
||||
commit_target: str = "HEAD",
|
||||
strategy: Optional[str] = None,
|
||||
) -> Tuple[bool, str, Dict[str, Any]]:
|
||||
"""
|
||||
Reverts the specified commit (or HEAD) cleanly.
|
||||
Attempts deterministic Git revert first without using AI tokens.
|
||||
If conflicts occur, detects them and safely aborts to keep working tree clean.
|
||||
"""
|
||||
ws = Path(workspace_path).expanduser().resolve()
|
||||
if not (ws / ".git").exists():
|
||||
return False, "مسیر مشخصشده یک مخزن گیت نیست.", {}
|
||||
|
||||
try:
|
||||
# 1. Check dirty files, stash if necessary
|
||||
porcelain = await self._run_cmd(["git", "status", "--porcelain"], cwd=str(ws))
|
||||
has_stash = False
|
||||
if porcelain.strip():
|
||||
try:
|
||||
await self._run_cmd(["git", "stash", "save", "Auto-stash before revert"], cwd=str(ws))
|
||||
has_stash = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Get target commit info before revert
|
||||
target_info = (await self._run_cmd(["git", "log", "-1", "--format=%h - %s", commit_target], cwd=str(ws))).strip()
|
||||
|
||||
# 3. Attempt revert
|
||||
cmd = ["git", "revert", "--no-edit", commit_target]
|
||||
if strategy:
|
||||
cmd.extend(["--strategy", strategy])
|
||||
|
||||
try:
|
||||
out = await self._run_cmd(cmd, cwd=str(ws))
|
||||
except Exception as e:
|
||||
# Check for conflicts
|
||||
status_out = await self._run_cmd(["git", "status", "--porcelain"], cwd=str(ws))
|
||||
conflicted_files = []
|
||||
for line in status_out.splitlines():
|
||||
line_s = line.strip()
|
||||
if line_s.startswith(("UU ", "AA ", "UD ", "DU ", "U ", "DD ")):
|
||||
parts = line_s.split(maxsplit=1)
|
||||
if len(parts) > 1:
|
||||
conflicted_files.append(parts[1])
|
||||
|
||||
if conflicted_files:
|
||||
# Abort revert to leave working directory clean
|
||||
try:
|
||||
await self._run_cmd(["git", "revert", "--abort"], cwd=str(ws))
|
||||
except Exception:
|
||||
pass
|
||||
if has_stash:
|
||||
try:
|
||||
await self._run_cmd(["git", "stash", "pop"], cwd=str(ws))
|
||||
except Exception:
|
||||
pass
|
||||
file_list_str = "\n".join(f"• <code>{f}</code>" for f in conflicted_files)
|
||||
return False, f"⚠️ تداخل (Conflict) در لغو کامیت رخ داد:\n{file_list_str}\n\n<i>فرآیند لغو متوقف شد تا کدها بدون دستکاری باقی بمانند.</i>", {
|
||||
"conflict": True,
|
||||
"conflicted_files": conflicted_files,
|
||||
"target": commit_target,
|
||||
}
|
||||
|
||||
# If other revert error, abort
|
||||
try:
|
||||
await self._run_cmd(["git", "revert", "--abort"], cwd=str(ws))
|
||||
except Exception:
|
||||
pass
|
||||
if has_stash:
|
||||
try:
|
||||
await self._run_cmd(["git", "stash", "pop"], cwd=str(ws))
|
||||
except Exception:
|
||||
pass
|
||||
return False, f"خطا در اجرای revert: {e}", {}
|
||||
|
||||
# 4. Push to remote Gitea
|
||||
try:
|
||||
await self._run_cmd(["git", "push", "origin", "main"], cwd=str(ws))
|
||||
except Exception as pe:
|
||||
logger.warning(f"Failed to push after revert in {ws}: {pe}")
|
||||
|
||||
# 5. Pop stash if we stashed earlier
|
||||
if has_stash:
|
||||
try:
|
||||
await self._run_cmd(["git", "stash", "pop"], cwd=str(ws))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 6. New commit log
|
||||
new_log = (await self._run_cmd(["git", "log", "-1", "--format=%h - %s"], cwd=str(ws))).strip()
|
||||
return True, f"✅ تغییرات کامیت <code>{target_info}</code> با موفقیت لغو شد (Reverted).\nکامیت جدید ثبت و پوش شد:\n<code>{new_log}</code>", {
|
||||
"success": True,
|
||||
"revert_log": new_log,
|
||||
"target_info": target_info,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reverting {commit_target} in {ws}: {e}")
|
||||
return False, f"❌ خطا در لغو کامیت: {e}", {}
|
||||
|
||||
async def git_undo_last_action(self, workspace_path: str) -> Tuple[bool, str, Dict[str, Any]]:
|
||||
"""Undoes the last commit by reverting HEAD."""
|
||||
return await self.git_revert(workspace_path, commit_target="HEAD")
|
||||
|
||||
async def _run_cmd(self, cmd: List[str], cwd: str) -> str:
|
||||
"""Helper to run a shell command asynchronously."""
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user