feat(bot): support concurrent drafts and reply-based edits with error resilience
This commit is contained in:
@@ -90,6 +90,17 @@ class BaleClient:
|
|||||||
payload.pop("parse_mode", None)
|
payload.pop("parse_mode", None)
|
||||||
return await self._call("editMessageText", payload)
|
return await self._call("editMessageText", payload)
|
||||||
|
|
||||||
|
async def delete_message(self, chat_id: int, message_id: int) -> bool:
|
||||||
|
payload: dict = {"chat_id": chat_id, "message_id": message_id}
|
||||||
|
try:
|
||||||
|
return bool(await self._call("deleteMessage", payload))
|
||||||
|
except BaleError as exc:
|
||||||
|
logger.warning("deleteMessage failed for chat %s msg %s: %s", chat_id, message_id, exc)
|
||||||
|
return False
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("unexpected error in deleteMessage for chat %s msg %s: %s", chat_id, message_id, exc)
|
||||||
|
return False
|
||||||
|
|
||||||
async def answer_callback_query(self, callback_query_id: str, text: str | None = None) -> None:
|
async def answer_callback_query(self, callback_query_id: str, text: str | None = None) -> None:
|
||||||
payload: dict = {"callback_query_id": callback_query_id}
|
payload: dict = {"callback_query_id": callback_query_id}
|
||||||
if text:
|
if text:
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ DDL = [
|
|||||||
missing JSONB,
|
missing JSONB,
|
||||||
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||||
source VARCHAR(16),
|
source VARCHAR(16),
|
||||||
|
source_message_id BIGINT,
|
||||||
review_message_id BIGINT,
|
review_message_id BIGINT,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
@@ -41,6 +42,9 @@ DDL = [
|
|||||||
"CREATE INDEX IF NOT EXISTS ix_bot_drafts_chat_id ON bot_drafts (chat_id)",
|
"CREATE INDEX IF NOT EXISTS ix_bot_drafts_chat_id ON bot_drafts (chat_id)",
|
||||||
"CREATE INDEX IF NOT EXISTS ix_bot_drafts_chat_status ON bot_drafts (chat_id, status)",
|
"CREATE INDEX IF NOT EXISTS ix_bot_drafts_chat_status ON bot_drafts (chat_id, status)",
|
||||||
"ALTER TABLE bot_drafts ADD COLUMN IF NOT EXISTS last_bale_update_at TIMESTAMPTZ",
|
"ALTER TABLE bot_drafts ADD COLUMN IF NOT EXISTS last_bale_update_at TIMESTAMPTZ",
|
||||||
|
"ALTER TABLE bot_drafts ADD COLUMN IF NOT EXISTS source_message_id BIGINT",
|
||||||
|
"CREATE INDEX IF NOT EXISTS ix_bot_drafts_review_msg ON bot_drafts (review_message_id)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS ix_bot_drafts_source_msg ON bot_drafts (source_message_id)",
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS bot_messages (
|
CREATE TABLE IF NOT EXISTS bot_messages (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|||||||
@@ -29,15 +29,16 @@ KIND_MATERIAL = "material"
|
|||||||
# Store
|
# Store
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async def create(chat_id: int, kind: str, payload: dict, source: str | None = None) -> BotDraft:
|
async def create(chat_id: int, kind: str, payload: dict, source: str | None = None,
|
||||||
|
source_message_id: int | None = None) -> BotDraft:
|
||||||
async with SessionLocal() as session:
|
async with SessionLocal() as session:
|
||||||
# one active draft per (chat, kind)
|
draft = BotDraft(
|
||||||
await session.execute(
|
chat_id=chat_id,
|
||||||
update(BotDraft)
|
kind=kind,
|
||||||
.where(BotDraft.chat_id == chat_id, BotDraft.kind == kind, BotDraft.status == "active")
|
payload=payload,
|
||||||
.values(status="superseded")
|
source=source,
|
||||||
|
source_message_id=source_message_id,
|
||||||
)
|
)
|
||||||
draft = BotDraft(chat_id=chat_id, kind=kind, payload=payload, source=source)
|
|
||||||
session.add(draft)
|
session.add(draft)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(draft)
|
await session.refresh(draft)
|
||||||
@@ -53,6 +54,22 @@ async def get_active(chat_id: int, kind: str | None = None) -> BotDraft | None:
|
|||||||
return (await session.execute(stmt)).scalar_one_or_none()
|
return (await session.execute(stmt)).scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_by_message_id(chat_id: int, message_id: int) -> BotDraft | None:
|
||||||
|
"""Find the active draft associated with a given message_id (review card or user source message)."""
|
||||||
|
async with SessionLocal() as session:
|
||||||
|
stmt = (
|
||||||
|
select(BotDraft)
|
||||||
|
.where(
|
||||||
|
BotDraft.chat_id == chat_id,
|
||||||
|
BotDraft.status == "active",
|
||||||
|
(BotDraft.review_message_id == message_id) | (BotDraft.source_message_id == message_id),
|
||||||
|
)
|
||||||
|
.order_by(BotDraft.updated_at.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
return (await session.execute(stmt)).scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
async def get_by_prefix(draft_prefix: str, chat_id: int | None = None) -> BotDraft | None:
|
async def get_by_prefix(draft_prefix: str, chat_id: int | None = None) -> BotDraft | None:
|
||||||
"""Resolve the short id used in callback_data (first 8 hex of the UUID)."""
|
"""Resolve the short id used in callback_data (first 8 hex of the UUID)."""
|
||||||
async with SessionLocal() as session:
|
async with SessionLocal() as session:
|
||||||
@@ -63,12 +80,20 @@ async def get_by_prefix(draft_prefix: str, chat_id: int | None = None) -> BotDra
|
|||||||
|
|
||||||
|
|
||||||
async def save_payload(draft: BotDraft, missing: dict | None = None,
|
async def save_payload(draft: BotDraft, missing: dict | None = None,
|
||||||
review_message_id: int | None = None) -> None:
|
review_message_id: int | None = None,
|
||||||
|
source_message_id: int | None = None) -> None:
|
||||||
async with SessionLocal() as session:
|
async with SessionLocal() as session:
|
||||||
|
values: dict = {
|
||||||
|
"payload": draft.payload,
|
||||||
|
"missing": missing,
|
||||||
|
"review_message_id": review_message_id or draft.review_message_id,
|
||||||
|
}
|
||||||
|
if source_message_id:
|
||||||
|
values["source_message_id"] = source_message_id
|
||||||
await session.execute(
|
await session.execute(
|
||||||
update(BotDraft)
|
update(BotDraft)
|
||||||
.where(BotDraft.id == draft.id)
|
.where(BotDraft.id == draft.id)
|
||||||
.values(payload=draft.payload, missing=missing, review_message_id=review_message_id or draft.review_message_id)
|
.values(**values)
|
||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -75,18 +75,36 @@ async def handle_message(ctx: BotContext, chat_id: int, message: dict) -> None:
|
|||||||
if user is None:
|
if user is None:
|
||||||
return # gate either started linking or is mid-linking
|
return # gate either started linking or is mid-linking
|
||||||
|
|
||||||
draft = await draft_store.get_active(chat_id)
|
message_id = message.get("message_id")
|
||||||
draft_summary = _draft_summary(draft)
|
reply_to = message.get("reply_to_message")
|
||||||
|
reply_to_msg_id = reply_to.get("message_id") if reply_to else None
|
||||||
|
|
||||||
|
target_draft = None
|
||||||
|
if reply_to_msg_id is not None:
|
||||||
|
target_draft = await draft_store.get_by_message_id(chat_id, reply_to_msg_id)
|
||||||
|
if target_draft is None:
|
||||||
|
await ctx.send(
|
||||||
|
chat_id,
|
||||||
|
"⚠️ پیشنویس فعال مربوط به این پیام یافت نشد (ممکن است ثبت نهایی، منقضی یا لغو شده باشد).\nبرای ثبت استعلام جدید، پیام خود را بدون ریپلای ارسال فرمایید.",
|
||||||
|
reply_to=message_id,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
draft_summary = _draft_summary(target_draft) if target_draft else None
|
||||||
result = await understand(settings, text, draft_summary)
|
result = await understand(settings, text, draft_summary)
|
||||||
intent, patch = result.get("intent"), result.get("patch") or {}
|
intent, patch = result.get("intent"), result.get("patch") or {}
|
||||||
logger.info("chat %s intent=%s conf=%s", chat_id, intent, result.get("confidence"))
|
logger.info("chat %s intent=%s conf=%s (is_edit=%s)", chat_id, intent, result.get("confidence"), target_draft is not None)
|
||||||
|
|
||||||
if intent in ("register_quote", "update_draft"):
|
if intent in ("register_quote", "update_draft"):
|
||||||
await quote.handle_entities(ctx, chat_id, user, patch, source)
|
await quote.handle_entities(
|
||||||
|
ctx, chat_id, user, patch, source,
|
||||||
|
incoming_msg_id=message_id,
|
||||||
|
target_draft=target_draft,
|
||||||
|
)
|
||||||
elif intent == "commit_draft":
|
elif intent == "commit_draft":
|
||||||
active = await draft_store.get_active(chat_id)
|
active = target_draft or await draft_store.get_active(chat_id)
|
||||||
if active is None:
|
if active is None:
|
||||||
await ctx.send(chat_id, NOTHING_TO_COMMIT)
|
await ctx.send(chat_id, NOTHING_TO_COMMIT, reply_to=message_id)
|
||||||
elif active.kind == draft_store.KIND_QUOTE:
|
elif active.kind == draft_store.KIND_QUOTE:
|
||||||
await quote.commit(ctx, chat_id, user, active)
|
await quote.commit(ctx, chat_id, user, active)
|
||||||
elif active.kind == draft_store.KIND_SUPPLIER:
|
elif active.kind == draft_store.KIND_SUPPLIER:
|
||||||
@@ -94,15 +112,20 @@ async def handle_message(ctx: BotContext, chat_id: int, message: dict) -> None:
|
|||||||
elif active.kind == draft_store.KIND_MATERIAL:
|
elif active.kind == draft_store.KIND_MATERIAL:
|
||||||
await material.commit(ctx, chat_id, user, active)
|
await material.commit(ctx, chat_id, user, active)
|
||||||
elif intent == "discard_draft":
|
elif intent == "discard_draft":
|
||||||
active = await draft_store.get_active(chat_id)
|
active = target_draft or await draft_store.get_active(chat_id)
|
||||||
if active:
|
if active:
|
||||||
await draft_store.set_status(active.id, "cancelled")
|
await draft_store.set_status(active.id, "cancelled")
|
||||||
ctx.pending.pop(chat_id)
|
if active.review_message_id:
|
||||||
await ctx.send(chat_id, CANCELLED)
|
try:
|
||||||
|
await ctx.delete(chat_id, active.review_message_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
ctx.pending.pop(chat_id, None)
|
||||||
|
await ctx.send(chat_id, CANCELLED, reply_to=message_id)
|
||||||
elif intent == "show_draft":
|
elif intent == "show_draft":
|
||||||
active = await draft_store.get_active(chat_id)
|
active = target_draft or await draft_store.get_active(chat_id)
|
||||||
if active is None:
|
if active is None:
|
||||||
await ctx.send(chat_id, NOTHING_TO_COMMIT)
|
await ctx.send(chat_id, NOTHING_TO_COMMIT, reply_to=message_id)
|
||||||
elif active.kind == draft_store.KIND_QUOTE:
|
elif active.kind == draft_store.KIND_QUOTE:
|
||||||
await quote.render(ctx, chat_id, active, fresh=False)
|
await quote.render(ctx, chat_id, active, fresh=False)
|
||||||
elif active.kind == draft_store.KIND_SUPPLIER:
|
elif active.kind == draft_store.KIND_SUPPLIER:
|
||||||
|
|||||||
@@ -16,18 +16,39 @@ logger = logging.getLogger("bot.quote")
|
|||||||
FIELD_FA = {"sup": "تأمینکننده", "prj": "پروژه"}
|
FIELD_FA = {"sup": "تأمینکننده", "prj": "پروژه"}
|
||||||
|
|
||||||
|
|
||||||
async def handle_entities(ctx, chat_id, user, patch: dict, source: str | None) -> None:
|
async def handle_entities(
|
||||||
draft = await draft_store.get_active(chat_id, KIND_QUOTE)
|
ctx, chat_id, user, patch: dict, source: str | None,
|
||||||
if draft is not None:
|
incoming_msg_id: int | None = None,
|
||||||
|
target_draft = None,
|
||||||
|
) -> None:
|
||||||
|
prev_review_msg_id = None
|
||||||
|
if target_draft is not None:
|
||||||
|
draft = target_draft
|
||||||
|
prev_review_msg_id = draft.review_message_id
|
||||||
payload = draft.payload
|
payload = draft.payload
|
||||||
merge_quote_patch(payload, patch)
|
merge_quote_patch(payload, patch)
|
||||||
|
draft.source_message_id = incoming_msg_id
|
||||||
else:
|
else:
|
||||||
payload = new_quote_payload()
|
payload = new_quote_payload()
|
||||||
merge_quote_patch(payload, patch)
|
merge_quote_patch(payload, patch)
|
||||||
draft = await draft_store.create(chat_id, KIND_QUOTE, payload, source=source)
|
draft = await draft_store.create(
|
||||||
|
chat_id, KIND_QUOTE, payload, source=source, source_message_id=incoming_msg_id
|
||||||
|
)
|
||||||
|
|
||||||
await resolve_payload(ctx, payload)
|
await resolve_payload(ctx, payload)
|
||||||
await draft_store.save_payload(draft, missing={"fields": compute_quote_missing(payload)})
|
await draft_store.save_payload(
|
||||||
await render(ctx, chat_id, draft, fresh=draft.review_message_id is None)
|
draft,
|
||||||
|
missing={"fields": compute_quote_missing(payload)},
|
||||||
|
source_message_id=incoming_msg_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if prev_review_msg_id:
|
||||||
|
try:
|
||||||
|
await ctx.delete(chat_id, prev_review_msg_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Could not delete previous review message %s: %s", prev_review_msg_id, e)
|
||||||
|
|
||||||
|
await render(ctx, chat_id, draft, fresh=True, reply_to=incoming_msg_id)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -172,7 +193,8 @@ def _keyboard(payload: dict, draft_id: str, wizard_url: str | None = None) -> di
|
|||||||
return quote_review_keyboard(prefix, pick_rows, has_partial_option=has_partial, wizard_url=wizard_url)
|
return quote_review_keyboard(prefix, pick_rows, has_partial_option=has_partial, wizard_url=wizard_url)
|
||||||
|
|
||||||
|
|
||||||
async def render(ctx, chat_id, draft, fresh: bool = True, note: str | None = None) -> None:
|
async def render(ctx, chat_id, draft, fresh: bool = True, note: str | None = None,
|
||||||
|
reply_to: int | None = None) -> None:
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
|
||||||
payload = draft.payload
|
payload = draft.payload
|
||||||
@@ -182,10 +204,16 @@ async def render(ctx, chat_id, draft, fresh: bool = True, note: str | None = Non
|
|||||||
text += f"\n\n🖥 مشاهده و تکمیل در ویزارد:\n{wizard_url}"
|
text += f"\n\n🖥 مشاهده و تکمیل در ویزارد:\n{wizard_url}"
|
||||||
keyboard = _keyboard(payload, str(draft.id), wizard_url=wizard_url)
|
keyboard = _keyboard(payload, str(draft.id), wizard_url=wizard_url)
|
||||||
if fresh:
|
if fresh:
|
||||||
message = await ctx.send(chat_id, text, reply_markup=keyboard)
|
message = await ctx.send(chat_id, text, reply_markup=keyboard, reply_to=reply_to)
|
||||||
draft.review_message_id = (message or {}).get("message_id")
|
new_review_id = (message or {}).get("message_id")
|
||||||
await draft_store.save_payload(draft, missing={"fields": missing},
|
if new_review_id:
|
||||||
review_message_id=draft.review_message_id)
|
draft.review_message_id = new_review_id
|
||||||
|
await draft_store.save_payload(
|
||||||
|
draft,
|
||||||
|
missing={"fields": missing},
|
||||||
|
review_message_id=new_review_id,
|
||||||
|
source_message_id=reply_to or draft.source_message_id,
|
||||||
|
)
|
||||||
await draft_store.set_bale_updated_at(draft.id)
|
await draft_store.set_bale_updated_at(draft.id)
|
||||||
else:
|
else:
|
||||||
await ctx.edit(chat_id, draft.review_message_id, text, reply_markup=keyboard)
|
await ctx.edit(chat_id, draft.review_message_id, text, reply_markup=keyboard)
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ class BotDraft(Base):
|
|||||||
"""A prepared-but-not-committed ERP action (quote / supplier / material)."""
|
"""A prepared-but-not-committed ERP action (quote / supplier / material)."""
|
||||||
|
|
||||||
__tablename__ = "bot_drafts"
|
__tablename__ = "bot_drafts"
|
||||||
__table_args__ = (Index("ix_bot_drafts_chat_status", "chat_id", "status"),)
|
__table_args__ = (
|
||||||
|
Index("ix_bot_drafts_chat_status", "chat_id", "status"),
|
||||||
|
Index("ix_bot_drafts_review_msg", "review_message_id"),
|
||||||
|
Index("ix_bot_drafts_source_msg", "source_message_id"),
|
||||||
|
)
|
||||||
|
|
||||||
id: Mapped[str] = _uuid_pk()
|
id: Mapped[str] = _uuid_pk()
|
||||||
chat_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
chat_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||||
@@ -50,6 +54,7 @@ class BotDraft(Base):
|
|||||||
missing: Mapped[dict | None] = mapped_column(JSONB)
|
missing: Mapped[dict | None] = mapped_column(JSONB)
|
||||||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active") # active|committed|cancelled|expired|failed
|
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active") # active|committed|cancelled|expired|failed
|
||||||
source: Mapped[str | None] = mapped_column(String(16)) # text|voice
|
source: Mapped[str | None] = mapped_column(String(16)) # text|voice
|
||||||
|
source_message_id: Mapped[int | None] = mapped_column(BigInteger)
|
||||||
review_message_id: Mapped[int | None] = mapped_column(BigInteger)
|
review_message_id: Mapped[int | None] = mapped_column(BigInteger)
|
||||||
last_bale_update_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
last_bale_update_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||||
created_at: Mapped[datetime] = _timestamptz()
|
created_at: Mapped[datetime] = _timestamptz()
|
||||||
|
|||||||
@@ -117,10 +117,11 @@ class BotContext:
|
|||||||
def admin_chat_ids(self) -> set[int]:
|
def admin_chat_ids(self) -> set[int]:
|
||||||
return settings.admin_chat_ids
|
return settings.admin_chat_ids
|
||||||
|
|
||||||
async def send(self, chat_id: int, text: str, reply_markup: dict | None = None) -> dict:
|
async def send(self, chat_id: int, text: str, reply_markup: dict | None = None,
|
||||||
|
reply_to: int | None = None) -> dict:
|
||||||
"""Send + audit-log every outbound message."""
|
"""Send + audit-log every outbound message."""
|
||||||
try:
|
try:
|
||||||
message = await self.bale.send_message(chat_id, text, reply_markup=reply_markup)
|
message = await self.bale.send_message(chat_id, text, reply_markup=reply_markup, reply_to=reply_to)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("failed to send to chat %s", chat_id)
|
logger.exception("failed to send to chat %s", chat_id)
|
||||||
message = {}
|
message = {}
|
||||||
@@ -137,6 +138,15 @@ class BotContext:
|
|||||||
except Exception:
|
except Exception:
|
||||||
await self.send(chat_id, text, reply_markup=reply_markup)
|
await self.send(chat_id, text, reply_markup=reply_markup)
|
||||||
|
|
||||||
|
async def delete(self, chat_id: int, message_id: int | None) -> bool:
|
||||||
|
if not message_id:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return await self.bale.delete_message(chat_id, message_id)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("failed to delete message %s in chat %s", message_id, chat_id)
|
||||||
|
return False
|
||||||
|
|
||||||
async def _log(self, chat_id: int, direction: str, kind: str, body: str | None = None) -> None:
|
async def _log(self, chat_id: int, direction: str, kind: str, body: str | None = None) -> None:
|
||||||
try:
|
try:
|
||||||
async with SessionLocal() as session:
|
async with SessionLocal() as session:
|
||||||
|
|||||||
@@ -445,6 +445,7 @@ async def ensure_bot_drafts_table(engine: AsyncEngine) -> None:
|
|||||||
missing JSONB,
|
missing JSONB,
|
||||||
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||||
source VARCHAR(16),
|
source VARCHAR(16),
|
||||||
|
source_message_id BIGINT,
|
||||||
review_message_id BIGINT,
|
review_message_id BIGINT,
|
||||||
last_bale_update_at TIMESTAMPTZ,
|
last_bale_update_at TIMESTAMPTZ,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
@@ -455,6 +456,9 @@ async def ensure_bot_drafts_table(engine: AsyncEngine) -> None:
|
|||||||
"CREATE INDEX IF NOT EXISTS ix_bot_drafts_chat_id ON bot_drafts(chat_id)",
|
"CREATE INDEX IF NOT EXISTS ix_bot_drafts_chat_id ON bot_drafts(chat_id)",
|
||||||
"CREATE INDEX IF NOT EXISTS ix_bot_drafts_chat_status ON bot_drafts(chat_id, status)",
|
"CREATE INDEX IF NOT EXISTS ix_bot_drafts_chat_status ON bot_drafts(chat_id, status)",
|
||||||
"ALTER TABLE bot_drafts ADD COLUMN IF NOT EXISTS last_bale_update_at TIMESTAMPTZ",
|
"ALTER TABLE bot_drafts ADD COLUMN IF NOT EXISTS last_bale_update_at TIMESTAMPTZ",
|
||||||
|
"ALTER TABLE bot_drafts ADD COLUMN IF NOT EXISTS source_message_id BIGINT",
|
||||||
|
"CREATE INDEX IF NOT EXISTS ix_bot_drafts_review_msg ON bot_drafts(review_message_id)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS ix_bot_drafts_source_msg ON bot_drafts(source_message_id)",
|
||||||
)
|
)
|
||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
await conn.execute(text(ddl))
|
await conn.execute(text(ddl))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class BotDraft(Base):
|
|||||||
missing = Column(JSONB)
|
missing = Column(JSONB)
|
||||||
status = Column(String(16), nullable=False, default="active")
|
status = Column(String(16), nullable=False, default="active")
|
||||||
source = Column(String(16))
|
source = Column(String(16))
|
||||||
|
source_message_id = Column(BigInteger)
|
||||||
review_message_id = Column(BigInteger)
|
review_message_id = Column(BigInteger)
|
||||||
last_bale_update_at = Column(DateTime(timezone=True))
|
last_bale_update_at = Column(DateTime(timezone=True))
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|||||||
@@ -272,7 +272,6 @@ export function WizardPage() {
|
|||||||
) {
|
) {
|
||||||
lastRemoteUpdatedAtRef.current = draft.updated_at;
|
lastRemoteUpdatedAtRef.current = draft.updated_at;
|
||||||
applyDraftPayloadToForm(draft.payload);
|
applyDraftPayloadToForm(draft.payload);
|
||||||
toast.info("🔄 اصلاحات جدید از ربات بله دریافت و در ویزارد اعمال شد.", { duration: 4000 });
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* خطای موقت شبکه در پولینگ نادیده گرفته میشود */
|
/* خطای موقت شبکه در پولینگ نادیده گرفته میشود */
|
||||||
|
|||||||
Reference in New Issue
Block a user