diff --git a/README.md b/README.md index a592a5b3..319488c6 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,17 @@ AutoTelegramForward v2.3 is engineered with strict, fail-closed microservice bou ## 🚀 What is New in v2.x +- 📡 **Branding Hop with a Channel-Only Gate (A → C → B)**: + Route channel content through your own branding channel so the target's header names it — while messages written by normal users are forwarded directly with their own name (`branding_require_channel`). Configurable from the bot's smart menu, the dashboard's rule editor and the mobile client; the trigger policy (origins, keywords, match mode, forward/channel gates) is one shared decision across the Python engine, the Go worker and the preview, pinned by cross-engine parity fixtures. +- 🔁 **Replace Any Link Found**: + `link_replace_enabled` + a rewrite map with three key shapes: `*` rewrites every link to one target, a domain key rewrites one domain, a URL-prefix key rewrites the deep links it starts with. Applied entity-aware by the link engine in both engines. +- ❕ **Replace Any @ID**: + `mention_rewrite_map` rewrites handles (`@oldshop → @newshop`, or `* → @main` for every mention) in the final replacement pass — after the AI stage, so a rewrite cannot resurrect a replaced handle. Dedicated editor panel in the bot (smart menu → 🔁 Link & @replace maps), registry rows on the dashboard and the mobile settings sheet. +- 🧠 **Prompt Library Management (bot + dashboard + mobile)**: + The global prompt manager (`AI → 🧠 Prompts`) closes the bot's missing surface: list, create, rename, edit the system prompt / user template / description, test against a real provider, and **enable/disable** — a disabled template stays in the library while deliveries fall back to the task's AI-config prompt (`[PIPELINE:AI_REWRITE:PROMPT_OFF]`). The mobile client gains the same library (CRUD + test) over the shared `/api/prompts` contract. +- 🚨 **Branding Failure Trail**: + Hop failures now carry their own event codes (`BRANDING_HOP_FAILED`, `BRANDING_STAGE1_FAILED`, `BRANDING_STAGE2_FAILED`) into the Rust log store and the Log Center, so "the branded route is broken for this task" is one filter instead of a needle in every delivery failure. + - 🧹 **Advanced Content Cleaner & Sanitizer**: Strip links, `@mentions`, `#hashtags`, emojis, and custom regex patterns per rule, with preview and dynamic toggle. - 🧬 **Message Deduplication Engine**: diff --git a/core/application/backup_service.py b/core/application/backup_service.py index aaf6ec16..aacbd725 100644 --- a/core/application/backup_service.py +++ b/core/application/backup_service.py @@ -299,6 +299,9 @@ def _prompt_dict(self, t: PromptTemplate) -> Dict[str, Any]: "current_version": t.current_version, "is_system": t.is_system, "owner_user_id": int(getattr(t, "owner_user_id", 0) or 0), + # The library enable/disable switch rides the backup so a restore + # brings back the same active set of prompts. + "enabled": bool(getattr(t, "enabled", True)), } # ------------------------------------------------------------------ import @@ -521,6 +524,7 @@ async def restore_snapshot( if target_owner_user_id is not None else raw.get("owner_user_id") or 0 ), + enabled=bool(raw.get("enabled", True)), ) await self._prompts.add_template(t) p_stats["imported"] += 1 diff --git a/core/application/pipeline.py b/core/application/pipeline.py index 0fd8c15a..d975d7ef 100644 --- a/core/application/pipeline.py +++ b/core/application/pipeline.py @@ -517,6 +517,16 @@ async def _perform_brand_hop(self, ctx: "PipelineContext", rule: ForwardRule, "[PIPELINE:BRANDING] [%s] Rule %s hop failed (%s) — delivering directly", ctx.correlation_id[:8], rule.id[:8], exc, ) + # Structured error trail: the branded hop is a delivery-stage side + # effect, so its failure is an audit event with its own reason code + # (the Rust log store and the Log Center filter on it) — not just a + # stdout line the operator has to grep for. + await self._audit( + ctx, "forward.branding_failed", "FAILED", severity="ERROR", + reason_code="BRANDING_HOP_FAILED", rule_id=rule.id, + message_id=str(getattr(ctx.payload, "message_id", "") or ""), + result_summary=f"branding hop failed: {type(exc).__name__}", + ) return None if plan: logger.info( @@ -530,6 +540,12 @@ async def _perform_brand_hop(self, ctx: "PipelineContext", rule: ForwardRule, "[PIPELINE:BRANDING] [%s] Rule %s msg %s: nothing posted into C — delivering directly", ctx.correlation_id[:8], rule.id[:8], getattr(ctx.payload, "message_id", "?"), ) + await self._audit( + ctx, "forward.branding_failed", "FAILED", severity="ERROR", + reason_code="BRANDING_HOP_EMPTY", rule_id=rule.id, + message_id=str(getattr(ctx.payload, "message_id", "") or ""), + result_summary="branding hop posted nothing into C", + ) return plan # ----------------------------------------------------------------- # @@ -1526,7 +1542,17 @@ async def _apply_transforms( ) # Link Management Engine (Entity-aware, domain allowlist/blocklist, SSRF protected) + # "Replace any link found": with `link_replace_enabled` on and a + # rewrite map configured, a task with no stricter policy upgrades to + # REWRITE_LINKS so the engine rewrites the links the map names (a `*` + # key rewrites every link) instead of leaving them untouched. link_policy = getattr(rule, "link_policy", "PRESERVE_ALL") + if ( + bool(getattr(rule, "link_replace_enabled", False)) + and (getattr(rule, "link_rewrite_map", None) or {}) + and str(link_policy or "").strip().upper() in ("", "PRESERVE_ALL") + ): + link_policy = "REWRITE_LINKS" if (link_policy and link_policy != "PRESERVE_ALL") or getattr(rule, "remove_links", False): clean_text, updated_ents, links_mod = LinkManagementEngine.apply_to_payload( payload, rule, text=text @@ -1634,6 +1660,22 @@ async def _apply_transforms( if getattr(rule, "replacements", None): text = self._filter_engine.replace_text(text, rule.replacements) + # "@mention rewrites" run in the same final pass: a map-driven replace + # of @handles (`@oldshop -> @newshop`, or a `*` catch-all), so a + # rewrite cannot reintroduce a handle the task asked to replace. + mention_map = getattr(rule, "mention_rewrite_map", None) or {} + if mention_map: + from ..domain.text_replace import apply_mention_rewrites + + rewritten = apply_mention_rewrites(text, mention_map) + if rewritten != text: + logger.info( + "[PIPELINE:REPLACE] [%s] Rule %s rewrote @mentions of msg %s (len %d -> %d)", + ctx.correlation_id[:8], rule.id[:8], payload.message_id, + len(text), len(rewritten), + ) + text = rewritten + # Header & Footer header = getattr(rule, "header", "") or getattr(rule, "header_text", "") footer = getattr(rule, "footer", "") or getattr(rule, "footer_text", "") @@ -1742,6 +1784,24 @@ async def _run_ai_transform( if isinstance(rule.metadata, dict) and rule.metadata.get("prompt_id"): prompt_id = str(rule.metadata.get("prompt_id")) + if self._prompt_service and prompt_id: + # A template switched off in the prompt manager stays in the + # library but stops steering deliveries: the task is rewritten + # with its AI config's own prompt, exactly as if none were set. + # Preview and the tester keep working on disabled templates — + # that is how an operator checks a prompt before re-enabling it. + try: + template = await self._prompt_service.get_template(prompt_id) + except Exception: + template = None + if template is not None and not bool(getattr(template, "enabled", True)): + logger.info( + "[PIPELINE:AI_REWRITE:PROMPT_OFF] [%s] Rule %s prompt '%s' is disabled — " + "using the AI config's own prompt", + ctx.correlation_id[:8], rule.id[:8], prompt_id, + ) + prompt_id = "" + prompt_ver = 0 if self._prompt_service and prompt_id: try: preview = await self._prompt_service.preview_prompt( diff --git a/core/application/prompt_service.py b/core/application/prompt_service.py index fab8933c..eb15fa1f 100644 --- a/core/application/prompt_service.py +++ b/core/application/prompt_service.py @@ -349,6 +349,23 @@ async def activate_version(self, template_id: str, version: int) -> bool: """Activates a specific version.""" return await self.prompt_repo.activate_version(template_id, version) + async def set_enabled(self, template_id: str, enabled: bool) -> PromptTemplate: + """Enable or disable a template without touching its version history. + + Disabling is a delivery-policy switch, not a content change: the + pipeline stops consulting the template (the task falls back to its AI + config's own prompt) but the library row, its versions and its + assignment on tasks stay exactly as they are, so re-enabling restores + the previous behavior with nothing re-entered. + """ + template = await self.prompt_repo.get_template(template_id) + if not template: + raise ValueError(f"Prompt template '{template_id}' not found.") + template.enabled = bool(enabled) + template.updated_at = int(time.time()) + await self.prompt_repo.update_template(template) + return template + async def get_template(self, template_id: str) -> Optional[PromptTemplate]: return await self.prompt_repo.get_template(template_id) diff --git a/core/domain/branding.py b/core/domain/branding.py index cea0f667..1e945ac7 100644 --- a/core/domain/branding.py +++ b/core/domain/branding.py @@ -26,6 +26,13 @@ * ``match_mode`` picks ``any`` (at least one group matches) or ``all``. * ``require_forward`` is a *gate*, not a check: it refuses a message that is not itself a forward, regardless of match_mode. +* ``require_channel`` is the same kind of gate for the *sender side*: only a + channel-originated message earns the hop (a native channel post in A, or a + forward whose origin is a channel). A message written by a normal user fails + the gate and is delivered directly — which is what keeps the user's own name + in B's "Forwarded from" header while channel content gets branded. The two + gates compose, so ``require_forward + require_channel`` reads "only content + that arrived as a channel forward". * ``force_forward`` (default true) is what makes stage 2 a native forward from C, which is the only way B can show C in its header. """ @@ -43,6 +50,7 @@ "channel_id", "enabled", "force_forward", + "is_channel_origin", "match_origin", "match_keywords", "should_hop", @@ -63,11 +71,12 @@ "keywords": [], "match_mode": "any", "require_forward": False, + "require_channel": False, "force_forward": True, } _LIST_KEYS = ("origin_ids", "origin_titles", "keywords") -_BOOL_KEYS = ("triggers_enabled", "require_forward", "force_forward") +_BOOL_KEYS = ("triggers_enabled", "require_forward", "require_channel", "force_forward") # --------------------------------------------------------------------------- # @@ -174,6 +183,30 @@ def _origin_of(payload: Any) -> Dict[str, Any]: return origin if isinstance(origin, dict) else {} +def is_channel_origin(payload: Any) -> bool: + """True when the message *comes from a channel*. + + Two shapes count, because Telegram produces both: + + * the message was **forwarded from a channel** — the forward-origin header + names a channel (the origin ``type`` carries "channel"; the pyrogram + adapter spells it ``channel``/``MessageForwardOriginChannel`` depending + on the client path, so the test is a substring, never an equality); + * or the message is a **native channel post in A** — the source chat A is + itself a channel (``chat_type == "channel"``), so the sender is the + channel rather than a person. + + A message written by a normal user in a group (or forwarded from a user) + answers False — that is the point: the ``require_channel`` gate lets user + messages keep their own "Forwarded from " header on a direct + delivery while channel content takes the branded hop. + """ + origin = _origin_of(payload) + if origin and "channel" in str(origin.get("type", "")).lower(): + return True + return str(getattr(payload, "chat_type", "") or "").strip().lower() == "channel" + + def match_origin(payload: Any, origin_ids: List[str], origin_titles: List[str]) -> bool: """Match the *forward origin channel* (Telegram's "Forwarded from" header). @@ -254,6 +287,8 @@ def should_hop( # A "require" is a gate and must hold on its own, whatever match_mode says. if cfg["require_forward"] and not _origin_of(payload): return False, "branding_requires_forward_origin" + if cfg["require_channel"] and not is_channel_origin(payload): + return False, "branding_requires_channel_origin" conditions: List[str] = [] matched: List[str] = [] diff --git a/core/domain/entities.py b/core/domain/entities.py index bbb7707e..08e83cfd 100644 --- a/core/domain/entities.py +++ b/core/domain/entities.py @@ -1346,6 +1346,22 @@ def branding_require_forward(self) -> bool: def branding_require_forward(self, val: Any) -> None: self._set_branding_key("require_forward", bool(val)) + @property + def branding_require_channel(self) -> bool: + """Gate: refuse the hop unless the message originates from a channel. + + This is the switch that makes the flow "normal users are forwarded + directly with their own name, channel content goes through C": a + user's own post fails the gate and keeps its personal "Forwarded + from " header on a direct delivery, while a channel post (or a + channel forward) earns the branded A -> C -> B route. + """ + return bool(self._branding_key("require_channel")) + + @branding_require_channel.setter + def branding_require_channel(self, val: Any) -> None: + self._set_branding_key("require_channel", bool(val)) + @property def branding_force_forward(self) -> bool: """Stage 2 sends a native forward of C — the only way B can show C.""" @@ -1675,6 +1691,41 @@ def smart_ai_rewrite(self, val: bool) -> None: self.metadata = {} self.metadata["smart_ai_rewrite"] = bool(val) + # --- Replace engine: link & mention rewrite maps ----------------------- + # "Replace any link found" and "replace any @ID" are the two map-driven + # replacement switches. The maps ride the rule's metadata blob so they + # need no schema migration, and the registry exposes them (with the + # column-backed `link_rewrite_map`) so the bot, dashboard, mobile client + # and the REST settings surface all edit the same values. + @property + def link_replace_enabled(self) -> bool: + """Rewrite every link the text carries through ``link_rewrite_map``.""" + return bool(self.metadata.get("link_replace_enabled", False)) if isinstance(self.metadata, dict) else False + + @link_replace_enabled.setter + def link_replace_enabled(self, val: bool) -> None: + if not isinstance(self.metadata, dict): + self.metadata = {} + self.metadata["link_replace_enabled"] = bool(val) + + @property + def mention_rewrite_map(self) -> Dict[str, str]: + """@username -> @username pairs applied to the message text. + + A ``*`` key replaces every mention with its value; otherwise only the + exact (case-insensitive) handles listed are rewritten. + """ + if not isinstance(self.metadata, dict): + return {} + raw = self.metadata.get("mention_rewrite_map", {}) + return dict(raw) if isinstance(raw, dict) else {} + + @mention_rewrite_map.setter + def mention_rewrite_map(self, val: Any) -> None: + if not isinstance(self.metadata, dict): + self.metadata = {} + self.metadata["mention_rewrite_map"] = dict(val or {}) + @property def ai_rewrite_active(self) -> bool: """Single authority for "does this task rewrite text with an LLM?". @@ -2465,6 +2516,10 @@ class PromptTemplate: updated_at: int = field(default_factory=_now) # v12: user prompts are per-tenant; system prompts (is_system) stay shared. owner_user_id: int = 0 + # A disabled template stays in the library (editable, testable) but the + # delivery pipeline skips it: a task pointing at it is rewritten with the + # AI config's own prompt instead, exactly as if no template were set. + enabled: bool = True @dataclass diff --git a/core/domain/rule_settings.py b/core/domain/rule_settings.py index 79921dd2..0cf3e72b 100644 --- a/core/domain/rule_settings.py +++ b/core/domain/rule_settings.py @@ -108,6 +108,27 @@ def _s(**kw: Any) -> SettingSpec: "(`i` ignore case, `c` case-sensitive, `w` whole word)." )), "link_replacement": _s(kind="str", default="", group="cleanup"), + # --- Replace engine: links & @mentions --------------------------------- + # The toggle arms the map below it: with `link_replace_enabled` on, every + # link the text carries is rewritten through `link_rewrite_map` (a `*` + # key rewrites every link to one target, a domain key rewrites that + # domain, a key containing "/" rewrites the URLs it prefixes). + "link_replace_enabled": _s( + kind="bool", default=False, group="cleanup", + help="Rewrite every link found in the text through the link rewrite map."), + # Column-backed like allowed_media_types: the Link Management Engine reads + # the real forward_rules column, and backups/the REST contract already + # carry it. Registered so the API settings surface and the generic + # settings UIs can edit the map without a bespoke route. + "link_rewrite_map": _s( + kind="dict", default={}, group="cleanup", + help='Link rewrite pairs: {"*": "https://t.me/me"} rewrites every link,' + ' "olddomain.com": "newdomain.com" rewrites one domain,' + ' "https://old.t.me/x": "https://new.t.me/y" rewrites a full URL.'), + "mention_rewrite_map": _s( + kind="dict", default={}, group="cleanup", + help='@mention rewrite pairs: {"@oldshop": "@newshop"} rewrites one' + ' handle (case-insensitive), "*": "@main" rewrites every @ID.'), "clean_usernames": _s(kind="bool", default=False, group="cleanup"), "clean_hashtags": _s(kind="bool", default=False, group="cleanup"), "clean_phones": _s(kind="bool", default=False, group="cleanup"), @@ -303,6 +324,9 @@ def _s(**kw: Any) -> SettingSpec: "branding_require_forward": _s( kind="bool", default=False, group="branding", help="Refuse the hop unless the message is itself a forward."), + "branding_require_channel": _s( + kind="bool", default=False, group="branding", + help="Refuse the hop unless the message comes from a channel — normal users are then forwarded directly with their own name."), "branding_force_forward": _s( kind="bool", default=True, group="branding", help="Send the final step as a native forward of C, so B's header names C."), diff --git a/core/domain/services.py b/core/domain/services.py index 36fa582a..17ae2e44 100644 --- a/core/domain/services.py +++ b/core/domain/services.py @@ -884,11 +884,24 @@ def _should_remove(u: str) -> bool: def _rewrite_url(u: str) -> str: dom = cls.extract_domain(u) + # The catch-all key rewrites every link to one target — the + # "replace any link found" switch. Explicit keys win over it, so a + # task that maps one domain specifically still maps that domain. + star = rewrite_map.get("*") + lowered = u.lower() for old_d, new_d in rewrite_map.items(): - clean_old = old_d.lower().strip() + clean_old = str(old_d).lower().strip() + if not clean_old or clean_old == "*": + continue + if "/" in clean_old: + # A key that carries a path is a URL-prefix match: it + # rewrites the URLs it starts with instead of a domain. + if lowered.startswith(clean_old): + return new_d + u[len(clean_old):] + continue if dom == clean_old or dom.endswith("." + clean_old): return u.replace(old_d, new_d) - return u + return star if star is not None else u # Handle text_link entities: if URL should be stripped, remove URL metadata # (retaining anchor text without breaking formatting). diff --git a/core/domain/text_replace.py b/core/domain/text_replace.py index 136e9fcc..35cc5231 100644 --- a/core/domain/text_replace.py +++ b/core/domain/text_replace.py @@ -437,6 +437,79 @@ def apply_replacements(text: str, replacements: Any) -> str: return out +# --------------------------------------------------------------------------- # +# @mention rewrites ("replace any ID that starts with @") +# --------------------------------------------------------------------------- # + +#: A Telegram handle: "@" + 4..32 username characters, not glued to a word or +#: a longer mention (an email address or a double-@@ never matches). This is +#: the same username alphabet the cleaner's mention stripper uses. +MENTION_RE = re.compile(r"(? Dict[str, str]: + """Normalise a stored mention map to ``{"@handle": "@handle-or-text"}``. + + Keys are case-folded to their canonical "@lower" spelling so a stored + ``@Shop`` matches a message that says ``@shop``. Keys and values are + accepted with or without the leading "@", so a user who types + ``oldshop newshop`` and one who types ``@oldshop @newshop`` mean the same + pair. Unusable rows (empty, wrong type, over the bound) are dropped, never + fatal — a bad pair must not block the message it was meant to edit. + """ + if not isinstance(raw, Mapping): + return {} + out: Dict[str, str] = {} + for key, value in raw.items(): + if not isinstance(value, str): + continue + k = str(key).strip() + v = value.strip() + if not k or not v: + continue + if len(out) >= MAX_MENTION_PAIRS: + break + if k != MENTION_MATCH_ALL: + k = "@" + k.lstrip("@").lower() + out[k] = v + return out + + +def apply_mention_rewrites(text: str, mention_map: Any) -> str: + """Rewrite @handles per ``mention_map``. + + Exact (case-insensitive) handles win over the ``*`` catch-all, so a task + that maps ``@oldshop -> @newshop`` and ``* -> @main`` still rewrites the + old shop specifically and every other mention to the main handle. The + catch-all alone rewrites every mention. Runs in one ``re.sub`` pass so + the output of one pair can never be re-matched by another. + """ + if not text: + return text + mapping = normalize_mention_map(mention_map) + if not mapping: + return text + exact = {k: v for k, v in mapping.items() if k != MENTION_MATCH_ALL} + catch_all = mapping.get(MENTION_MATCH_ALL) + + def _sub(match: "re.Match[str]") -> str: + handle = match.group(0) + replacement = exact.get("@" + match.group(1).lower()) + if replacement is None: + replacement = catch_all + if replacement is None: + return handle + return replacement if replacement.startswith("@") else "@" + replacement.lstrip("@") + + return MENTION_RE.sub(_sub, text) + + def stored_entries(replacements: Any, task_id: str = "") -> List[Tuple[str, ReplaceEntry]]: """``(rule_id, entry)`` for every stored pair, in map order. diff --git a/core/infrastructure/persistence/database.py b/core/infrastructure/persistence/database.py index 727e5a38..629b92ab 100644 --- a/core/infrastructure/persistence/database.py +++ b/core/infrastructure/persistence/database.py @@ -312,7 +312,10 @@ current_version INTEGER NOT NULL DEFAULT 1, is_system INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL + updated_at INTEGER NOT NULL, + -- library enable/disable switch (see _ADDITIVE_COLUMNS; the guarded + -- ALTER also adds it to tables created before the switch existed) + enabled INTEGER NOT NULL DEFAULT 1 ); CREATE INDEX IF NOT EXISTS idx_prompt_name ON prompt_templates(name); @@ -1166,6 +1169,10 @@ # through a backup export, and overwrite another tenant's prompt templates. ("api_credentials", "owner_user_id", "INTEGER NOT NULL DEFAULT 0", "idx_api_credentials_owner"), ("prompt_templates", "owner_user_id", "INTEGER NOT NULL DEFAULT 0", "idx_prompt_templates_owner"), + # The prompt library's enable/disable switch ("on" for every row written + # before the switch existed). A disabled template stays in the library but + # the delivery pipeline skips it in favour of the task's AI config prompt. + ("prompt_templates", "enabled", "INTEGER NOT NULL DEFAULT 1", None), ("users", "status", "TEXT NOT NULL DEFAULT 'active'", "idx_users_status"), # v19: a bot token belongs to the tenant that pasted it. Without an owner # every owner's route screen listed every other owner's bots, and any diff --git a/core/infrastructure/persistence/postgres_repositories.py b/core/infrastructure/persistence/postgres_repositories.py index de160d04..a6bc54fc 100644 --- a/core/infrastructure/persistence/postgres_repositories.py +++ b/core/infrastructure/persistence/postgres_repositories.py @@ -1905,7 +1905,8 @@ class PostgresPromptRepository(IPromptRepository): is_system BOOLEAN NOT NULL DEFAULT FALSE, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, - owner_user_id BIGINT NOT NULL DEFAULT 0 + owner_user_id BIGINT NOT NULL DEFAULT 0, + enabled BOOLEAN NOT NULL DEFAULT TRUE ); CREATE INDEX IF NOT EXISTS idx_pg_prompt_name ON prompt_templates (name); @@ -1946,20 +1947,22 @@ async def init_schema(self) -> None: ADDITIVE_COLUMNS = """ ALTER TABLE prompt_templates ADD COLUMN IF NOT EXISTS owner_user_id BIGINT NOT NULL DEFAULT 0; CREATE INDEX IF NOT EXISTS idx_pg_prompt_templates_owner ON prompt_templates (owner_user_id); + ALTER TABLE prompt_templates ADD COLUMN IF NOT EXISTS enabled BOOLEAN NOT NULL DEFAULT TRUE; """ async def add_template(self, template: PromptTemplate) -> PromptTemplate: query = """ INSERT INTO prompt_templates ( id, name, description, system_prompt, user_prompt_template, - target_language, current_version, is_system, created_at, updated_at, owner_user_id - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + target_language, current_version, is_system, created_at, updated_at, owner_user_id, enabled + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ params = ( template.id, template.name, template.description, template.system_prompt, template.user_prompt_template, template.target_language, template.current_version, bool(template.is_system), int(template.created_at), int(template.updated_at), int(getattr(template, "owner_user_id", 0) or 0), + bool(getattr(template, "enabled", True)), ) await _execute_db(self._db, query, params) return template @@ -1970,7 +1973,7 @@ async def update_template(self, template: PromptTemplate) -> PromptTemplate: UPDATE prompt_templates SET name = %s, description = %s, system_prompt = %s, user_prompt_template = %s, target_language = %s, current_version = %s, is_system = %s, updated_at = %s, - owner_user_id = %s + owner_user_id = %s, enabled = %s WHERE id = %s """ params = ( @@ -1978,6 +1981,7 @@ async def update_template(self, template: PromptTemplate) -> PromptTemplate: template.user_prompt_template, template.target_language, template.current_version, bool(template.is_system), template.updated_at, int(getattr(template, "owner_user_id", 0) or 0), + bool(getattr(template, "enabled", True)), template.id, ) await _execute_db(self._db, query, params) @@ -2012,6 +2016,8 @@ def _row_to_template(self, r) -> PromptTemplate: target_language=d.get("target_language", "en"), current_version=int(d["current_version"]), is_system=bool(d["is_system"]), created_at=int(d["created_at"]), updated_at=int(d["updated_at"]), owner_user_id=int(d.get("owner_user_id") or 0), + # Rows written before the enable switch existed read as enabled. + enabled=bool(d.get("enabled", True)), ) async def delete_template(self, template_id: str) -> bool: diff --git a/core/infrastructure/persistence/sqlite_repositories.py b/core/infrastructure/persistence/sqlite_repositories.py index 4054d9f0..53ac77fe 100644 --- a/core/infrastructure/persistence/sqlite_repositories.py +++ b/core/infrastructure/persistence/sqlite_repositories.py @@ -3304,14 +3304,15 @@ async def add_template(self, template: PromptTemplate) -> PromptTemplate: self._db.execute( """ INSERT INTO prompt_templates (id, name, description, system_prompt, user_prompt_template, - target_language, current_version, is_system, created_at, updated_at, owner_user_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + target_language, current_version, is_system, created_at, updated_at, owner_user_id, enabled) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( template.id, template.name, template.description, template.system_prompt, template.user_prompt_template, template.target_language, template.current_version, int(template.is_system), template.created_at, template.updated_at, int(getattr(template, "owner_user_id", 0) or 0), + int(bool(getattr(template, "enabled", True))), ), ) return template @@ -3322,7 +3323,7 @@ async def update_template(self, template: PromptTemplate) -> PromptTemplate: """ UPDATE prompt_templates SET name=?, description=?, system_prompt=?, user_prompt_template=?, - target_language=?, current_version=?, is_system=?, updated_at=?, owner_user_id=? + target_language=?, current_version=?, is_system=?, updated_at=?, owner_user_id=?, enabled=? WHERE id=? """, ( @@ -3330,6 +3331,7 @@ async def update_template(self, template: PromptTemplate) -> PromptTemplate: template.user_prompt_template, template.target_language, template.current_version, int(template.is_system), template.updated_at, int(getattr(template, "owner_user_id", 0) or 0), + int(bool(getattr(template, "enabled", True))), template.id, ), ) @@ -3361,6 +3363,8 @@ def _row_to_template(self, r) -> PromptTemplate: target_language=r["target_language"], current_version=int(r["current_version"]), is_system=bool(r["is_system"]), created_at=r["created_at"], updated_at=r["updated_at"], owner_user_id=int(r["owner_user_id"]) if "owner_user_id" in r.keys() else 0, + # Rows written before the enable switch existed read as enabled. + enabled=bool(r["enabled"]) if "enabled" in r.keys() else True, ) async def delete_template(self, template_id: str) -> bool: diff --git a/core/infrastructure/telegram/dispatcher.py b/core/infrastructure/telegram/dispatcher.py index 1b0c2a1c..bab9c943 100644 --- a/core/infrastructure/telegram/dispatcher.py +++ b/core/infrastructure/telegram/dispatcher.py @@ -30,6 +30,7 @@ from ...domain.value_objects import ContentMode, ForwardMode from .album_aggregator import AlbumAggregator from .client_pool import ClientPool, payload_from_pyrogram +from .pyrogram_compat import thread_kwargs as pyrogram_thread_kwargs from .errors import is_restricted_source_error, is_timeout_error, tg_detail, tg_error, wait_seconds from .media_reference import ( extract_media_reference as _extract_media_reference, @@ -782,9 +783,13 @@ async def _deliver( # addresses a real thread; 0/None means "no topic configured" and must # not be passed (Telegram would reject a zero thread id). topic_id = int(getattr(rule, "topic_id", None) or 0) - thread_kwargs: dict = {} - if topic_id > 0: - thread_kwargs["message_thread_id"] = topic_id + # Same documented-surface guard as the durable queue: the pinned + # pyrogram predates message_thread_id on these methods. + thread_kwargs: dict = pyrogram_thread_kwargs( + client, + ["forward_messages", "copy_message", "send_message"], + topic_id, + ) # If the branding hop ran, stage 2 starts from C — never from A. if intermediate_msg_id and branding.enabled(rule): diff --git a/core/infrastructure/telegram/durable_queue.py b/core/infrastructure/telegram/durable_queue.py index b3af47c4..3cb671c9 100644 --- a/core/infrastructure/telegram/durable_queue.py +++ b/core/infrastructure/telegram/durable_queue.py @@ -39,6 +39,7 @@ ) from .failure_policy import classify_failure, error_code, rate_limit_seconds from .bot_transport import BotRouteAdapter, BotTransportCache, BotTransportUnsupported +from core.infrastructure.telegram.pyrogram_compat import thread_kwargs as pyrogram_thread_kwargs from .media_reference import extract_media_reference, send_media_reference logger = logging.getLogger("atf.durable_queue") @@ -436,6 +437,11 @@ async def enqueue_delivery( # owner sees immediately — so they ride the snapshot too. "sender_name": str(getattr(payload, "sender_name", "") or ""), "forward_origin": dict(getattr(payload, "forward_origin", None) or {}) or None, + # The source chat's type ("channel" | "supergroup" | "group" | + # "private"). The Worker re-decides the branding hop at claim time + # from the same inputs Python used, and the require_channel gate + # needs this to recognise a native channel post. + "chat_type": str(getattr(payload, "chat_type", "") or ""), # --- Branding hop (A -> C -> B) ---------------------------------- # Already performed on the dispatch path; the worker only repeats it # so B receives a forward of C instead of the original source. @@ -1463,12 +1469,17 @@ def _bump_target(k: str) -> None: if isinstance(exc, DeliveryBlockedError): self.stats["failed"] += 1 _bump_target("failed") + # A job that died inside the branding hop gets its own event + # code, so "the branded route is broken for this task" is one + # Log Center filter instead of a needle in every DELIVERY_BLOCKED. + stage = str(getattr(job, "delivery_stage", "") or "") + event_code = "BRANDING_STAGE2_FAILED" if stage == "BRANDED" else "DELIVERY_BLOCKED" logger.error( "[PIPELINE:DELIVERY_BLOCKED] [%s] No permitted delivery method succeeded for %s " "(rule=%s, src=%s:%s): %s. Marking job FAILED (task stays active; message is replayable).", worker_id, target, job.rule_id[:8], job.source_chat_id, job.source_message_id, err_msg or exc, - extra={"event_code": "DELIVERY_BLOCKED"}, + extra={"event_code": event_code}, ) if self._log_client is not None: try: @@ -1480,7 +1491,9 @@ def _bump_target(k: str) -> None: "source_chat_id": job.source_chat_id, "source_message_id": job.source_message_id, "target": target, - "event_code": "DELIVERY_BLOCKED", + "event_code": event_code, + "delivery_stage": stage, + "intermediate_chat_id": str(getattr(job, "intermediate_chat_id", "") or ""), "owner_user_id": owner_id, }, ) @@ -2096,9 +2109,17 @@ async def _dispatch_to_telegram(self, client: Any, job: DeliveryJob) -> int: # the message silently lands in the wrong place. Only a positive id is # meaningful: 0/None means "no topic configured". topic_id = int(data.get("topic_id", 0) or 0) - thread_kwargs: Dict[str, Any] = {} - if topic_id > 0: - thread_kwargs["message_thread_id"] = topic_id + # Topic targeting rides the documented surface: the pinned pyrogram + # 2.0.106 predates message_thread_id on forward/copy/send, so the + # kwarg is threaded only when this session's client accepts it — + # every spread below lands in the chat's General topic otherwise, + # instead of failing the whole delivery with a TypeError. + thread_kwargs: Dict[str, Any] = pyrogram_thread_kwargs( + client, + ["forward_messages", "copy_message", "copy_media_group", + "send_message", "send_media_group"], + topic_id, + ) # --- Branding hop (A -> C -> B) -------------------------------------- # The message was already copied into our channel C on the dispatch path diff --git a/core/infrastructure/telegram/locales/translations.json b/core/infrastructure/telegram/locales/translations.json index b948cbf7..3cde6b7c 100644 --- a/core/infrastructure/telegram/locales/translations.json +++ b/core/infrastructure/telegram/locales/translations.json @@ -232,6 +232,7 @@ "ui_ai_btn_health": "🩺 Health check (Ping)", "ui_ai_btn_key": "🔑 Change key (API Key)", "ui_ai_btn_model": "🧠 Change model", + "ui_ai_btn_open_prompts": "🧠 Manage prompts", "ui_ai_btn_pick_config": "🧠 Select AI configuration", "ui_ai_btn_pick_prompt": "📝 Select prompt", "ui_ai_btn_sample": "🧪 Test text rewrite", @@ -298,6 +299,7 @@ "ui_ai_prompt_enter_system_instruction": "🤖 Enter the system prompt instructions for '{name}':\n\nExample: `Rewrite this message in formal business tone without changing core facts.`", "ui_ai_prompt_label": "Prompt", "ui_ai_prompt_name_invalid": "❌ Invalid prompt name. Please enter a name between 1 and 80 characters.", + "ui_ai_prompt_off_tag": "off", "ui_ai_prompt_staged_alert": "✅ Prompt selected (will be saved when you tap Save)", "ui_ai_prompt_sys_empty_error": "❌ System prompt cannot be empty. Please enter your rewrite instructions.", "ui_ai_provider": "🧠 Send provider: openai, anthropic, gemini, groq, deepseek, 9router, openrouter, custom", @@ -403,10 +405,10 @@ "ui_bl_count": "Blocked keyword count: **{count}**\n", "ui_bl_desc": "Messages containing any of these entries are **NOT** forwarded.\n\nThree ways to match: **Keyword / Phrase**, **Regex**, or **Long-Text Matching**.\n\n", "ui_bl_empty": "❌ No keywords in blacklist.", - "ui_bl_only_regex": "🚫 No keywords — but the regex rules below are active and filtering.", "ui_bl_entry_deleted_alert": "Deleted: {entry}", "ui_bl_hint": "👇 Choose an action to manage the blacklist:", "ui_bl_more": "\n... and {more} more words", + "ui_bl_only_regex": "🚫 No keywords — but the regex rules below are active and filtering.", "ui_bl_state_off": "Status: **disabled** (words are kept but not enforced).\n\n", "ui_bl_state_on": "Status: **enabled** (words are enforced).\n\n", "ui_bl_title": "🚫 **Blacklist Keywords**\n\n", @@ -424,6 +426,7 @@ "ui_block_video_on": "Video blocking enabled 🚫", "ui_block_voice_off": "Voice messages allowed 🟢", "ui_block_voice_on": "Voice blocking enabled 🚫", + "ui_brand_channel_saved": "📡 Channel-only hop: {state}", "ui_brand_cleared": "🧹 Branding filter cleared — every message will hop.", "ui_brand_force_saved": "🔁 Forward from C: {state}", "ui_brand_gate_saved": "🚧 Forward-only gate: {state}", @@ -437,6 +440,7 @@ "ui_brand_words_saved": "✅ Route words saved: {value}", "ui_btn_ai_pv_assistant": "💬 Smart DM Assistant (AI PV Assistant)", "ui_btn_ar_assistant": "🤖 AI Auto-Reply (Private Messages)", + "ui_btn_prompts": "🧠 Prompt library", "ui_btn_use_default_host": "✅ Use default (or next)", "ui_bulk_confirm": "Delete {n} selected rule(s)? This cannot be undone.", "ui_bulk_delete": "🗑 Delete selected", @@ -1015,6 +1019,48 @@ "ui_plan_status_none": "0 days", "ui_prev": "◀️ Prev", "ui_priority_alert": "Priority: {priority}", + "ui_prm_btn_back_list": "← Prompt library", + "ui_prm_btn_edit_desc": "📝 Description", + "ui_prm_btn_edit_name": "✏️ Rename", + "ui_prm_btn_edit_sys": "🧩 System prompt", + "ui_prm_btn_edit_user": "✉️ User template", + "ui_prm_btn_new": "➕ New prompt", + "ui_prm_created": "✅ Prompt “{name}” created and saved to your library.", + "ui_prm_delete_confirm": "⚠️ Delete the prompt “{name}”?\nTasks pointing at it fall back to their AI config's own prompt.", + "ui_prm_delete_yes": "🗑 Yes, delete", + "ui_prm_deleted": "🗑 Prompt deleted.", + "ui_prm_detail_desc": "📝 {desc}", + "ui_prm_detail_lang": "🌐 Target language: {code}", + "ui_prm_detail_sys": "🧩 System prompt:\n{sys}", + "ui_prm_detail_title": "🧠 {name}", + "ui_prm_detail_user": "✉️ User template:\n{user}", + "ui_prm_detail_version": "🔖 Version: v{version}", + "ui_prm_disabled_alert": "🔴 Prompt disabled — deliveries fall back to the AI config's own prompt.", + "ui_prm_edit_desc_prompt": "📝 Send the new description for “{name}” — send - to clear it.", + "ui_prm_edit_name_prompt": "✏️ Send the new name for “{name}”.", + "ui_prm_edit_sys_prompt": "🧩 Send the new system prompt for “{name}”.", + "ui_prm_edit_user_prompt": "✉️ Send the new user template for “{name}”.\nIt must contain the {text} placeholder — send - to reset it to “{text}”.", + "ui_prm_enabled_alert": "🟢 Prompt enabled.", + "ui_prm_error_generic": "⚠️ Could not save the prompt — please try again.", + "ui_prm_hint": "Tap a prompt to manage it. A disabled prompt stays in the library but deliveries use the task's AI config prompt.", + "ui_prm_needs_text_placeholder": "✋ The user template must contain the {text} placeholder.", + "ui_prm_new_name_prompt": "🧠 New prompt — step 1/3\n\n✍️ Send the prompt's name.", + "ui_prm_new_sys_prompt": "🧠 New prompt — step 2/3\n\n✍️ Send the system prompt: the instructions the AI follows on every rewrite.", + "ui_prm_new_user_prompt": "🧠 New prompt — step 3/3\n\n✍️ Send the user template. It must contain the {text} placeholder — send - to use just “{text}”.", + "ui_prm_none": "No prompt templates yet. Tap ➕ New prompt to add one.", + "ui_prm_not_found": "Prompt not found — it may have been deleted.", + "ui_prm_saved": "✅ Saved — now at v{version}.", + "ui_prm_state_off": "🔴 Disabled — deliveries use the AI config's own prompt", + "ui_prm_state_on": "🟢 Enabled — used by deliveries", + "ui_prm_system_immutable": "System prompts are shared by every account on this installation. To customize one, create a new prompt with the wording you need.", + "ui_prm_system_tag": "shared", + "ui_prm_test_ask": "🧪 Send a sample message to rewrite with “{name}”.", + "ui_prm_test_failed": "🧪 “{name}” failed ({code}):\n{message}", + "ui_prm_test_no_config": "No enabled AI config found — add one in the AI menu first.", + "ui_prm_test_result": "🧪 “{name}” — result ({model}):\n\n{result}", + "ui_prm_title": "🧠 Prompt templates — {count} · 🟢 {active}", + "ui_prm_too_long": "✋ Too long — please shorten the text and send it again.", + "ui_prm_unavailable": "The prompt service is not available on this installation.", "ui_profile_actions": "👇 Account actions:", "ui_profile_reset_config": "⚠️ Reset task settings", "ui_profile_restart_setup": "🔄 Restart setup", @@ -1026,7 +1072,6 @@ "ui_prompt_change_base_url": "🌐 **Change server address / host:**\n\nCurrent address: `{current}`\n\n✍️ Send the new host address:\n\nOr send `default` to reset to default:", "ui_prompt_change_model": "🧠 **Change AI model:**\n\nCurrent model: `{model}`\n\n✍️ Type and send the new model name:\n(Example: `coding` or `gpt-4o-mini`)", "ui_prompt_edit_entry": "✏️ Send the new value to replace `{entry}`:", - "ui_stale_entry": "⚠️ The list changed while the menu was open — showing the current state.", "ui_prompt_enter_base_url": "🌐 **Set server address / host ({provider}):**\n\nDefault address:\n`{default_url}`\n\n✍️ If you want to use a custom host or port, send the address:\n\nOr tap the button below to use default:", "ui_prompt_enter_delay": "⏱ **Enter desired delay (seconds):**\n\nPlease send the delay in seconds (e.g. `5` or `15`).", "ui_prompt_enter_every_n": "🔢 **Enter the interval:**\n\nSend a whole number from 1 to 1000. `1` forwards every message; `5` forwards the 1st, 6th, 11th … message that passes the filters.", @@ -1081,6 +1126,7 @@ "ui_reason_branding_always": "branding is on for every message", "ui_reason_branding_disabled": "branding is off ({reason})", "ui_reason_branding_no_triggers_configured": "branding is on with no triggers, so every message hops", + "ui_reason_branding_requires_channel_origin": "branding hops only messages that come from a channel", "ui_reason_branding_requires_forward_origin": "branding hops only messages that are themselves forwards", "ui_reason_branding_triggers_matched": "the hop matched its triggers ({words})", "ui_reason_branding_triggers_unmatched": "the hop matched none of its triggers ({words})", @@ -1180,6 +1226,7 @@ "ui_reg_help_branding_match_mode": "any = one trigger is enough, all = every configured trigger must match.", "ui_reg_help_branding_origin_ids": "Hop only when the message was forwarded from one of these chat ids/@usernames.", "ui_reg_help_branding_origin_titles": "Hop only when the message was forwarded from one of these channel names.", + "ui_reg_help_branding_require_channel": "Refuse the hop unless the message comes from a channel — normal users are then forwarded directly with their own name.", "ui_reg_help_branding_require_forward": "Refuse the hop unless the message is itself a forward.", "ui_reg_help_branding_triggers_enabled": "When off every matched message takes the hop via C; when on, the triggers below decide.", "ui_reg_help_clean_custom_words": "Words stripped from the text.", @@ -1192,7 +1239,10 @@ "ui_reg_help_footer": "Text appended to every forwarded message.", "ui_reg_help_forward_every_n": "Forward only every Nth message that passes this task's filters (1 = every message, 1000 = one in a thousand). An album counts as one message.", "ui_reg_help_header": "Text prepended to every forwarded message.", + "ui_reg_help_link_replace_enabled": "Rewrite every link found in the text through the link rewrite map.", + "ui_reg_help_link_rewrite_map": "Link rewrite pairs: {\"*\": \"https://t.me/me\"} rewrites every link, \"olddomain.com\": \"newdomain.com\" rewrites one domain, \"https://old.t.me/x\": \"https://new.t.me/y\" rewrites a full URL.", "ui_reg_help_max_time_edit": "Seconds after which an edit is no longer mirrored.", + "ui_reg_help_mention_rewrite_map": "@mention rewrite pairs: {\"@oldshop\": \"@newshop\"} rewrites one handle (case-insensitive), \"*\": \"@main\" rewrites every @ID.", "ui_reg_help_ocr_image_ai": "Run OCR on images and forward the extracted text.", "ui_reg_help_power_control": "Convenience view of power_schedule.enabled.", "ui_reg_help_process_raw_text": "Run the pipeline on raw text before any parsing.", @@ -1254,6 +1304,7 @@ "ui_reg_set_branding_match_mode": "Branding match mode", "ui_reg_set_branding_origin_ids": "Origin IDs", "ui_reg_set_branding_origin_titles": "Origin titles", + "ui_reg_set_branding_require_channel": "Channel-only hop", "ui_reg_set_branding_require_forward": "Require forward", "ui_reg_set_branding_triggers_enabled": "Branding triggers", "ui_reg_set_button_to_text": "Button to text", @@ -1279,8 +1330,11 @@ "ui_reg_set_keyword_confirmation": "Keyword confirmation", "ui_reg_set_keyword_reply": "Keyword reply", "ui_reg_set_link_buttons": "Link buttons", + "ui_reg_set_link_replace_enabled": "Replace any link found", "ui_reg_set_link_replacement": "Link replacement", + "ui_reg_set_link_rewrite_map": "Link rewrite map", "ui_reg_set_max_time_edit": "Max time to edit", + "ui_reg_set_mention_rewrite_map": "@mention rewrite map", "ui_reg_set_news_digest": "News digest", "ui_reg_set_ocr_image_ai": "OCR image AI", "ui_reg_set_paid_addons": "Paid add-ons", @@ -1365,6 +1419,26 @@ "ui_reset_config_done": "✅ All settings were reset successfully.", "ui_retry": "🔄 Try Again", "ui_retry_connect": "🚀 Retry connection", + "ui_rmp_btn_edit_links": "🔗 Edit link map", + "ui_rmp_btn_edit_mentions": "❕ Edit @mention map", + "ui_rmp_btn_links_off": "🔗 Replace links: OFF", + "ui_rmp_btn_links_on": "🔗 Replace links: ON", + "ui_rmp_cleared": "✅ Map cleared.", + "ui_rmp_link_map": "🔗 Link map ({count}): {pairs}", + "ui_rmp_link_map_empty": "🔗 Link map: empty", + "ui_rmp_link_state_off": "🔗 Link replace: OFF", + "ui_rmp_link_state_on": "🔗 Link replace: ON — links are rewritten through the map", + "ui_rmp_links_prompt": "🔗 Send the link rewrite map — one pair per line:\n`olddomain.com newdomain.com` — rewrite one domain\n`https://old.t.me/x https://new.t.me/y` — rewrite one link\n`* https://t.me/me` — rewrite EVERY link\n\nSend - to clear the map.", + "ui_rmp_mention_map": "❕ Mention map ({count}): {pairs}", + "ui_rmp_mention_map_empty": "❕ Mention map: empty", + "ui_rmp_mentions_prompt": "❕ Send the @mention map — one pair per line:\n`@oldshop @newshop` — rewrite one handle\n`* @main` — rewrite EVERY @ID\n\nSend - to clear the map.", + "ui_rmp_off_hint": "The map is set but link replace is OFF — turn it on to apply it.", + "ui_rmp_rule": "Task: {name}", + "ui_rmp_saved": "✅ Saved — {count} pair(s).", + "ui_rmp_skipped": "✅ Saved — {count} pair(s). Skipped {skipped} invalid line(s).", + "ui_rmp_title": "🔁 Replace links & @mentions", + "ui_rmp_toggle_off": "🔴 Link replace disabled.", + "ui_rmp_toggle_on": "🟢 Link replace enabled — every link found is rewritten through the map.", "ui_route_add_sources": "📥 Sources ({count})", "ui_route_add_targets": "📤 Targets ({count})", "ui_route_bad_edit": "❌ Send `index = value`, or `name -> ` for a new route.", @@ -1742,11 +1816,14 @@ "ui_smart_active_path": "🛣 **Active message-flow path:**\n{path_desc}\n", "ui_smart_antiloop": "🔒 **Anti-loop & anti-double-send:** active with Idempotency Key and message-origin checks\n", "ui_smart_brand_block": "🔀 **Branding hop (A → C → B):**", + "ui_smart_brand_channel": "📡 Channel-only hop: {state}", "ui_smart_brand_origins": "🎯 **Origin channels:** {origins}", "ui_smart_brand_state": "🎛 **Hop filter:** {state}", "ui_smart_brand_state_all": "every message", "ui_smart_brand_state_filtered": "filtered", "ui_smart_brand_words": "🔑 **Route words:** {words}", + "ui_smart_btn_brand_chan_off": "📡 Channel-only hop: OFF", + "ui_smart_btn_brand_chan_on": "📡 Channel-only hop: ON", "ui_smart_btn_brand_force_off": "🔁 Forward from C: OFF", "ui_smart_btn_brand_force_on": "🔁 Forward from C: ON", "ui_smart_btn_brand_gate_off": "🚧 Forward-only: OFF", @@ -1763,6 +1840,7 @@ "ui_smart_btn_prio_down": "➖ Priority (−10)", "ui_smart_btn_prio_up": "➕ Priority (+10)", "ui_smart_btn_regex": "🔣 Set Regex", + "ui_smart_btn_replace_maps": "🔁 Link & @replace maps", "ui_smart_custom_header": "🏷 **Custom copy header:** {header}", "ui_smart_hint": "👇 To change settings, tap the options below:", "ui_smart_keywords": "🔎 **Text keywords:** {keywords}", @@ -1790,6 +1868,7 @@ "ui_src_desc": "New messages from this chat will be read and forwarded.", "ui_src_sel": "📥 **Select Source Channel or Group (Source):**", "ui_src_selected": "✅ Source: {title}", + "ui_stale_entry": "⚠️ The list changed while the menu was open — showing the current state.", "ui_stat_allowed_word": "Allowed 🟢", "ui_stat_clean_word": "Clean 🧹", "ui_stat_errors": "Errors", @@ -1964,10 +2043,10 @@ "ui_wl_count": "Allowed keyword count: **{count}**\n", "ui_wl_desc": "Only messages containing at least one of these entries are forwarded.\n\nThree ways to match: **Keyword / Phrase**, **Regex**, or **Long-Text Matching**.\n\n", "ui_wl_empty": "❌ No keywords in whitelist (all messages allowed).", - "ui_wl_only_regex": "🔒 No keywords — but the regex rules below are active and filtering.", "ui_wl_entry_deleted_alert": "Deleted: {entry}", "ui_wl_hint": "👇 Choose an action to manage the whitelist:", "ui_wl_more": "\n... and {more} more words", + "ui_wl_only_regex": "🔒 No keywords — but the regex rules below are active and filtering.", "ui_wl_state_off": "Status: **disabled** (words are kept but not enforced).\n\n", "ui_wl_state_on": "Status: **enabled** (words are enforced).\n\n", "ui_wl_title": "✅ **Whitelist Keywords**\n\n", @@ -2219,6 +2298,7 @@ "ui_ai_btn_health": "🩺 Comprobación (Ping)", "ui_ai_btn_key": "🔑 Cambiar clave (API Key)", "ui_ai_btn_model": "🧠 Cambiar modelo", + "ui_ai_btn_open_prompts": "🧠 Gestionar prompts", "ui_ai_btn_pick_config": "🧠 Seleccionar configuración de IA", "ui_ai_btn_pick_prompt": "📝 Seleccionar prompt", "ui_ai_btn_sample": "🧪 Probar reescritura de texto", @@ -2285,6 +2365,7 @@ "ui_ai_prompt_enter_system_instruction": "🤖 Ingrese las instrucciones del prompt del sistema para '{name}':\n\nEjemplo: `Reescriba este mensaje en tono formal sin cambiar los hechos principales.`", "ui_ai_prompt_label": "Prompt", "ui_ai_prompt_name_invalid": "❌ Nombre de prompt inválido. Ingrese entre 1 y 80 caracteres.", + "ui_ai_prompt_off_tag": "apagado", "ui_ai_prompt_staged_alert": "✅ Prompt seleccionado (se guardará al guardar)", "ui_ai_prompt_sys_empty_error": "❌ El prompt del sistema no puede estar vacío.", "ui_ai_provider": "🧠 Envía el proveedor: openai, anthropic, gemini, groq, deepseek, 9router, openrouter, custom", @@ -2390,10 +2471,10 @@ "ui_bl_count": "Cantidad de palabras bloqueadas: **{count}**\n", "ui_bl_desc": "Los mensajes que contengan cualquiera de estas entradas **NO** se reenvían.\n\nTres formas de coincidir: **Palabra / Frase**, **Regex** o **Coincidencia de texto largo**.\n\n", "ui_bl_empty": "❌ No hay palabras en la lista negra.", - "ui_bl_only_regex": "🚫 Sin palabras clave, pero las reglas regex de abajo están activas y filtran.", "ui_bl_entry_deleted_alert": "Eliminado: {entry}", "ui_bl_hint": "👇 Elige una acción para gestionar la lista negra:", "ui_bl_more": "\n... y {more} palabras más", + "ui_bl_only_regex": "🚫 Sin palabras clave, pero las reglas regex de abajo están activas y filtran.", "ui_bl_state_off": "Estado: **desactivado** (se guardan las palabras pero no se aplican).\n\n", "ui_bl_state_on": "Estado: **activado** (se aplican las palabras).\n\n", "ui_bl_title": "🚫 **Lista Negra de Palabras (Blacklist Keywords)**\n\n", @@ -2411,6 +2492,7 @@ "ui_block_video_on": "Bloqueo de vídeos activado 🚫", "ui_block_voice_off": "Notas de voz permitidas 🟢", "ui_block_voice_on": "Bloqueo de notas de voz activado 🚫", + "ui_brand_channel_saved": "📡 Salto solo de canales: {state}", "ui_brand_cleared": "🧹 Filtro de branding borrado — todos los mensajes harán el salto.", "ui_brand_force_saved": "🔁 Reenviar desde C: {state}", "ui_brand_gate_saved": "🚧 Condición solo reenviados: {state}", @@ -2424,6 +2506,7 @@ "ui_brand_words_saved": "✅ Palabras de la ruta guardadas: {value}", "ui_btn_ai_pv_assistant": "💬 Asistente inteligente para PV (AI PV Assistant)", "ui_btn_ar_assistant": "🤖 Respuesta automática con IA (mensajes privados)", + "ui_btn_prompts": "🧠 Biblioteca de prompts", "ui_btn_use_default_host": "✅ Usar predeterminado (o siguiente)", "ui_bulk_confirm": "¿Eliminar {n} regla(s) seleccionada(s)? No se puede deshacer.", "ui_bulk_delete": "🗑 Eliminar seleccionadas", @@ -3002,6 +3085,48 @@ "ui_plan_status_none": "0 días", "ui_prev": "◀️ Anterior", "ui_priority_alert": "Prioridad: {priority}", + "ui_prm_btn_back_list": "← Biblioteca", + "ui_prm_btn_edit_desc": "📝 Descripción", + "ui_prm_btn_edit_name": "✏️ Renombrar", + "ui_prm_btn_edit_sys": "🧩 Prompt del sistema", + "ui_prm_btn_edit_user": "✉️ Plantilla de usuario", + "ui_prm_btn_new": "➕ Nuevo prompt", + "ui_prm_created": "✅ Prompt «{name}» creado y guardado en tu biblioteca.", + "ui_prm_delete_confirm": "⚠️ ¿Eliminar el prompt «{name}»?\nLas tareas que lo usan pasarán al prompt de su configuración IA.", + "ui_prm_delete_yes": "🗑 Sí, eliminar", + "ui_prm_deleted": "🗑 Prompt eliminado.", + "ui_prm_detail_desc": "📝 {desc}", + "ui_prm_detail_lang": "🌐 Idioma de destino: {code}", + "ui_prm_detail_sys": "🧩 Prompt del sistema:\n{sys}", + "ui_prm_detail_title": "🧠 {name}", + "ui_prm_detail_user": "✉️ Plantilla de usuario:\n{user}", + "ui_prm_detail_version": "🔖 Versión: v{version}", + "ui_prm_disabled_alert": "🔴 Prompt apagado — los envíos vuelven al prompt de la configuración IA.", + "ui_prm_edit_desc_prompt": "📝 Envía la nueva descripción para «{name}» — envía - para borrarla.", + "ui_prm_edit_name_prompt": "✏️ Envía el nuevo nombre para «{name}».", + "ui_prm_edit_sys_prompt": "🧩 Envía el nuevo prompt del sistema para «{name}».", + "ui_prm_edit_user_prompt": "✉️ Envía la nueva plantilla de usuario para «{name}».\nDebe contener el marcador {text} — envía - para restablecerla a «{text}».", + "ui_prm_enabled_alert": "🟢 Prompt activado.", + "ui_prm_error_generic": "⚠️ No se pudo guardar el prompt — inténtalo de nuevo.", + "ui_prm_hint": "Toca un prompt para gestionarlo. Un prompt apagado sigue en la biblioteca, pero los envíos usan el prompt de la configuración IA de la tarea.", + "ui_prm_needs_text_placeholder": "✋ La plantilla de usuario debe contener el marcador {text}.", + "ui_prm_new_name_prompt": "🧠 Nuevo prompt — paso 1/3\n\n✍️ Envía el nombre del prompt.", + "ui_prm_new_sys_prompt": "🧠 Nuevo prompt — paso 2/3\n\n✍️ Envía el prompt del sistema: las instrucciones que la IA sigue en cada reescritura.", + "ui_prm_new_user_prompt": "🧠 Nuevo prompt — paso 3/3\n\n✍️ Envía la plantilla de usuario. Debe contener el marcador {text} — envía - para usar solo «{text}».", + "ui_prm_none": "Aún no hay plantillas de prompt. Toca ➕ Nuevo prompt para añadir una.", + "ui_prm_not_found": "Prompt no encontrado — puede que se haya eliminado.", + "ui_prm_saved": "✅ Guardado — ahora en v{version}.", + "ui_prm_state_off": "🔴 Apagado — los envíos usan el propio prompt de la configuración IA", + "ui_prm_state_on": "🟢 Activado — lo usan los envíos", + "ui_prm_system_immutable": "Los prompts del sistema son compartidos por todas las cuentas de esta instalación. Para personalizar uno, crea un prompt nuevo con el texto que necesites.", + "ui_prm_system_tag": "compartido", + "ui_prm_test_ask": "🧪 Envía un mensaje de ejemplo para reescribir con «{name}».", + "ui_prm_test_failed": "🧪 «{name}» falló ({code}):\n{message}", + "ui_prm_test_no_config": "No se encontró una configuración IA activada — añade una en el menú IA primero.", + "ui_prm_test_result": "🧪 «{name}» — resultado ({model}):\n\n{result}", + "ui_prm_title": "🧠 Plantillas de prompt — {count} · 🟢 {active}", + "ui_prm_too_long": "✋ Demasiado largo — acorta el texto y envíalo de nuevo.", + "ui_prm_unavailable": "El servicio de prompts no está disponible en esta instalación.", "ui_profile_actions": "👇 Acciones de la cuenta:", "ui_profile_reset_config": "⚠️ Restablecer la configuración de tareas", "ui_profile_restart_setup": "🔄 Reiniciar configuración", @@ -3013,7 +3138,6 @@ "ui_prompt_change_base_url": "🌐 **Cambiar dirección del servidor:**\n\nDirección actual: `{current}`\n\n✍️ Envía la nueva dirección:\n\nO envía `default` para restablecer:", "ui_prompt_change_model": "🧠 **Cambiar modelo de IA:**\n\nModelo actual: `{model}`\n\n✍️ Escriba el nombre del nuevo modelo:\n(Ejemplo: `coding` o `gpt-4o-mini`)", "ui_prompt_edit_entry": "✏️ Envía el nuevo valor para reemplazar `{entry}`:", - "ui_stale_entry": "⚠️ La lista cambió mientras el menú estaba abierto — mostrando el estado actual.", "ui_prompt_enter_base_url": "🌐 **Configurar dirección del servidor ({provider}):**\n\nDirección predeterminada:\n`{default_url}`\n\n✍️ Para usar un host personalizado, envía la dirección:\n\nO toca el botón para usar la predeterminada:", "ui_prompt_enter_delay": "⏱ **Introduce el retraso deseado (segundos):**\n\nEnvía el número de segundos (ej. `5` o `15`).", "ui_prompt_enter_every_n": "🔢 **Introduce el intervalo:**\n\nEnvía un número entero de 1 a 1000. `1` reenvía todo; `5` reenvía el 1.º, 6.º, 11.º … mensaje que supere los filtros.", @@ -3068,6 +3192,7 @@ "ui_reason_branding_always": "el branding está activo para todos los mensajes", "ui_reason_branding_disabled": "el branding está apagado ({reason})", "ui_reason_branding_no_triggers_configured": "el branding está activo sin disparadores, así que todos los mensajes saltan", + "ui_reason_branding_requires_channel_origin": "el branding solo salta mensajes que vienen de un canal", "ui_reason_branding_requires_forward_origin": "el branding solo salta mensajes que ya son reenvíos", "ui_reason_branding_triggers_matched": "el salto coincidió con sus disparadores ({words})", "ui_reason_branding_triggers_unmatched": "el salto no coincidió con ninguno de sus disparadores ({words})", @@ -3167,6 +3292,7 @@ "ui_reg_help_branding_match_mode": "any = basta un disparador, all = deben coincidir todos los configurados.", "ui_reg_help_branding_origin_ids": "Pasar solo si el mensaje se reenvió desde uno de estos ids/@nombres.", "ui_reg_help_branding_origin_titles": "Pasar solo si el mensaje se reenvió desde uno de estos canales.", + "ui_reg_help_branding_require_channel": "Rechaza el salto salvo que el mensaje venga de un canal — los usuarios normales se reenvían directamente con su propio nombre.", "ui_reg_help_branding_require_forward": "Rechazar el salto salvo que el mensaje sea en sí un reenvío.", "ui_reg_help_branding_triggers_enabled": "Apagado, todo mensaje coincidente pasa por C; encendido, deciden los disparadores de abajo.", "ui_reg_help_clean_custom_words": "Palabras que se quitan del texto.", @@ -3179,7 +3305,10 @@ "ui_reg_help_footer": "Texto que se añade al final de cada mensaje reenviado.", "ui_reg_help_forward_every_n": "Reenviar solo el N-ésimo mensaje que supera los filtros de esta tarea (1 = todos los mensajes, 1000 = uno de cada mil). Un álbum cuenta como un mensaje.", "ui_reg_help_header": "Texto que se añade al inicio de cada mensaje reenviado.", + "ui_reg_help_link_replace_enabled": "Reescribe cada enlace del texto según el mapa de reescritura de enlaces.", + "ui_reg_help_link_rewrite_map": "Pares de reescritura: {\"*\": \"https://t.me/yo\"} reescribe todos los enlaces, \"dominioviejo.com\": \"dominionuevo.com\" reescribe un dominio, \"https://viejo.t.me/x\": \"https://nuevo.t.me/y\" reescribe una URL completa.", "ui_reg_help_max_time_edit": "Segundos tras los cuales una edición ya no se refleja.", + "ui_reg_help_mention_rewrite_map": "Pares de reescritura de menciones: {\"@tiendavieja\": \"@tiendanueva\"} reescribe un alias (sin distinguir mayúsculas), \"*\": \"@principal\" reescribe todos los @ID.", "ui_reg_help_ocr_image_ai": "Ejecutar OCR en las imágenes y reenviar el texto extraído.", "ui_reg_help_power_control": "Vista cómoda de power_schedule.enabled.", "ui_reg_help_process_raw_text": "Procesar el texto sin formato antes de cualquier análisis.", @@ -3241,6 +3370,7 @@ "ui_reg_set_branding_match_mode": "Modo de coincidencia de marca", "ui_reg_set_branding_origin_ids": "IDs de origen", "ui_reg_set_branding_origin_titles": "Títulos de origen", + "ui_reg_set_branding_require_channel": "Salto solo de canales", "ui_reg_set_branding_require_forward": "Exigir reenvío", "ui_reg_set_branding_triggers_enabled": "Disparadores de marca", "ui_reg_set_button_to_text": "Botón a texto", @@ -3266,8 +3396,11 @@ "ui_reg_set_keyword_confirmation": "Confirmación por palabra", "ui_reg_set_keyword_reply": "Respuesta por palabra", "ui_reg_set_link_buttons": "Botones de enlace", + "ui_reg_set_link_replace_enabled": "Reemplazar cualquier enlace encontrado", "ui_reg_set_link_replacement": "Reemplazo de enlaces", + "ui_reg_set_link_rewrite_map": "Mapa de reescritura de enlaces", "ui_reg_set_max_time_edit": "Tiempo máximo de edición", + "ui_reg_set_mention_rewrite_map": "Mapa de reescritura de @menciones", "ui_reg_set_news_digest": "Resumen de noticias", "ui_reg_set_ocr_image_ai": "IA de OCR de imágenes", "ui_reg_set_paid_addons": "Complementos de pago", @@ -3352,6 +3485,26 @@ "ui_reset_config_done": "✅ Todos los ajustes se restablecieron correctamente.", "ui_retry": "🔄 Intentar de nuevo", "ui_retry_connect": "🚀 Reintentar conexión", + "ui_rmp_btn_edit_links": "🔗 Editar mapa de enlaces", + "ui_rmp_btn_edit_mentions": "❕ Editar mapa de @menciones", + "ui_rmp_btn_links_off": "🔗 Reemplazar enlaces: DESACTIVADO", + "ui_rmp_btn_links_on": "🔗 Reemplazar enlaces: ACTIVADO", + "ui_rmp_cleared": "✅ Mapa borrado.", + "ui_rmp_link_map": "🔗 Mapa de enlaces ({count}): {pairs}", + "ui_rmp_link_map_empty": "🔗 Mapa de enlaces: vacío", + "ui_rmp_link_state_off": "🔗 Reemplazo de enlaces: DESACTIVADO", + "ui_rmp_link_state_on": "🔗 Reemplazo de enlaces: ACTIVADO — los enlaces se reescriben según el mapa", + "ui_rmp_links_prompt": "🔗 Envía el mapa de reescritura de enlaces — un par por línea:\n`dominioviejo.com dominionuevo.com` — reescribe un dominio\n`https://viejo.t.me/x https://nuevo.t.me/y` — reescribe un enlace\n`* https://t.me/yo` — reescribe TODOS los enlaces\n\nEnvía - para borrar el mapa.", + "ui_rmp_mention_map": "❕ Mapa de menciones ({count}): {pairs}", + "ui_rmp_mention_map_empty": "❕ Mapa de menciones: vacío", + "ui_rmp_mentions_prompt": "❕ Envía el mapa de @menciones — un par por línea:\n`@tiendavieja @tiendanueva` — reescribe un alias\n`* @principal` — reescribe TODOS los @ID\n\nEnvía - para borrar el mapa.", + "ui_rmp_off_hint": "El mapa está configurado pero el reemplazo de enlaces está DESACTIVADO — actívalo para aplicarlo.", + "ui_rmp_rule": "Tarea: {name}", + "ui_rmp_saved": "✅ Guardado — {count} par(es).", + "ui_rmp_skipped": "✅ Guardado — {count} par(es). Se omitieron {skipped} línea(s) no válida(s).", + "ui_rmp_title": "🔁 Reemplazar enlaces y @menciones", + "ui_rmp_toggle_off": "🔴 Reemplazo de enlaces desactivado.", + "ui_rmp_toggle_on": "🟢 Reemplazo de enlaces activado — cada enlace encontrado se reescribe según el mapa.", "ui_route_add_sources": "📥 Orígenes ({count})", "ui_route_add_targets": "📤 Destinos ({count})", "ui_route_bad_edit": "❌ Envía `índice = valor`, o `nombre -> ` para una ruta nueva.", @@ -3729,11 +3882,14 @@ "ui_smart_active_path": "🛣 **Ruta activa del flujo de mensajes:**\n{path_desc}\n", "ui_smart_antiloop": "🔒 **Anti-bucle y anti-envío-doble:** activo con clave de idempotencia y verificación del origen del mensaje\n", "ui_smart_brand_block": "🔀 **Branding (A → C → B):**", + "ui_smart_brand_channel": "📡 Salto solo de canales: {state}", "ui_smart_brand_origins": "🎯 **Canales de origen:** {origins}", "ui_smart_brand_state": "🎛 **Filtro del salto:** {state}", "ui_smart_brand_state_all": "todos los mensajes", "ui_smart_brand_state_filtered": "filtrado", "ui_smart_brand_words": "🔑 **Palabras de la ruta:** {words}", + "ui_smart_btn_brand_chan_off": "📡 Salto solo de canales: DESACTIVADO", + "ui_smart_btn_brand_chan_on": "📡 Salto solo de canales: ACTIVADO", "ui_smart_btn_brand_force_off": "🔁 Reenviar desde C: OFF", "ui_smart_btn_brand_force_on": "🔁 Reenviar desde C: ON", "ui_smart_btn_brand_gate_off": "🚧 Solo reenviados: OFF", @@ -3750,6 +3906,7 @@ "ui_smart_btn_prio_down": "➖ Prioridad (−10)", "ui_smart_btn_prio_up": "➕ Prioridad (+10)", "ui_smart_btn_regex": "🔣 Definir Regex", + "ui_smart_btn_replace_maps": "🔁 Mapas de enlaces y @menciones", "ui_smart_custom_header": "🏷 **Encabezado de copia personalizado:** {header}", "ui_smart_hint": "👇 Para cambiar la configuración, toque las opciones a continuación:", "ui_smart_keywords": "🔎 **Palabras clave de texto:** {keywords}", @@ -3777,6 +3934,7 @@ "ui_src_desc": "Los mensajes nuevos de este chat se leerán y reenviarán.", "ui_src_sel": "📥 **Seleccionar Canal o Grupo de Origen (Source):**", "ui_src_selected": "✅ Origen: {title}", + "ui_stale_entry": "⚠️ La lista cambió mientras el menú estaba abierto — mostrando el estado actual.", "ui_stat_allowed_word": "Permitido 🟢", "ui_stat_clean_word": "Limpiar 🧹", "ui_stat_errors": "Errores", @@ -3951,10 +4109,10 @@ "ui_wl_count": "Cantidad de palabras permitidas: **{count}**\n", "ui_wl_desc": "Solo se reenvían los mensajes que contengan al menos una de estas entradas.\n\nTres formas de coincidir: **Palabra / Frase**, **Regex** o **Coincidencia de texto largo**.\n\n", "ui_wl_empty": "❌ No hay palabras en la lista blanca (se permiten todos los mensajes).", - "ui_wl_only_regex": "🔒 Sin palabras clave, pero las reglas regex de abajo están activas y filtran.", "ui_wl_entry_deleted_alert": "Eliminado: {entry}", "ui_wl_hint": "👇 Elige una acción para gestionar la lista blanca:", "ui_wl_more": "\n... y {more} palabras más", + "ui_wl_only_regex": "🔒 Sin palabras clave, pero las reglas regex de abajo están activas y filtran.", "ui_wl_state_off": "Estado: **desactivado** (se guardan las palabras pero no se aplican).\n\n", "ui_wl_state_on": "Estado: **activado** (se aplican las palabras).\n\n", "ui_wl_title": "✅ **Lista Blanca de Palabras (Whitelist Keywords)**\n\n", @@ -4206,6 +4364,7 @@ "ui_ai_btn_health": "🩺 تست سلامت (Ping)", "ui_ai_btn_key": "🔑 تغییر کلید (API Key)", "ui_ai_btn_model": "🧠 تغییر مدل", + "ui_ai_btn_open_prompts": "🧠 مدیریت پرامپت‌ها", "ui_ai_btn_pick_config": "🧠 انتخاب پیکربندی AI", "ui_ai_btn_pick_prompt": "📝 انتخاب پرامپت", "ui_ai_btn_sample": "🧪 تست بازنویسی متن", @@ -4272,6 +4431,7 @@ "ui_ai_prompt_enter_system_instruction": "🤖 دستورالعمل سیستمی را برای «{name}» ارسال کنید:\n\nمثال: `این پیام را با لحن رسمی و تجاری بازنویسی کن بدون اینکه اصل خبر تغییر کند.`", "ui_ai_prompt_label": "پرامپت", "ui_ai_prompt_name_invalid": "❌ نام پرامپت نامعتبر است. نام باید بین ۱ تا ۸۰ کاراکتر باشد.", + "ui_ai_prompt_off_tag": "خاموش", "ui_ai_prompt_staged_alert": "✅ پرامپت ثبت شد (در Save نهایی می‌آید)", "ui_ai_prompt_sys_empty_error": "❌ پرامپت سیستمی نمی‌تواند خالی باشد. لطفاً دستورالعمل بازنویسی را ارسال کنید.", "ui_ai_provider": "🧠 تامین‌کننده را بفرستید: openai, anthropic, gemini, groq, deepseek, 9router, openrouter, custom", @@ -4377,10 +4537,10 @@ "ui_bl_count": "تعداد کلمات مسدود: **{count}**\n", "ui_bl_desc": "پیام‌های دارای این موارد فرستاده **نمی‌شوند**.\n\nسه روش تطبیق: **کلمه/عبارت**، **Regex** یا **تطبیق متن بلند**.\n\n", "ui_bl_empty": "❌ هیچ کلمه‌ای در لیست سیاه نیست.", - "ui_bl_only_regex": "🚫 هیچ کلمه‌ای نیست — اما قوانین regex زیر فعال و در حال فیلتر کردن هستند.", "ui_bl_entry_deleted_alert": "حذف شد: {entry}", "ui_bl_hint": "👇 جهت مدیریت لیست سیاه انتخاب کنید:", "ui_bl_more": "\n... و {more} کلمه دیگر", + "ui_bl_only_regex": "🚫 هیچ کلمه‌ای نیست — اما قوانین regex زیر فعال و در حال فیلتر کردن هستند.", "ui_bl_state_off": "وضعیت: **غیرفعال** (کلمات نگه داشته می‌شوند اما اعمال نمی‌شوند).\n\n", "ui_bl_state_on": "وضعیت: **فعال** (کلمات اعمال می‌شوند).\n\n", "ui_bl_title": "🚫 **لیست سیاه کلمات (Blacklist Keywords)**\n\n", @@ -4398,6 +4558,7 @@ "ui_block_video_on": "مسدودسازی ویدیو فعال شد 🚫", "ui_block_voice_off": "ارسال وویس مجاز 🟢", "ui_block_voice_on": "مسدودسازی وویس فعال شد 🚫", + "ui_brand_channel_saved": "📡 پرش فقط برای کانال‌ها: {state}", "ui_brand_cleared": "🧹 فیلتر برندسازی پاک شد — همهٔ پیام‌ها پرش می‌کنند.", "ui_brand_force_saved": "🔁 فوروارد از C: {state}", "ui_brand_gate_saved": "🚧 شرط فقط فوروارد: {state}", @@ -4411,6 +4572,7 @@ "ui_brand_words_saved": "✅ کلمات مسیر ذخیره شد: {value}", "ui_btn_ai_pv_assistant": "💬 پاسخگوی هوشمند پی‌وی (AI PV Assistant)", "ui_btn_ar_assistant": "🤖 پاسخ خودکار هوش مصنوعی (پیام‌های خصوصی)", + "ui_btn_prompts": "🧠 کتابخانه پرامپت", "ui_btn_use_default_host": "✅ استفاده از پیش‌فرض (یا بعدی)", "ui_bulk_confirm": "{n} قانون انتخاب‌شده حذف شود؟ این کار برگشت‌پذیر نیست.", "ui_bulk_delete": "🗑 حذف انتخاب‌شده‌ها", @@ -4989,6 +5151,48 @@ "ui_plan_status_none": "۰ روز", "ui_prev": "◀️ قبلی", "ui_priority_alert": "اولویت: {priority}", + "ui_prm_btn_back_list": "← کتابخانه", + "ui_prm_btn_edit_desc": "📝 توضیحات", + "ui_prm_btn_edit_name": "✏️ تغییر نام", + "ui_prm_btn_edit_sys": "🧩 پرامپت سیستم", + "ui_prm_btn_edit_user": "✉️ قالب کاربر", + "ui_prm_btn_new": "➕ پرامپت جدید", + "ui_prm_created": "✅ پرامپت «{name}» ساخته و در کتابخانه شما ذخیره شد.", + "ui_prm_delete_confirm": "⚠️ پرامپت «{name}» حذف شود؟\nتسک‌هایی که به آن اشاره می‌کنند به پرامپت خود پیکربندی هوش مصنوعی برمی‌گردند.", + "ui_prm_delete_yes": "🗑 بله، حذف کن", + "ui_prm_deleted": "🗑 پرامپت حذف شد.", + "ui_prm_detail_desc": "📝 {desc}", + "ui_prm_detail_lang": "🌐 زبان هدف: {code}", + "ui_prm_detail_sys": "🧩 پرامپت سیستم:\n{sys}", + "ui_prm_detail_title": "🧠 {name}", + "ui_prm_detail_user": "✉️ قالب کاربر:\n{user}", + "ui_prm_detail_version": "🔖 نسخه: v{version}", + "ui_prm_disabled_alert": "🔴 پرامپت خاموش شد — ارسال‌ها به پرامپت خود پیکربندی هوش مصنوعی برمی‌گردند.", + "ui_prm_edit_desc_prompt": "📝 توضیحات جدید برای «{name}» را بفرستید — برای پاک کردن، - را بفرستید.", + "ui_prm_edit_name_prompt": "✏️ نام جدید برای «{name}» را بفرستید.", + "ui_prm_edit_sys_prompt": "🧩 پرامپت سیستم جدید برای «{name}» را بفرستید.", + "ui_prm_edit_user_prompt": "✉️ قالب کاربر جدید برای «{name}» را بفرستید.\nباید شامل جای‌نگهدار {text} باشد — برای بازنشانی به «{text}» علامت - را بفرستید.", + "ui_prm_enabled_alert": "🟢 پرامپت فعال شد.", + "ui_prm_error_generic": "⚠️ ذخیره پرامپت ممکن نشد — دوباره تلاش کنید.", + "ui_prm_hint": "برای مدیریت، روی یک پرامپت بزنید. پرامپت خاموش در کتابخانه می‌ماند اما ارسال‌ها از پرامپت پیکربندی هوش مصنوعی تسک استفاده می‌کنند.", + "ui_prm_needs_text_placeholder": "✋ قالب کاربر باید شامل جای‌نگهدار {text} باشد.", + "ui_prm_new_name_prompt": "🧠 پرامپت جدید — مرحله ۱ از ۳\n\n✍️ نام پرامپت را بفرستید.", + "ui_prm_new_sys_prompt": "🧠 پرامپت جدید — مرحله ۲ از ۳\n\n✍️ پرامپت سیستم را بفرستید: دستورالعملی که هوش مصنوعی در هر بازنویسی دنبال می‌کند.", + "ui_prm_new_user_prompt": "🧠 پرامپت جدید — مرحله ۳ از ۳\n\n✍️ قالب کاربر را بفرستید. باید شامل جای‌نگهدار {text} باشد — برای استفاده از فقط «{text}» علامت - را بفرستید.", + "ui_prm_none": "هنوز قالب پرامپتی وجود ندارد. برای افزودن، ➕ پرامپت جدید را بزنید.", + "ui_prm_not_found": "پرامپت پیدا نشد — ممکن است حذف شده باشد.", + "ui_prm_saved": "✅ ذخیره شد — اکنون در نسخه v{version}.", + "ui_prm_state_off": "🔴 خاموش — ارسال‌ها از پرامپت خود پیکربندی هوش مصنوعی استفاده می‌کنند", + "ui_prm_state_on": "🟢 فعال — در ارسال‌ها استفاده می‌شود", + "ui_prm_system_immutable": "پرامپت‌های سیستمی بین همه حساب‌های این نصب مشترک‌اند. برای شخصی‌سازی، یک پرامپت جدید با متن دلخواه بسازید.", + "ui_prm_system_tag": "اشتراکی", + "ui_prm_test_ask": "🧪 یک پیام نمونه برای بازنویسی با «{name}» بفرستید.", + "ui_prm_test_failed": "🧪 «{name}» شکست خورد ({code}):\n{message}", + "ui_prm_test_no_config": "هیچ پیکربندی هوش مصنوعی فعالی پیدا نشد — ابتدا از منوی هوش مصنوعی یکی اضافه کنید.", + "ui_prm_test_result": "🧪 «{name}» — نتیجه ({model}):\n\n{result}", + "ui_prm_title": "🧠 قالب‌های پرامپت — {count} · 🟢 {active}", + "ui_prm_too_long": "✋ بیش از حد طولانی است — متن را کوتاه‌تر کنيد و دوباره بفرستید.", + "ui_prm_unavailable": "سرویس پرامپت در این نصب در دسترس نیست.", "ui_profile_actions": "👇 عملیات حساب:", "ui_profile_reset_config": "⚠️ بازنشانی تنظیمات تسک‌ها", "ui_profile_restart_setup": "🔄 راه‌اندازی مجدد", @@ -5000,7 +5204,6 @@ "ui_prompt_change_base_url": "🌐 **تغییر آدرس سرور / هاست:**\n\nآدرس فعلی: `{current}`\n\n✍️ لطفاً آدرس هاست جدید را ارسال کنید:\n\nیا برای بازنشانی به حالت پیش‌فرض عبارت `default` را بفرستید:", "ui_prompt_change_model": "🧠 **تغییر مدل هوش مصنوعی:**\n\nمدل فعلی: `{model}`\n\n✍️ لطفاً نام مدل جدید را تایپ و ارسال کنید:\n(مثال: `coding` یا `gpt-4o-mini`)", "ui_prompt_edit_entry": "✏️ مقدار جدید برای جایگزینی `{entry}` را ارسال کنید:", - "ui_stale_entry": "⚠️ لیست در حالی که منو باز بود تغییر کرد — وضعیت فعلی نمایش داده می‌شود.", "ui_prompt_enter_base_url": "🌐 **تنظیم آدرس سرور / هاست ({provider}):**\n\nآدرس پیش‌فرض:\n`{default_url}`\n\n✍️ اگر مایلید از هاست اختصاصی یا پورت دیگری استفاده کنید، آدرس آن را ارسال نمایید:\n\nیا برای استفاده از آدرس پیش‌فرض دکمه زیر را لمس کنید:", "ui_prompt_enter_delay": "⏱ **ورود میزان تأخیر دلخواه (Delay)**\n\nلطفاً عدد ثانیه تأخیر موردنظر را ارسال کنید (مثلاً `5` یا `15`).", "ui_prompt_enter_every_n": "🔢 **بازه را وارد کنید:**\n\nیک عدد صحیح از ۱ تا ۱۰۰۰ بفرستید. `1` یعنی همه پیام‌ها؛ `5` یعنی پیام ۱، ۶، ۱۱ … از پیام‌هایی که از فیلترها عبور می‌کنند.", @@ -5055,6 +5258,7 @@ "ui_reason_branding_always": "برندینگ برای همه پیام‌ها فعال است", "ui_reason_branding_disabled": "برندینگ خاموش است ({reason})", "ui_reason_branding_no_triggers_configured": "برندینگ بدون هیچ محرکی روشن است، پس همه پیام‌ها عبور می‌کنند", + "ui_reason_branding_requires_channel_origin": "برندینگ فقط پیام‌هایی را می‌فرستد که از یک کانال می‌آیند", "ui_reason_branding_requires_forward_origin": "برندینگ فقط پیام‌هایی را می‌فرستد که خودشان فوروارد باشند", "ui_reason_branding_triggers_matched": "عبور با محرک‌هایش تطبیق داشت ({words})", "ui_reason_branding_triggers_unmatched": "عبور با هیچ‌یک از محرک‌هایش تطبیق نداشت ({words})", @@ -5154,6 +5358,7 @@ "ui_reg_help_branding_match_mode": "any = یک محرک کافی است، all = همه محرک‌های تنظیم‌شده باید منطبق شوند.", "ui_reg_help_branding_origin_ids": "عبور فقط وقتی پیام از یکی از این شناسه‌ها/یوزرنیم‌ها فوروارد شده باشد.", "ui_reg_help_branding_origin_titles": "عبور فقط وقتی پیام از یکی از این نام‌های کانال فوروارد شده باشد.", + "ui_reg_help_branding_require_channel": "پرش را رد می‌کند مگر اینکه پیام از یک کانال بیاید — کاربران عادی مستقیماً با نام خودشان فوروارد می‌شوند.", "ui_reg_help_branding_require_forward": "عبور رد می‌شود مگر آنکه خود پیام فوروارد باشد.", "ui_reg_help_branding_triggers_enabled": "وقتی خاموش است هر پیام منطبق از مسیر C عبور می‌کند؛ وقتی روشن است، محرک‌های زیر تصمیم می‌گیرند.", "ui_reg_help_clean_custom_words": "کلماتی که از متن حذف می‌شوند.", @@ -5166,7 +5371,10 @@ "ui_reg_help_footer": "متنی که به انتهای هر پیام فوروارد‌شده اضافه می‌شود.", "ui_reg_help_forward_every_n": "فقط هر N-امین پیامی که از فیلترهای این تسک عبور می‌کند فوروارد شود (۱ = همه پیام‌ها، ۱۰۰۰ = یکی از هزار). هر آلبوم یک پیام شمرده می‌شود.", "ui_reg_help_header": "متنی که به ابتدای هر پیام فوروارد‌شده اضافه می‌شود.", + "ui_reg_help_link_replace_enabled": "هر لینک موجود در متن را بر اساس نقشه بازنویسی لینک بازنویسی می‌کند.", + "ui_reg_help_link_rewrite_map": "جفت‌های بازنویسی لینک: {\"*\": \"https://t.me/man\"} همه لینک‌ها را بازنویسی می‌کند، \"دامنه-قدیم.com\": \"دامنه-جدید.com\" یک دامنه را بازنویسی می‌کند، \"https://ghadim.t.me/x\": \"https://jadid.t.me/y\" یک URL کامل را بازنویسی می‌کند.", "ui_reg_help_max_time_edit": "پس از این تعداد ثانیه، ویرایش‌ها دیگر منتقل نمی‌شوند.", + "ui_reg_help_mention_rewrite_map": "جفت‌های بازنویسی منشن: {\"@maghaze-ghadim\": \"@maghaze-jadid\"} یک آیدی را بازنویسی می‌کند (بی‌تفاوت به بزرگی حروف)، \"*\": \"@asli\" همه @آیدی‌ها را بازنویسی می‌کند.", "ui_reg_help_ocr_image_ai": "اجرای OCR روی تصاویر و فوروارد متن استخراج‌شده.", "ui_reg_help_power_control": "نمای میانبر از power_schedule.enabled.", "ui_reg_help_process_raw_text": "اجرای خط پردازش روی متن خام، پیش از هر تجزیه.", @@ -5228,6 +5436,7 @@ "ui_reg_set_branding_match_mode": "حالت تطبیق برندینگ", "ui_reg_set_branding_origin_ids": "شناسه‌های مبدأ", "ui_reg_set_branding_origin_titles": "عنوان‌های مبدأ", + "ui_reg_set_branding_require_channel": "پرش فقط برای کانال‌ها", "ui_reg_set_branding_require_forward": "الزام فوروارد", "ui_reg_set_branding_triggers_enabled": "محرک‌های برندینگ", "ui_reg_set_button_to_text": "تبدیل دکمه به متن", @@ -5253,8 +5462,11 @@ "ui_reg_set_keyword_confirmation": "تأیید با کلیدواژه", "ui_reg_set_keyword_reply": "پاسخ با کلیدواژه", "ui_reg_set_link_buttons": "دکمه‌های لینک", + "ui_reg_set_link_replace_enabled": "جایگزینی هر لینک پیدا شده", "ui_reg_set_link_replacement": "جایگزینی لینک", + "ui_reg_set_link_rewrite_map": "نقشه بازنویسی لینک", "ui_reg_set_max_time_edit": "حداکثر زمان ویرایش", + "ui_reg_set_mention_rewrite_map": "نقشه بازنویسی @منشن", "ui_reg_set_news_digest": "خلاصه اخبار", "ui_reg_set_ocr_image_ai": "هوش مصنوعی متن‌خوانی تصویر", "ui_reg_set_paid_addons": "افزونه‌های پرداختی", @@ -5339,6 +5551,26 @@ "ui_reset_config_done": "✅ همه تنظیمات با موفقیت بازنشانی شدند.", "ui_retry": "🔄 تلاش مجدد", "ui_retry_connect": "🚀 تلاش مجدد برای اتصال", + "ui_rmp_btn_edit_links": "🔗 ویرایش نقشه لینک‌ها", + "ui_rmp_btn_edit_mentions": "❕ ویرایش نقشه @منشن‌ها", + "ui_rmp_btn_links_off": "🔗 جایگزینی لینک‌ها: خاموش", + "ui_rmp_btn_links_on": "🔗 جایگزینی لینک‌ها: روشن", + "ui_rmp_cleared": "✅ نقشه پاک شد.", + "ui_rmp_link_map": "🔗 نقشه لینک‌ها ({count}): {pairs}", + "ui_rmp_link_map_empty": "🔗 نقشه لینک‌ها: خالی", + "ui_rmp_link_state_off": "🔗 جایگزینی لینک: خاموش", + "ui_rmp_link_state_on": "🔗 جایگزینی لینک: روشن — لینک‌ها بر اساس نقشه بازنویسی می‌شوند", + "ui_rmp_links_prompt": "🔗 نقشه بازنویسی لینک‌ها را بفرستید — هر جفت در یک خط:\n`damane-ghadim.com damane-jadid.com` — بازنویسی یک دامنه\n`https://ghadim.t.me/x https://jadid.t.me/y` — بازنویسی یک لینک\n`* https://t.me/man` — بازنویسی همه لینک‌ها\n\nبرای پاک کردن نقشه، - را بفرستید.", + "ui_rmp_mention_map": "❕ نقشه منشن‌ها ({count}): {pairs}", + "ui_rmp_mention_map_empty": "❕ نقشه منشن‌ها: خالی", + "ui_rmp_mentions_prompt": "❕ نقشه @منشن‌ها را بفرستید — هر جفت در یک خط:\n`@maghaze-ghadim @maghaze-jadid` — بازنویسی یک آیدی\n`* @asli` — بازنویسی همه @آیدی‌ها\n\nبرای پاک کردن نقشه، - را بفرستید.", + "ui_rmp_off_hint": "نقشه تنظیم شده اما جایگزینی لینک خاموش است — برای اعمال، آن را روشن کنید.", + "ui_rmp_rule": "تسک: {name}", + "ui_rmp_saved": "✅ ذخیره شد — {count} جفت.", + "ui_rmp_skipped": "✅ ذخیره شد — {count} جفت. {skipped} خط نامعتبر نادیده گرفته شد.", + "ui_rmp_title": "🔁 جایگزینی لینک‌ها و @منشن‌ها", + "ui_rmp_toggle_off": "🔴 جایگزینی لینک خاموش شد.", + "ui_rmp_toggle_on": "🟢 جایگزینی لینک فعال شد — هر لینکی که پیدا شود بر اساس نقشه بازنویسی می‌شود.", "ui_route_add_sources": "📥 منابع ({count})", "ui_route_add_targets": "📤 مقصدها ({count})", "ui_route_bad_edit": "❌ بفرستید `index = value`، یا برای مسیر جدید `name -> `.", @@ -5716,11 +5948,14 @@ "ui_smart_active_path": "🛣 **مسیر فعال جریان پیام:**\n{path_desc}\n", "ui_smart_antiloop": "🔒 **ضد لوپ و ضد دابل‌سند:** فعال با بررسی کلید یکتایی و منشأ پیام\n", "ui_smart_brand_block": "🔀 **پرش برندسازی (A → C → B):**", + "ui_smart_brand_channel": "📡 پرش فقط برای کانال‌ها: {state}", "ui_smart_brand_origins": "🎯 **کانال‌های مبدأ:** {origins}", "ui_smart_brand_state": "🎛 **فیلتر پرش:** {state}", "ui_smart_brand_state_all": "همهٔ پیام‌ها", "ui_smart_brand_state_filtered": "فیلترشده", "ui_smart_brand_words": "🔑 **کلمات مسیر:** {words}", + "ui_smart_btn_brand_chan_off": "📡 پرش فقط برای کانال‌ها: خاموش", + "ui_smart_btn_brand_chan_on": "📡 پرش فقط برای کانال‌ها: روشن", "ui_smart_btn_brand_force_off": "🔁 فوروارد از C: خاموش", "ui_smart_btn_brand_force_on": "🔁 فوروارد از C: روشن", "ui_smart_btn_brand_gate_off": "🚧 فقط فوروارد: خاموش", @@ -5737,6 +5972,7 @@ "ui_smart_btn_prio_down": "➖ اولویت (-10)", "ui_smart_btn_prio_up": "➕ اولویت (+10)", "ui_smart_btn_regex": "🔣 تنظیم Regex", + "ui_smart_btn_replace_maps": "🔁 نقشه لینک‌ها و @منشن‌ها", "ui_smart_custom_header": "🏷 **هدر اختصاصی کپی:** {header}", "ui_smart_hint": "👇 جهت تغییر تنظیمات، گزینه‌های زیر را لمس کنید:", "ui_smart_keywords": "🔎 **کلیدواژه‌های متنی:** {keywords}", @@ -5764,6 +6000,7 @@ "ui_src_desc": "پیام‌های جدید از این گفتگو خوانده و رله خواهند شد.", "ui_src_sel": "📥 **انتخاب کانال یا گروه مبدأ (Source):**", "ui_src_selected": "✅ مبدأ: {title}", + "ui_stale_entry": "⚠️ لیست در حالی که منو باز بود تغییر کرد — وضعیت فعلی نمایش داده می‌شود.", "ui_stat_allowed_word": "مجاز 🟢", "ui_stat_clean_word": "پاکسازی 🧹", "ui_stat_errors": "خطاها", @@ -5938,10 +6175,10 @@ "ui_wl_count": "تعداد کلمات مجاز: **{count}**\n", "ui_wl_desc": "تنها پیام‌های دارای حداقل یکی از این موارد فرستاده می‌شوند.\n\nسه روش تطبیق: **کلمه/عبارت**، **Regex** یا **تطبیق متن بلند**.\n\n", "ui_wl_empty": "❌ هیچ کلمه‌ای در لیست سفید نیست (تمام پیام‌ها مجازند).", - "ui_wl_only_regex": "🔒 هیچ کلمه‌ای نیست — اما قوانین regex زیر فعال و در حال فیلتر کردن هستند.", "ui_wl_entry_deleted_alert": "حذف شد: {entry}", "ui_wl_hint": "👇 جهت مدیریت لیست سفید انتخاب کنید:", "ui_wl_more": "\n... و {more} کلمه دیگر", + "ui_wl_only_regex": "🔒 هیچ کلمه‌ای نیست — اما قوانین regex زیر فعال و در حال فیلتر کردن هستند.", "ui_wl_state_off": "وضعیت: **غیرفعال** (کلمات نگه داشته می‌شوند اما اعمال نمی‌شوند).\n\n", "ui_wl_state_on": "وضعیت: **فعال** (کلمات اعمال می‌شوند).\n\n", "ui_wl_title": "✅ **لیست سفید کلمات (Whitelist Keywords)**\n\n", @@ -6193,6 +6430,7 @@ "ui_ai_btn_health": "🩺 Vérification (Ping)", "ui_ai_btn_key": "🔑 Changer la clé (API Key)", "ui_ai_btn_model": "🧠 Changer de modèle", + "ui_ai_btn_open_prompts": "🧠 Gérer les prompts", "ui_ai_btn_pick_config": "🧠 Sélectionner la configuration IA", "ui_ai_btn_pick_prompt": "📝 Choisir le prompt", "ui_ai_btn_sample": "🧪 Tester la réécriture du texte", @@ -6259,6 +6497,7 @@ "ui_ai_prompt_enter_system_instruction": "🤖 Entrez les instructions système pour '{name}' :\n\nExemple : `Réécrivez ce message sur un ton professionnel sans modifier les faits.`", "ui_ai_prompt_label": "Prompt", "ui_ai_prompt_name_invalid": "❌ Nom de prompt invalide. Veuillez entrer entre 1 et 80 caractères.", + "ui_ai_prompt_off_tag": "désactivé", "ui_ai_prompt_staged_alert": "✅ Prompt sélectionné (enregistré lors de la sauvegarde)", "ui_ai_prompt_sys_empty_error": "❌ Le prompt système ne peut pas être vide.", "ui_ai_provider": "🧠 Envoyez le fournisseur : openai, anthropic, gemini, groq, deepseek, 9router, openrouter, custom", @@ -6364,10 +6603,10 @@ "ui_bl_count": "Nombre de mots bloqués : **{count}**\n", "ui_bl_desc": "Les messages contenant l'une de ces entrées ne sont **PAS** transférés.\n\nTrois modes de correspondance : **Mot / Phrase**, **Regex** ou **Correspondance de texte long**.\n\n", "ui_bl_empty": "❌ Aucun mot dans la liste noire.", - "ui_bl_only_regex": "🚫 Aucun mot-clé — mais les règles regex ci-dessous sont actives et filtrent.", "ui_bl_entry_deleted_alert": "Supprimé : {entry}", "ui_bl_hint": "👇 Choisissez une action pour gérer la liste noire :", "ui_bl_more": "\n... et {more} mots de plus", + "ui_bl_only_regex": "🚫 Aucun mot-clé — mais les règles regex ci-dessous sont actives et filtrent.", "ui_bl_state_off": "État : **désactivé** (les mots sont conservés mais non appliqués).\n\n", "ui_bl_state_on": "État : **activé** (les mots sont appliqués).\n\n", "ui_bl_title": "🚫 **Liste Noire de Mots (Blacklist Keywords)**\n\n", @@ -6385,6 +6624,7 @@ "ui_block_video_on": "Blocage des vidéos activé 🚫", "ui_block_voice_off": "Messages vocaux autorisés 🟢", "ui_block_voice_on": "Blocage des messages vocaux activé 🚫", + "ui_brand_channel_saved": "📡 Saut canaux uniquement : {state}", "ui_brand_cleared": "🧹 Filtre de branding effacé — tous les messages feront le saut.", "ui_brand_force_saved": "🔁 Transfert depuis C : {state}", "ui_brand_gate_saved": "🚧 Condition transferts uniquement : {state}", @@ -6398,6 +6638,7 @@ "ui_brand_words_saved": "✅ Mots du chemin enregistrés : {value}", "ui_btn_ai_pv_assistant": "💬 Assistant intelligent pour MP (AI PV Assistant)", "ui_btn_ar_assistant": "🤖 Réponse automatique IA (messages privés)", + "ui_btn_prompts": "🧠 Bibliothèque de prompts", "ui_btn_use_default_host": "✅ Utiliser par défaut (ou suivant)", "ui_bulk_confirm": "Supprimer {n} règle(s) sélectionnée(s) ? Action irréversible.", "ui_bulk_delete": "🗑 Supprimer la sélection", @@ -6976,6 +7217,48 @@ "ui_plan_status_none": "0 jour", "ui_prev": "◀️ Précédent", "ui_priority_alert": "Priorité : {priority}", + "ui_prm_btn_back_list": "← Bibliothèque", + "ui_prm_btn_edit_desc": "📝 Descriptif", + "ui_prm_btn_edit_name": "✏️ Renommer", + "ui_prm_btn_edit_sys": "🧩 Prompt système", + "ui_prm_btn_edit_user": "✉️ Modèle utilisateur", + "ui_prm_btn_new": "➕ Nouveau prompt", + "ui_prm_created": "✅ Prompt « {name} » créé et enregistré dans votre bibliothèque.", + "ui_prm_delete_confirm": "⚠️ Supprimer le prompt « {name} » ?\nLes tâches qui l'utilisent reviendront au prompt de leur configuration IA.", + "ui_prm_delete_yes": "🗑 Oui, supprimer", + "ui_prm_deleted": "🗑 Prompt supprimé.", + "ui_prm_detail_desc": "📝 {desc}", + "ui_prm_detail_lang": "🌐 Langue cible : {code}", + "ui_prm_detail_sys": "🧩 Prompt système :\n{sys}", + "ui_prm_detail_title": "🧠 {name}", + "ui_prm_detail_user": "✉️ Modèle utilisateur :\n{user}", + "ui_prm_detail_version": "🔖 Version : v{version}", + "ui_prm_disabled_alert": "🔴 Prompt désactivé — les envois reviennent au prompt de la configuration IA.", + "ui_prm_edit_desc_prompt": "📝 Envoyez la nouvelle description de « {name} » — envoyez - pour l'effacer.", + "ui_prm_edit_name_prompt": "✏️ Envoyez le nouveau nom de « {name} ».", + "ui_prm_edit_sys_prompt": "🧩 Envoyez le nouveau prompt système de « {name} ».", + "ui_prm_edit_user_prompt": "✉️ Envoyez le nouveau modèle utilisateur de « {name} ».\nIl doit contenir le marqueur {text} — envoyez - pour le réinitialiser à « {text} ».", + "ui_prm_enabled_alert": "🟢 Prompt activé.", + "ui_prm_error_generic": "⚠️ Impossible d'enregistrer le prompt — réessayez.", + "ui_prm_hint": "Touchez un prompt pour le gérer. Un prompt désactivé reste dans la bibliothèque, mais les envois utilisent le prompt de la configuration IA de la tâche.", + "ui_prm_needs_text_placeholder": "✋ Le modèle utilisateur doit contenir le marqueur {text}.", + "ui_prm_new_name_prompt": "🧠 Nouveau prompt — étape 1/3\n\n✍️ Envoyez le nom du prompt.", + "ui_prm_new_sys_prompt": "🧠 Nouveau prompt — étape 2/3\n\n✍️ Envoyez le prompt système : les instructions que l'IA suit à chaque réécriture.", + "ui_prm_new_user_prompt": "🧠 Nouveau prompt — étape 3/3\n\n✍️ Envoyez le modèle utilisateur. Il doit contenir le marqueur {text} — envoyez - pour utiliser simplement « {text} ».", + "ui_prm_none": "Aucun modèle de prompt pour l'instant. Touchez ➕ Nouveau prompt pour en ajouter un.", + "ui_prm_not_found": "Prompt introuvable — il a peut-être été supprimé.", + "ui_prm_saved": "✅ Enregistré — désormais en v{version}.", + "ui_prm_state_off": "🔴 Désactivé — les envois utilisent le prompt propre à la configuration IA", + "ui_prm_state_on": "🟢 Activé — utilisé par les envois", + "ui_prm_system_immutable": "Les prompts système sont partagés par tous les comptes de cette installation. Pour en personnaliser un, créez un nouveau prompt avec la formulation souhaitée.", + "ui_prm_system_tag": "partagé", + "ui_prm_test_ask": "🧪 Envoyez un message d'exemple à réécrire avec « {name} ».", + "ui_prm_test_failed": "🧪 « {name} » a échoué ({code}) :\n{message}", + "ui_prm_test_no_config": "Aucune configuration IA activée trouvée — ajoutez-en une dans le menu IA d'abord.", + "ui_prm_test_result": "🧪 « {name} » — résultat ({model}) :\n\n{result}", + "ui_prm_title": "🧠 Modèles de prompt — {count} · 🟢 {active}", + "ui_prm_too_long": "✋ Trop long — raccourcissez le texte et renvoyez-le.", + "ui_prm_unavailable": "Le service de prompts n'est pas disponible sur cette installation.", "ui_profile_actions": "👇 Actions du compte :", "ui_profile_reset_config": "⚠️ Réinitialiser les paramètres des tâches", "ui_profile_restart_setup": "🔄 Relancer la configuration", @@ -6987,7 +7270,6 @@ "ui_prompt_change_base_url": "🌐 **Modifier l'adresse du serveur :**\n\nAdresse actuelle : `{current}`\n\n✍️ Envoyez la nouvelle adresse :\n\nOu envoyez `default` pour réinitialiser :", "ui_prompt_change_model": "🧠 **Changer le modèle IA :**\n\nModèle actuel : `{model}`\n\n✍️ Tapez le nouveau nom de modèle :\n(Exemple : `coding` ou `gpt-4o-mini`)", "ui_prompt_edit_entry": "✏️ Envoyez la nouvelle valeur pour remplacer `{entry}` :", - "ui_stale_entry": "⚠️ La liste a changé pendant que le menu était ouvert — affichage de l'état actuel.", "ui_prompt_enter_base_url": "🌐 **Configurer l'adresse du serveur ({provider}) :**\n\nAdresse par défaut :\n`{default_url}`\n\n✍️ Pour un hôte personnalisé, envoyez l'adresse :\n\nOu appuyez sur le bouton pour utiliser celle par défaut :", "ui_prompt_enter_delay": "⏱ **Entrez le délai souhaité (secondes) :**\n\nVeuillez envoyer le délai en secondes (ex. `5` ou `15`).", "ui_prompt_enter_every_n": "🔢 **Indiquez l'intervalle :**\n\nEnvoyez un nombre entier de 1 à 1000. `1` transfère tout ; `5` transfère le 1er, 6e, 11e … message qui passe les filtres.", @@ -7042,6 +7324,7 @@ "ui_reason_branding_always": "le branding est actif pour tous les messages", "ui_reason_branding_disabled": "le branding est éteint ({reason})", "ui_reason_branding_no_triggers_configured": "le branding est actif sans déclencheur, donc tous les messages passent", + "ui_reason_branding_requires_channel_origin": "le branding ne saute que les messages qui viennent d'un canal", "ui_reason_branding_requires_forward_origin": "le branding ne fait passer que les messages déjà transférés", "ui_reason_branding_triggers_matched": "le saut a correspondu à ses déclencheurs ({words})", "ui_reason_branding_triggers_unmatched": "le saut n'a correspondu à aucun de ses déclencheurs ({words})", @@ -7141,6 +7424,7 @@ "ui_reg_help_branding_match_mode": "any = un déclencheur suffit, all = tous les déclencheurs configurés doivent correspondre.", "ui_reg_help_branding_origin_ids": "Passer seulement si le message a été transféré depuis l'un de ces id/@noms.", "ui_reg_help_branding_origin_titles": "Passer seulement si le message a été transféré depuis l'un de ces canaux.", + "ui_reg_help_branding_require_channel": "Refuse le saut sauf si le message vient d'un canal — les utilisateurs ordinaires sont transférés directement avec leur propre nom.", "ui_reg_help_branding_require_forward": "Refuser le saut sauf si le message est lui-même un transfert.", "ui_reg_help_branding_triggers_enabled": "Désactivé, chaque message correspondant passe par C ; activé, les déclencheurs ci-dessous décident.", "ui_reg_help_clean_custom_words": "Mots supprimés du texte.", @@ -7153,7 +7437,10 @@ "ui_reg_help_footer": "Texte ajouté à la fin de chaque message transféré.", "ui_reg_help_forward_every_n": "Ne transférer qu'un message sur N parmi ceux qui passent les filtres de cette tâche (1 = tous les messages, 1000 = un sur mille). Un album compte pour un message.", "ui_reg_help_header": "Texte ajouté au début de chaque message transféré.", + "ui_reg_help_link_replace_enabled": "Réécrit chaque lien du texte selon la table de réécriture des liens.", + "ui_reg_help_link_rewrite_map": "Paires de réécriture : {\"*\": \"https://t.me/moi\"} réécrit tous les liens, \"vieuxdomaine.com\": \"nouveaudomaine.com\" réécrit un domaine, \"https://vieux.t.me/x\": \"https://nouveau.t.me/y\" réécrit une URL complète.", "ui_reg_help_max_time_edit": "Délai en secondes après lequel une modification n'est plus répercutée.", + "ui_reg_help_mention_rewrite_map": "Paires de réécriture de mentions : {\"@vieuxboutique\": \"@nouveauboutique\"} réécrit un identifiant (insensible à la casse), \"*\": \"@principal\" réécrit tous les @ID.", "ui_reg_help_ocr_image_ai": "Lancer l'OCR sur les images et transférer le texte extrait.", "ui_reg_help_power_control": "Vue pratique de power_schedule.enabled.", "ui_reg_help_process_raw_text": "Traiter le texte brut avant toute analyse.", @@ -7215,6 +7502,7 @@ "ui_reg_set_branding_match_mode": "Mode de correspondance de marque", "ui_reg_set_branding_origin_ids": "ID d'origine", "ui_reg_set_branding_origin_titles": "Titres d'origine", + "ui_reg_set_branding_require_channel": "Saut canaux uniquement", "ui_reg_set_branding_require_forward": "Exiger le transfert", "ui_reg_set_branding_triggers_enabled": "Déclencheurs de marque", "ui_reg_set_button_to_text": "Bouton en texte", @@ -7240,8 +7528,11 @@ "ui_reg_set_keyword_confirmation": "Confirmation par mot-clé", "ui_reg_set_keyword_reply": "Réponse par mot-clé", "ui_reg_set_link_buttons": "Boutons de lien", + "ui_reg_set_link_replace_enabled": "Remplacer tout lien trouvé", "ui_reg_set_link_replacement": "Remplacement de liens", + "ui_reg_set_link_rewrite_map": "Table de réécriture des liens", "ui_reg_set_max_time_edit": "Délai max. de modification", + "ui_reg_set_mention_rewrite_map": "Table de réécriture des @mentions", "ui_reg_set_news_digest": "Résumé d'actualités", "ui_reg_set_ocr_image_ai": "IA OCR des images", "ui_reg_set_paid_addons": "Modules payants", @@ -7326,6 +7617,26 @@ "ui_reset_config_done": "✅ Tous les paramètres ont été réinitialisés.", "ui_retry": "🔄 Réessayer", "ui_retry_connect": "🚀 Réessayer la connexion", + "ui_rmp_btn_edit_links": "🔗 Modifier la table de liens", + "ui_rmp_btn_edit_mentions": "❕ Modifier la table de @mentions", + "ui_rmp_btn_links_off": "🔗 Remplacer les liens : DÉSACTIVÉ", + "ui_rmp_btn_links_on": "🔗 Remplacer les liens : ACTIVÉ", + "ui_rmp_cleared": "✅ Table vidée.", + "ui_rmp_link_map": "🔗 Table de liens ({count}) : {pairs}", + "ui_rmp_link_map_empty": "🔗 Table de liens : vide", + "ui_rmp_link_state_off": "🔗 Remplacement de liens : DÉSACTIVÉ", + "ui_rmp_link_state_on": "🔗 Remplacement de liens : ACTIVÉ — les liens sont réécrits selon la table", + "ui_rmp_links_prompt": "🔗 Envoyez la table de réécriture des liens — une paire par ligne :\n`vieuxdomaine.com nouveaudomaine.com` — réécrit un domaine\n`https://vieux.t.me/x https://nouveau.t.me/y` — réécrit un lien\n`* https://t.me/moi` — réécrit TOUS les liens\n\nEnvoyez - pour vider la table.", + "ui_rmp_mention_map": "❕ Table de mentions ({count}) : {pairs}", + "ui_rmp_mention_map_empty": "❕ Table de mentions : vide", + "ui_rmp_mentions_prompt": "❕ Envoyez la table de @mentions — une paire par ligne :\n`@vieuxboutique @nouveauboutique` — réécrit un identifiant\n`* @principal` — réécrit TOUS les @ID\n\nEnvoyez - pour vider la table.", + "ui_rmp_off_hint": "La table est définie mais le remplacement de liens est DÉSACTIVÉ — activez-le pour l'appliquer.", + "ui_rmp_rule": "Tâche : {name}", + "ui_rmp_saved": "✅ Enregistré — {count} paire(s).", + "ui_rmp_skipped": "✅ Enregistré — {count} paire(s). {skipped} ligne(s) invalide(s) ignorée(s).", + "ui_rmp_title": "🔁 Remplacer liens et @mentions", + "ui_rmp_toggle_off": "🔴 Remplacement de liens désactivé.", + "ui_rmp_toggle_on": "🟢 Remplacement de liens activé — chaque lien trouvé est réécrit selon la table.", "ui_route_add_sources": "📥 Sources ({count})", "ui_route_add_targets": "📤 Cibles ({count})", "ui_route_bad_edit": "❌ Envoyez `index = valeur`, ou `nom -> ` pour une nouvelle route.", @@ -7703,11 +8014,14 @@ "ui_smart_active_path": "🛣 **Route active du flux de messages :**\n{path_desc}\n", "ui_smart_antiloop": "🔒 **Anti-boucle et anti-double-envoi :** actif avec clé d'idempotence et vérification de l'origine du message\n", "ui_smart_brand_block": "🔀 **Branding (A → C → B) :**", + "ui_smart_brand_channel": "📡 Saut canaux uniquement : {state}", "ui_smart_brand_origins": "🎯 **Chaînes d'origine :** {origins}", "ui_smart_brand_state": "🎛 **Filtre du saut :** {state}", "ui_smart_brand_state_all": "tous les messages", "ui_smart_brand_state_filtered": "filtré", "ui_smart_brand_words": "🔑 **Mots du chemin :** {words}", + "ui_smart_btn_brand_chan_off": "📡 Saut canaux uniquement : DÉSACTIVÉ", + "ui_smart_btn_brand_chan_on": "📡 Saut canaux uniquement : ACTIVÉ", "ui_smart_btn_brand_force_off": "🔁 Transfert depuis C : OFF", "ui_smart_btn_brand_force_on": "🔁 Transfert depuis C : ON", "ui_smart_btn_brand_gate_off": "🚧 Transferts uniquement : OFF", @@ -7724,6 +8038,7 @@ "ui_smart_btn_prio_down": "➖ Priorité (−10)", "ui_smart_btn_prio_up": "➕ Priorité (+10)", "ui_smart_btn_regex": "🔣 Définir Regex", + "ui_smart_btn_replace_maps": "🔁 Tables de liens et @mentions", "ui_smart_custom_header": "🏷 **En-tête de copie personnalisé :** {header}", "ui_smart_hint": "👇 Pour modifier les paramètres, touchez les options ci-dessous :", "ui_smart_keywords": "🔎 **Mots-clés du texte :** {keywords}", @@ -7751,6 +8066,7 @@ "ui_src_desc": "Les nouveaux messages de cette discussion seront lus et transférés.", "ui_src_sel": "📥 **Sélectionner le Canal source (Source):**", "ui_src_selected": "✅ Source : {title}", + "ui_stale_entry": "⚠️ La liste a changé pendant que le menu était ouvert — affichage de l'état actuel.", "ui_stat_allowed_word": "Autorisé 🟢", "ui_stat_clean_word": "Nettoyer 🧹", "ui_stat_errors": "Erreurs", @@ -7925,10 +8241,10 @@ "ui_wl_count": "Nombre de mots autorisés : **{count}**\n", "ui_wl_desc": "Seuls les messages contenant au moins une de ces entrées sont transférés.\n\nTrois modes de correspondance : **Mot / Phrase**, **Regex** ou **Correspondance de texte long**.\n\n", "ui_wl_empty": "❌ Aucun mot dans la liste blanche (tous les messages sont autorisés).", - "ui_wl_only_regex": "🔒 Aucun mot-clé — mais les règles regex ci-dessous sont actives et filtrent.", "ui_wl_entry_deleted_alert": "Supprimé : {entry}", "ui_wl_hint": "👇 Choisissez une action pour gérer la liste blanche :", "ui_wl_more": "\n... et {more} mots de plus", + "ui_wl_only_regex": "🔒 Aucun mot-clé — mais les règles regex ci-dessous sont actives et filtrent.", "ui_wl_state_off": "État : **désactivé** (les mots sont conservés mais non appliqués).\n\n", "ui_wl_state_on": "État : **activé** (les mots sont appliqués).\n\n", "ui_wl_title": "✅ **Liste Blanche de Mots (Whitelist Keywords)**\n\n", @@ -8180,6 +8496,7 @@ "ui_ai_btn_health": "🩺 Cek kesehatan (Ping)", "ui_ai_btn_key": "🔑 Ubah kunci (API Key)", "ui_ai_btn_model": "🧠 Ubah model", + "ui_ai_btn_open_prompts": "🧠 Kelola prompt", "ui_ai_btn_pick_config": "🧠 Pilih konfigurasi AI", "ui_ai_btn_pick_prompt": "📝 Pilih prompt", "ui_ai_btn_sample": "🧪 Uji tulis ulang teks", @@ -8246,6 +8563,7 @@ "ui_ai_prompt_enter_system_instruction": "🤖 Masukkan instruksi prompt sistem untuk '{name}':\n\nContoh: `Tulis ulang pesan ini dengan nada formal tanpa mengubah fakta utama.`", "ui_ai_prompt_label": "Prompt", "ui_ai_prompt_name_invalid": "❌ Nama prompt tidak valid. Masukkan nama antara 1 hingga 80 karakter.", + "ui_ai_prompt_off_tag": "nonaktif", "ui_ai_prompt_staged_alert": "✅ Prompt dipilih (akan disimpan saat Anda mengetuk Simpan)", "ui_ai_prompt_sys_empty_error": "❌ Prompt sistem tidak boleh kosong.", "ui_ai_provider": "🧠 Kirim provider: openai, anthropic, gemini, groq, deepseek, 9router, openrouter, custom", @@ -8351,10 +8669,10 @@ "ui_bl_count": "Jumlah kata yang diblokir: **{count}**\n", "ui_bl_desc": "Pesan yang memuat salah satu entri ini **TIDAK** akan diteruskan.\n\nTiga cara mencocokkan: **Kata / Frasa**, **Regex**, atau **Pencocokan Teks Panjang**.\n\n", "ui_bl_empty": "❌ Tidak ada kata dalam daftar terlarang.", - "ui_bl_only_regex": "🚫 Tidak ada kata kunci — tetapi aturan regex di bawah ini aktif dan menyaring.", "ui_bl_entry_deleted_alert": "Dihapus: {entry}", "ui_bl_hint": "👇 Pilih tindakan untuk mengelola daftar terlarang:", "ui_bl_more": "\n... dan {more} kata lainnya", + "ui_bl_only_regex": "🚫 Tidak ada kata kunci — tetapi aturan regex di bawah ini aktif dan menyaring.", "ui_bl_state_off": "Status: **nonaktif** (kata disimpan tetapi tidak diberlakukan).\n\n", "ui_bl_state_on": "Status: **aktif** (kata diberlakukan).\n\n", "ui_bl_title": "🚫 **Daftar Kata Terlarang (Blacklist Keywords)**\n\n", @@ -8372,6 +8690,7 @@ "ui_block_video_on": "Pemblokiran video diaktifkan 🚫", "ui_block_voice_off": "Pesan suara diizinkan 🟢", "ui_block_voice_on": "Pemblokiran suara diaktifkan 🚫", + "ui_brand_channel_saved": "📡 Hop khusus kanal: {state}", "ui_brand_cleared": "🧹 Filter branding dihapus — semua pesan akan melompat.", "ui_brand_force_saved": "🔁 Teruskan dari C: {state}", "ui_brand_gate_saved": "🚧 Syarat hanya teruskan: {state}", @@ -8385,6 +8704,7 @@ "ui_brand_words_saved": "✅ Kata kunci rute disimpan: {value}", "ui_btn_ai_pv_assistant": "💬 Asisten DM Pintar (AI PV Assistant)", "ui_btn_ar_assistant": "🤖 Balasan Otomatis AI (Pesan Pribadi)", + "ui_btn_prompts": "🧠 Pustaka prompt", "ui_btn_use_default_host": "✅ Gunakan default (atau lanjut)", "ui_bulk_confirm": "Hapus {n} aturan terpilih? Tidak bisa dibatalkan.", "ui_bulk_delete": "🗑 Hapus yang dipilih", @@ -8963,6 +9283,48 @@ "ui_plan_status_none": "0 hari", "ui_prev": "◀️ Sebelumnya", "ui_priority_alert": "Prioritas: {priority}", + "ui_prm_btn_back_list": "← Pustaka", + "ui_prm_btn_edit_desc": "📝 Deskripsi", + "ui_prm_btn_edit_name": "✏️ Ganti nama", + "ui_prm_btn_edit_sys": "🧩 Prompt sistem", + "ui_prm_btn_edit_user": "✉️ Template pengguna", + "ui_prm_btn_new": "➕ Prompt baru", + "ui_prm_created": "✅ Prompt “{name}” dibuat dan disimpan ke pustaka Anda.", + "ui_prm_delete_confirm": "⚠️ Hapus prompt “{name}”?\nTugas yang memakainya akan kembali ke prompt konfigurasi AI masing-masing.", + "ui_prm_delete_yes": "🗑 Ya, hapus", + "ui_prm_deleted": "🗑 Prompt dihapus.", + "ui_prm_detail_desc": "📝 {desc}", + "ui_prm_detail_lang": "🌐 Bahasa target: {code}", + "ui_prm_detail_sys": "🧩 Prompt sistem:\n{sys}", + "ui_prm_detail_title": "🧠 {name}", + "ui_prm_detail_user": "✉️ Template pengguna:\n{user}", + "ui_prm_detail_version": "🔖 Versi: v{version}", + "ui_prm_disabled_alert": "🔴 Prompt dinonaktifkan — pengiriman kembali memakai prompt konfigurasi AI.", + "ui_prm_edit_desc_prompt": "📝 Kirim deskripsi baru untuk “{name}” — kirim - untuk mengosongkannya.", + "ui_prm_edit_name_prompt": "✏️ Kirim nama baru untuk “{name}”.", + "ui_prm_edit_sys_prompt": "🧩 Kirim prompt sistem baru untuk “{name}”.", + "ui_prm_edit_user_prompt": "✉️ Kirim template pengguna baru untuk “{name}”.\nHarus memuat placeholder {text} — kirim - untuk mengembalikannya ke “{text}”.", + "ui_prm_enabled_alert": "🟢 Prompt diaktifkan.", + "ui_prm_error_generic": "⚠️ Prompt tidak bisa disimpan — silakan coba lagi.", + "ui_prm_hint": "Ketuk prompt untuk mengelolanya. Prompt nonaktif tetap di pustaka, tetapi pengiriman memakai prompt konfigurasi AI tugas.", + "ui_prm_needs_text_placeholder": "✋ Template pengguna harus memuat placeholder {text}.", + "ui_prm_new_name_prompt": "🧠 Prompt baru — langkah 1/3\n\n✍️ Kirim nama promptnya.", + "ui_prm_new_sys_prompt": "🧠 Prompt baru — langkah 2/3\n\n✍️ Kirim prompt sistem: instruksi yang diikuti AI pada setiap penulisan ulang.", + "ui_prm_new_user_prompt": "🧠 Prompt baru — langkah 3/3\n\n✍️ Kirim template pengguna. Harus memuat placeholder {text} — kirim - untuk memakai “{text}” saja.", + "ui_prm_none": "Belum ada template prompt. Ketuk ➕ Prompt baru untuk menambahkan.", + "ui_prm_not_found": "Prompt tidak ditemukan — mungkin sudah dihapus.", + "ui_prm_saved": "✅ Tersimpan — sekarang v{version}.", + "ui_prm_state_off": "🔴 Nonaktif — pengiriman memakai prompt milik konfigurasi AI", + "ui_prm_state_on": "🟢 Aktif — dipakai saat pengiriman", + "ui_prm_system_immutable": "Prompt sistem dipakai bersama oleh semua akun di instalasi ini. Untuk menyesuaikannya, buat prompt baru dengan kalimat yang Anda perlukan.", + "ui_prm_system_tag": "bersama", + "ui_prm_test_ask": "🧪 Kirim pesan contoh untuk ditulis ulang dengan “{name}”.", + "ui_prm_test_failed": "🧪 “{name}” gagal ({code}):\n{message}", + "ui_prm_test_no_config": "Tidak ditemukan konfigurasi AI yang aktif — tambahkan dulu di menu AI.", + "ui_prm_test_result": "🧪 “{name}” — hasil ({model}):\n\n{result}", + "ui_prm_title": "🧠 Template prompt — {count} · 🟢 {active}", + "ui_prm_too_long": "✋ Terlalu panjang — persingkat teksnya lalu kirim lagi.", + "ui_prm_unavailable": "Layanan prompt tidak tersedia pada instalasi ini.", "ui_profile_actions": "👇 Tindakan akun:", "ui_profile_reset_config": "⚠️ Atur ulang konfigurasi tugas", "ui_profile_restart_setup": "🔄 Mulai ulang penyiapan", @@ -8974,7 +9336,6 @@ "ui_prompt_change_base_url": "🌐 **Ubah alamat server:**\n\nAlamat saat ini: `{current}`\n\n✍️ Kirim alamat host baru:\n\nAtau kirim `default` untuk reset:", "ui_prompt_change_model": "🧠 **Ubah model AI:**\n\nModel saat ini: `{model}`\n\n✍️ Ketik dan kirim nama model baru:\n(Contoh: `coding` atau `gpt-4o-mini`)", "ui_prompt_edit_entry": "✏️ Kirim nilai baru untuk mengganti `{entry}`:", - "ui_stale_entry": "⚠️ Daftar berubah saat menu terbuka — menampilkan kondisi saat ini.", "ui_prompt_enter_base_url": "🌐 **Atur alamat server ({provider}):**\n\nAlamat default:\n`{default_url}`\n\n✍️ Untuk menggunakan host kustom, kirim alamatnya:\n\nAtau ketuk tombol untuk menggunakan default:", "ui_prompt_enter_delay": "⏱ **Masukkan penundaan yang diinginkan (detik):**\n\nSilakan kirimkan jumlah detik (mis. `5` atau `15`).", "ui_prompt_enter_every_n": "🔢 **Masukkan interval:**\n\nKirim bilangan bulat 1 sampai 1000. `1` meneruskan semua pesan; `5` meneruskan pesan ke-1, ke-6, ke-11 … yang lolos filter.", @@ -9029,6 +9390,7 @@ "ui_reason_branding_always": "branding aktif untuk semua pesan", "ui_reason_branding_disabled": "branding mati ({reason})", "ui_reason_branding_no_triggers_configured": "branding aktif tanpa pemicu, jadi semua pesan dilewatkan", + "ui_reason_branding_requires_channel_origin": "branding hanya me-hop pesan yang berasal dari kanal", "ui_reason_branding_requires_forward_origin": "branding hanya meneruskan pesan yang memang terusan", "ui_reason_branding_triggers_matched": "lompatan cocok dengan pemicunya ({words})", "ui_reason_branding_triggers_unmatched": "lompatan tidak cocok dengan pemicunya ({words})", @@ -9128,6 +9490,7 @@ "ui_reg_help_branding_match_mode": "any = satu pemicu cukup, all = semua pemicu yang diatur harus cocok.", "ui_reg_help_branding_origin_ids": "Lewati hanya jika pesan diteruskan dari salah satu id/@username ini.", "ui_reg_help_branding_origin_titles": "Lewati hanya jika pesan diteruskan dari salah satu nama kanal ini.", + "ui_reg_help_branding_require_channel": "Menolak hop kecuali pesan berasal dari kanal — pengguna biasa diteruskan langsung dengan nama mereka sendiri.", "ui_reg_help_branding_require_forward": "Tolak lompatan kecuali pesan itu sendiri hasil teruskan.", "ui_reg_help_branding_triggers_enabled": "Saat mati, setiap pesan yang cocok melewati C; saat hidup, pemicu di bawah yang menentukan.", "ui_reg_help_clean_custom_words": "Kata yang dihapus dari teks.", @@ -9140,7 +9503,10 @@ "ui_reg_help_footer": "Teks yang ditambahkan di akhir setiap pesan yang diteruskan.", "ui_reg_help_forward_every_n": "Teruskan hanya pesan ke-N yang lolos filter tugas ini (1 = semua pesan, 1000 = satu dari seribu). Satu album dihitung sebagai satu pesan.", "ui_reg_help_header": "Teks yang ditambahkan di awal setiap pesan yang diteruskan.", + "ui_reg_help_link_replace_enabled": "Menulis ulang setiap tautan di teks lewat peta penulisan ulang tautan.", + "ui_reg_help_link_rewrite_map": "Pasangan penulisan ulang: {\"*\": \"https://t.me/saya\"} menulis ulang semua tautan, \"domainlama.com\": \"domainbaru.com\" menulis ulang satu domain, \"https://lama.t.me/x\": \"https://baru.t.me/y\" menulis ulang URL penuh.", "ui_reg_help_max_time_edit": "Detik setelah edit tidak lagi disalin.", + "ui_reg_help_mention_rewrite_map": "Pasangan penulisan ulang sebutan: {\"@tokoLama\": \"@tokoBaru\"} menulis ulang satu akun (tanpa membedakan huruf besar/kecil), \"*\": \"@utama\" menulis ulang semua @ID.", "ui_reg_help_ocr_image_ai": "Jalankan OCR pada gambar dan teruskan teks yang diambil.", "ui_reg_help_power_control": "Tampilan praktis dari power_schedule.enabled.", "ui_reg_help_process_raw_text": "Proses teks mentah sebelum penguraian apa pun.", @@ -9202,6 +9568,7 @@ "ui_reg_set_branding_match_mode": "Mode pencocokan merek", "ui_reg_set_branding_origin_ids": "ID asal", "ui_reg_set_branding_origin_titles": "Judul asal", + "ui_reg_set_branding_require_channel": "Hop khusus kanal", "ui_reg_set_branding_require_forward": "Wajibkan penerusan", "ui_reg_set_branding_triggers_enabled": "Pemicu merek", "ui_reg_set_button_to_text": "Tombol jadi teks", @@ -9227,8 +9594,11 @@ "ui_reg_set_keyword_confirmation": "Konfirmasi kata kunci", "ui_reg_set_keyword_reply": "Balasan kata kunci", "ui_reg_set_link_buttons": "Tombol tautan", + "ui_reg_set_link_replace_enabled": "Ganti setiap tautan yang ditemukan", "ui_reg_set_link_replacement": "Penggantian tautan", + "ui_reg_set_link_rewrite_map": "Peta penulisan ulang tautan", "ui_reg_set_max_time_edit": "Batas waktu edit", + "ui_reg_set_mention_rewrite_map": "Peta penulisan ulang @sebutan", "ui_reg_set_news_digest": "Ringkasan berita", "ui_reg_set_ocr_image_ai": "AI OCR gambar", "ui_reg_set_paid_addons": "Add-on berbayar", @@ -9313,6 +9683,26 @@ "ui_reset_config_done": "✅ Semua pengaturan berhasil diatur ulang.", "ui_retry": "🔄 Coba Lagi", "ui_retry_connect": "🚀 Coba lagi", + "ui_rmp_btn_edit_links": "🔗 Edit peta tautan", + "ui_rmp_btn_edit_mentions": "❕ Edit peta @sebutan", + "ui_rmp_btn_links_off": "🔗 Ganti tautan: NONAKTIF", + "ui_rmp_btn_links_on": "🔗 Ganti tautan: AKTIF", + "ui_rmp_cleared": "✅ Peta dikosongkan.", + "ui_rmp_link_map": "🔗 Peta tautan ({count}): {pairs}", + "ui_rmp_link_map_empty": "🔗 Peta tautan: kosong", + "ui_rmp_link_state_off": "🔗 Ganti tautan: NONAKTIF", + "ui_rmp_link_state_on": "🔗 Ganti tautan: AKTIF — tautan ditulis ulang lewat peta", + "ui_rmp_links_prompt": "🔗 Kirim peta penulisan ulang tautan — satu pasang per baris:\n`domainlama.com domainbaru.com` — tulis ulang satu domain\n`https://lama.t.me/x https://baru.t.me/y` — tulis ulang satu tautan\n`* https://t.me/saya` — tulis ulang SEMUA tautan\n\nKirim - untuk mengosongkan peta.", + "ui_rmp_mention_map": "❕ Peta sebutan ({count}): {pairs}", + "ui_rmp_mention_map_empty": "❕ Peta sebutan: kosong", + "ui_rmp_mentions_prompt": "❕ Kirim peta @sebutan — satu pasang per baris:\n`@tokoLama @tokoBaru` — tulis ulang satu akun\n`* @utama` — tulis ulang SEMUA @ID\n\nKirim - untuk mengosongkan peta.", + "ui_rmp_off_hint": "Peta sudah diisi tetapi ganti tautan NONAKTIF — aktifkan untuk menerapkannya.", + "ui_rmp_rule": "Tugas: {name}", + "ui_rmp_saved": "✅ Tersimpan — {count} pasangan.", + "ui_rmp_skipped": "✅ Tersimpan — {count} pasangan. {skipped} baris tidak valid dilewati.", + "ui_rmp_title": "🔁 Ganti tautan & @sebutan", + "ui_rmp_toggle_off": "🔴 Ganti tautan dinonaktifkan.", + "ui_rmp_toggle_on": "🟢 Ganti tautan diaktifkan — setiap tautan yang ditemukan ditulis ulang lewat peta.", "ui_route_add_sources": "📥 Sumber ({count})", "ui_route_add_targets": "📤 Tujuan ({count})", "ui_route_bad_edit": "❌ Kirim `indeks = nilai`, atau `nama -> ` untuk rute baru.", @@ -9690,11 +10080,14 @@ "ui_smart_active_path": "🛣 **Jalur aliran pesan aktif:**\n{path_desc}\n", "ui_smart_antiloop": "🔒 **Anti-loop & anti-kirim-ganda:** aktif dengan kunci idempotensi dan pemeriksaan asal pesan\n", "ui_smart_brand_block": "🔀 **Branding (A → C → B):**", + "ui_smart_brand_channel": "📡 Hop khusus kanal: {state}", "ui_smart_brand_origins": "🎯 **Kanal asal:** {origins}", "ui_smart_brand_state": "🎛 **Filter lompatan:** {state}", "ui_smart_brand_state_all": "semua pesan", "ui_smart_brand_state_filtered": "difilter", "ui_smart_brand_words": "🔑 **Kata kunci rute:** {words}", + "ui_smart_btn_brand_chan_off": "📡 Hop khusus kanal: NONAKTIF", + "ui_smart_btn_brand_chan_on": "📡 Hop khusus kanal: AKTIF", "ui_smart_btn_brand_force_off": "🔁 Teruskan dari C: OFF", "ui_smart_btn_brand_force_on": "🔁 Teruskan dari C: ON", "ui_smart_btn_brand_gate_off": "🚧 Hanya teruskan: OFF", @@ -9711,6 +10104,7 @@ "ui_smart_btn_prio_down": "➖ Prioritas (−10)", "ui_smart_btn_prio_up": "➕ Prioritas (+10)", "ui_smart_btn_regex": "🔣 Atur Regex", + "ui_smart_btn_replace_maps": "🔁 Peta tautan & @sebutan", "ui_smart_custom_header": "🏷 **Header salinan kustom:** {header}", "ui_smart_hint": "👇 Untuk mengubah pengaturan, ketuk opsi di bawah ini:", "ui_smart_keywords": "🔎 **Kata kunci teks:** {keywords}", @@ -9738,6 +10132,7 @@ "ui_src_desc": "Pesan baru dari obrolan ini akan dibaca dan diteruskan.", "ui_src_sel": "📥 **Pilih Saluran atau Grup Sumber (Source):**", "ui_src_selected": "✅ Sumber: {title}", + "ui_stale_entry": "⚠️ Daftar berubah saat menu terbuka — menampilkan kondisi saat ini.", "ui_stat_allowed_word": "Diizinkan 🟢", "ui_stat_clean_word": "Bersihkan 🧹", "ui_stat_errors": "Kesalahan", @@ -9912,10 +10307,10 @@ "ui_wl_count": "Jumlah kata yang diizinkan: **{count}**\n", "ui_wl_desc": "Hanya pesan yang memuat setidaknya salah satu entri ini yang diteruskan.\n\nTiga cara mencocokkan: **Kata / Frasa**, **Regex**, atau **Pencocokan Teks Panjang**.\n\n", "ui_wl_empty": "❌ Tidak ada kata dalam daftar yang diizinkan (semua pesan diizinkan).", - "ui_wl_only_regex": "🔒 Tidak ada kata kunci — tetapi aturan regex di bawah ini aktif dan menyaring.", "ui_wl_entry_deleted_alert": "Dihapus: {entry}", "ui_wl_hint": "👇 Pilih tindakan untuk mengelola daftar yang diizinkan:", "ui_wl_more": "\n... dan {more} kata lainnya", + "ui_wl_only_regex": "🔒 Tidak ada kata kunci — tetapi aturan regex di bawah ini aktif dan menyaring.", "ui_wl_state_off": "Status: **nonaktif** (kata disimpan tetapi tidak diberlakukan).\n\n", "ui_wl_state_on": "Status: **aktif** (kata diberlakukan).\n\n", "ui_wl_title": "✅ **Daftar Kata Diizinkan (Whitelist Keywords)**\n\n", @@ -10167,6 +10562,7 @@ "ui_ai_btn_health": "🩺 Verifica (Ping)", "ui_ai_btn_key": "🔑 Cambia chiave (API Key)", "ui_ai_btn_model": "🧠 Cambia modello", + "ui_ai_btn_open_prompts": "🧠 Gestisci prompt", "ui_ai_btn_pick_config": "🧠 Seleziona configurazione AI", "ui_ai_btn_pick_prompt": "📝 Seleziona prompt", "ui_ai_btn_sample": "🧪 Testa riscrittura del testo", @@ -10233,6 +10629,7 @@ "ui_ai_prompt_enter_system_instruction": "🤖 Inserisci le istruzioni del prompt di sistema per '{name}':\n\nEsempio: `Riscrivi questo messaggio in tono formale senza alterare i fatti.`", "ui_ai_prompt_label": "Prompt", "ui_ai_prompt_name_invalid": "❌ Nome prompt non valido. Inserisci tra 1 e 80 caratteri.", + "ui_ai_prompt_off_tag": "disattivato", "ui_ai_prompt_staged_alert": "✅ Prompt selezionato (verrà salvato al salvataggio)", "ui_ai_prompt_sys_empty_error": "❌ Il prompt di sistema non può essere vuoto.", "ui_ai_provider": "🧠 Invia il provider: openai, anthropic, gemini, groq, deepseek, 9router, openrouter, custom", @@ -10338,10 +10735,10 @@ "ui_bl_count": "Numero di parole bloccate: **{count}**\n", "ui_bl_desc": "I messaggi che contengono una di queste voci **NON** verranno inoltrati.\n\nTre modalità: **Parola / Frase**, **Regex** o **Corrispondenza di testo lungo**.\n\n", "ui_bl_empty": "❌ Nessuna parola nella lista nera.", - "ui_bl_only_regex": "🚫 Nessuna parola chiave — ma le regole regex qui sotto sono attive e filtrano.", "ui_bl_entry_deleted_alert": "Eliminato: {entry}", "ui_bl_hint": "👇 Scegli un'azione per gestire la lista nera:", "ui_bl_more": "\n... e altre {more} parole", + "ui_bl_only_regex": "🚫 Nessuna parola chiave — ma le regole regex qui sotto sono attive e filtrano.", "ui_bl_state_off": "Stato: **disattivato** (le parole sono mantenute ma non applicate).\n\n", "ui_bl_state_on": "Stato: **attivo** (le parole sono applicate).\n\n", "ui_bl_title": "🚫 **Lista Nera di Parole (Blacklist Keywords)**\n\n", @@ -10359,6 +10756,7 @@ "ui_block_video_on": "Blocco dei video attivato 🚫", "ui_block_voice_off": "Messaggi vocali permessi 🟢", "ui_block_voice_on": "Blocco dei messaggi vocali attivato 🚫", + "ui_brand_channel_saved": "📡 Hop solo canali: {state}", "ui_brand_cleared": "🧹 Filtro branding cancellato — tutti i messaggi faranno il salto.", "ui_brand_force_saved": "🔁 Inoltro da C: {state}", "ui_brand_gate_saved": "🚧 Condizione solo inoltri: {state}", @@ -10372,6 +10770,7 @@ "ui_brand_words_saved": "✅ Parole del percorso salvate: {value}", "ui_btn_ai_pv_assistant": "💬 Assistente intelligente per messaggi privati (AI PV Assistant)", "ui_btn_ar_assistant": "🤖 Risposta automatica IA (messaggi privati)", + "ui_btn_prompts": "🧠 Libreria di prompt", "ui_btn_use_default_host": "✅ Usa predefinito (o avanti)", "ui_bulk_confirm": "Eliminare {n} regola(e) selezionata(e)? Operazione irreversibile.", "ui_bulk_delete": "🗑 Elimina selezionate", @@ -10950,6 +11349,48 @@ "ui_plan_status_none": "0 giorni", "ui_prev": "◀️ Precedente", "ui_priority_alert": "Priorità: {priority}", + "ui_prm_btn_back_list": "← Libreria", + "ui_prm_btn_edit_desc": "📝 Descrizione", + "ui_prm_btn_edit_name": "✏️ Rinomina", + "ui_prm_btn_edit_sys": "🧩 Prompt di sistema", + "ui_prm_btn_edit_user": "✉️ Template utente", + "ui_prm_btn_new": "➕ Nuovo prompt", + "ui_prm_created": "✅ Prompt «{name}» creato e salvato nella tua libreria.", + "ui_prm_delete_confirm": "⚠️ Eliminare il prompt «{name}»?\nLe attività che lo usano torneranno al prompt della loro configurazione IA.", + "ui_prm_delete_yes": "🗑 Sì, elimina", + "ui_prm_deleted": "🗑 Prompt eliminato.", + "ui_prm_detail_desc": "📝 {desc}", + "ui_prm_detail_lang": "🌐 Lingua di destinazione: {code}", + "ui_prm_detail_sys": "🧩 Prompt di sistema:\n{sys}", + "ui_prm_detail_title": "🧠 {name}", + "ui_prm_detail_user": "✉️ Template utente:\n{user}", + "ui_prm_detail_version": "🔖 Versione: v{version}", + "ui_prm_disabled_alert": "🔴 Prompt disattivato — le consegne tornano al prompt della configurazione IA.", + "ui_prm_edit_desc_prompt": "📝 Invia la nuova descrizione per «{name}» — invia - per cancellarla.", + "ui_prm_edit_name_prompt": "✏️ Invia il nuovo nome per «{name}».", + "ui_prm_edit_sys_prompt": "🧩 Invia il nuovo prompt di sistema per «{name}».", + "ui_prm_edit_user_prompt": "✉️ Invia il nuovo template utente per «{name}».\nDeve contenere il segnaposto {text} — invia - per ripristinarlo a «{text}».", + "ui_prm_enabled_alert": "🟢 Prompt attivato.", + "ui_prm_error_generic": "⚠️ Impossibile salvare il prompt — riprova.", + "ui_prm_hint": "Tocca un prompt per gestirlo. Un prompt disattivato resta nella libreria, ma le consegne usano il prompt della configurazione IA dell'attività.", + "ui_prm_needs_text_placeholder": "✋ Il template utente deve contenere il segnaposto {text}.", + "ui_prm_new_name_prompt": "🧠 Nuovo prompt — passo 1/3\n\n✍️ Invia il nome del prompt.", + "ui_prm_new_sys_prompt": "🧠 Nuovo prompt — passo 2/3\n\n✍️ Invia il prompt di sistema: le istruzioni che l'IA segue a ogni riscrittura.", + "ui_prm_new_user_prompt": "🧠 Nuovo prompt — passo 3/3\n\n✍️ Invia il template utente. Deve contenere il segnaposto {text} — invia - per usare solo «{text}».", + "ui_prm_none": "Ancora nessun template di prompt. Tocca ➕ Nuovo prompt per aggiungerne uno.", + "ui_prm_not_found": "Prompt non trovato — potrebbe essere stato eliminato.", + "ui_prm_saved": "✅ Salvato — ora alla v{version}.", + "ui_prm_state_off": "🔴 Disattivato — le consegne usano il prompt propio della configurazione IA", + "ui_prm_state_on": "🟢 Attivo — usato dalle consegne", + "ui_prm_system_immutable": "I prompt di sistema sono condivisi da tutti gli account di questa installazione. Per personalizzarne uno, crea un nuovo prompt con il testo che ti serve.", + "ui_prm_system_tag": "condiviso", + "ui_prm_test_ask": "🧪 Invia un messaggio di esempio da riscrivere con «{name}».", + "ui_prm_test_failed": "🧪 «{name}» non riuscito ({code}):\n{message}", + "ui_prm_test_no_config": "Nessuna configurazione IA attivata trovata — aggiungine una nel menu IA prima.", + "ui_prm_test_result": "🧪 «{name}» — risultato ({model}):\n\n{result}", + "ui_prm_title": "🧠 Template di prompt — {count} · 🟢 {active}", + "ui_prm_too_long": "✋ Troppo lungo — accorcia il testo e invialo di nuovo.", + "ui_prm_unavailable": "Il servizio prompt non è disponibile su questa installazione.", "ui_profile_actions": "👇 Azioni account:", "ui_profile_reset_config": "⚠️ Reimposta configurazione attività", "ui_profile_restart_setup": "🔄 Riavvia configurazione", @@ -10961,7 +11402,6 @@ "ui_prompt_change_base_url": "🌐 **Modifica indirizzo server:**\n\nIndirizzo attuale: `{current}`\n\n✍️ Invia il nuovo indirizzo:\n\nOppure invia `default` per ripristinare:", "ui_prompt_change_model": "🧠 **Cambia modello IA:**\n\nModello attuale: `{model}`\n\n✍️ Digita il nome del nuovo modello:\n(Esempio: `coding` o `gpt-4o-mini`)", "ui_prompt_edit_entry": "✏️ Invia il nuovo valore per sostituire `{entry}`:", - "ui_stale_entry": "⚠️ L'elenco è cambiato mentre il menu era aperto — mostra lo stato attuale.", "ui_prompt_enter_base_url": "🌐 **Imposta indirizzo server ({provider}):**\n\nIndirizzo predefinito:\n`{default_url}`\n\n✍️ Per usare un host personalizzato, invia l'indirizzo:\n\nOppure tocca il pulsante per usare quello predefinito:", "ui_prompt_enter_delay": "⏱ **Inserisci il ritardo desiderato (secondi):**\n\nInvia il numero di secondi (es. `5` o `15`).", "ui_prompt_enter_every_n": "🔢 **Inserisci l'intervallo:**\n\nInvia un numero intero da 1 a 1000. `1` inoltra tutto; `5` inoltra il 1º, 6º, 11º … messaggio che supera i filtri.", @@ -11016,6 +11456,7 @@ "ui_reason_branding_always": "il branding è attivo per tutti i messaggi", "ui_reason_branding_disabled": "il branding è spento ({reason})", "ui_reason_branding_no_triggers_configured": "il branding è attivo senza trigger, quindi tutti i messaggi passano", + "ui_reason_branding_requires_channel_origin": "il branding salta solo i messaggi che provengono da un canale", "ui_reason_branding_requires_forward_origin": "il branding salta solo i messaggi che sono già inoltri", "ui_reason_branding_triggers_matched": "il salto ha corrisposto ai suoi trigger ({words})", "ui_reason_branding_triggers_unmatched": "il salto non ha corrisposto a nessuno dei suoi trigger ({words})", @@ -11115,6 +11556,7 @@ "ui_reg_help_branding_match_mode": "any = basta un trigger, all = tutti i trigger configurati devono corrispondere.", "ui_reg_help_branding_origin_ids": "Passa solo se il messaggio è stato inoltrato da uno di questi id/@nomi.", "ui_reg_help_branding_origin_titles": "Passa solo se il messaggio è stato inoltrato da uno di questi canali.", + "ui_reg_help_branding_require_channel": "Rifiuta il salto a meno che il messaggio provenga da un canale — gli utenti normali vengono inoltrati direttamente con il loro nome.", "ui_reg_help_branding_require_forward": "Rifiuta il passaggio a meno che il messaggio non sia esso stesso un inoltro.", "ui_reg_help_branding_triggers_enabled": "Spento, ogni messaggio corrispondente passa per C; acceso, decidono i trigger qui sotto.", "ui_reg_help_clean_custom_words": "Parole rimosse dal testo.", @@ -11127,7 +11569,10 @@ "ui_reg_help_footer": "Testo aggiunto alla fine di ogni messaggio inoltrato.", "ui_reg_help_forward_every_n": "Inoltra solo l'N-esimo messaggio che supera i filtri di questa attività (1 = tutti i messaggi, 1000 = uno su mille). Un album conta come un messaggio.", "ui_reg_help_header": "Testo aggiunto all'inizio di ogni messaggio inoltrato.", + "ui_reg_help_link_replace_enabled": "Riscrive ogni link presente nel testo tramite la mappa di riscrittura dei link.", + "ui_reg_help_link_rewrite_map": "Coppie di riscrittura: {\"*\": \"https://t.me/io\"} riscrive tutti i link, \"vecchiodominio.com\": \"nuovodominio.com\" riscrive un dominio, \"https://vecchio.t.me/x\": \"https://nuovo.t.me/y\" riscrive un URL completo.", "ui_reg_help_max_time_edit": "Secondi dopo i quali una modifica non viene più riportata.", + "ui_reg_help_mention_rewrite_map": "Coppie di riscrittura delle menzioni: {\"@vecchionegozio\": \"@nuovonegozio\"} riscrive un handle (senza distinzione maiuscole/minuscole), \"*\": \"@principale\" riscrive tutti gli @ID.", "ui_reg_help_ocr_image_ai": "Esegui l'OCR sulle immagini e inoltra il testo estratto.", "ui_reg_help_power_control": "Vista comoda di power_schedule.enabled.", "ui_reg_help_process_raw_text": "Elabora il testo grezzo prima di qualsiasi analisi.", @@ -11189,6 +11634,7 @@ "ui_reg_set_branding_match_mode": "Modalità corrispondenza branding", "ui_reg_set_branding_origin_ids": "ID di origine", "ui_reg_set_branding_origin_titles": "Titoli di origine", + "ui_reg_set_branding_require_channel": "Hop solo canali", "ui_reg_set_branding_require_forward": "Richiedi inoltro", "ui_reg_set_branding_triggers_enabled": "Trigger di branding", "ui_reg_set_button_to_text": "Pulsante in testo", @@ -11214,8 +11660,11 @@ "ui_reg_set_keyword_confirmation": "Conferma con parola chiave", "ui_reg_set_keyword_reply": "Risposta con parola chiave", "ui_reg_set_link_buttons": "Pulsanti link", + "ui_reg_set_link_replace_enabled": "Sostituisci qualsiasi link trovato", "ui_reg_set_link_replacement": "Sostituzione link", + "ui_reg_set_link_rewrite_map": "Mappa di riscrittura dei link", "ui_reg_set_max_time_edit": "Tempo massimo di modifica", + "ui_reg_set_mention_rewrite_map": "Mappa di riscrittura delle @menzioni", "ui_reg_set_news_digest": "Digest delle notizie", "ui_reg_set_ocr_image_ai": "IA OCR delle immagini", "ui_reg_set_paid_addons": "Componenti aggiuntivi a pagamento", @@ -11300,6 +11749,26 @@ "ui_reset_config_done": "✅ Tutte le impostazioni sono state ripristinate.", "ui_retry": "🔄 Riprova", "ui_retry_connect": "🚀 Riprova connessione", + "ui_rmp_btn_edit_links": "🔗 Modifica mappa dei link", + "ui_rmp_btn_edit_mentions": "❕ Modifica mappa @menzioni", + "ui_rmp_btn_links_off": "🔗 Sostituisci link: DISATTIVO", + "ui_rmp_btn_links_on": "🔗 Sostituisci link: ATTIVO", + "ui_rmp_cleared": "✅ Mappa svuotata.", + "ui_rmp_link_map": "🔗 Mappa dei link ({count}): {pairs}", + "ui_rmp_link_map_empty": "🔗 Mappa dei link: vuota", + "ui_rmp_link_state_off": "🔗 Sostituzione link: DISATTIVATA", + "ui_rmp_link_state_on": "🔗 Sostituzione link: ATTIVA — i link vengono riscritti tramite la mappa", + "ui_rmp_links_prompt": "🔗 Invia la mappa di riscrittura dei link — una coppia per riga:\n`vecchiodominio.com nuovodominio.com` — riscrive un dominio\n`https://vecchio.t.me/x https://nuovo.t.me/y` — riscrive un link\n`* https://t.me/io` — riscrive TUTTI i link\n\nInvia - per svuotare la mappa.", + "ui_rmp_mention_map": "❕ Mappa delle menzioni ({count}): {pairs}", + "ui_rmp_mention_map_empty": "❕ Mappa delle menzioni: vuota", + "ui_rmp_mentions_prompt": "❕ Invia la mappa delle @menzioni — una coppia per riga:\n`@vecchionegozio @nuovonegozio` — riscrive un handle\n`* @principale` — riscrive TUTTI gli @ID\n\nInvia - per svuotare la mappa.", + "ui_rmp_off_hint": "La mappa è impostata ma la sostituzione link è DISATTIVATA — attivala per applicarla.", + "ui_rmp_rule": "Attività: {name}", + "ui_rmp_saved": "✅ Salvato — {count} coppia/e.", + "ui_rmp_skipped": "✅ Salvato — {count} coppia/e. Ignorate {skipped} riga/he non valide.", + "ui_rmp_title": "🔁 Sostituisci link e @menzioni", + "ui_rmp_toggle_off": "🔴 Sostituzione link disattivata.", + "ui_rmp_toggle_on": "🟢 Sostituzione link attivata — ogni link trovato viene riscritto tramite la mappa.", "ui_route_add_sources": "📥 Origini ({count})", "ui_route_add_targets": "📤 Destinazioni ({count})", "ui_route_bad_edit": "❌ Invia `indice = valore`, o `nome -> ` per un nuovo percorso.", @@ -11677,11 +12146,14 @@ "ui_smart_active_path": "🛣 **Percorso attivo del flusso di messaggi:**\n{path_desc}\n", "ui_smart_antiloop": "🔒 **Anti-ciclo e anti-doppio-invio:** attivo con chiave di idempotenza e verifica dell'origine del messaggio\n", "ui_smart_brand_block": "🔀 **Branding (A → C → B):**", + "ui_smart_brand_channel": "📡 Hop solo canali: {state}", "ui_smart_brand_origins": "🎯 **Canali di origine:** {origins}", "ui_smart_brand_state": "🎛 **Filtro del salto:** {state}", "ui_smart_brand_state_all": "tutti i messaggi", "ui_smart_brand_state_filtered": "filtrato", "ui_smart_brand_words": "🔑 **Parole del percorso:** {words}", + "ui_smart_btn_brand_chan_off": "📡 Hop solo canali: DISATTIVO", + "ui_smart_btn_brand_chan_on": "📡 Hop solo canali: ATTIVO", "ui_smart_btn_brand_force_off": "🔁 Inoltro da C: OFF", "ui_smart_btn_brand_force_on": "🔁 Inoltro da C: ON", "ui_smart_btn_brand_gate_off": "🚧 Solo inoltri: OFF", @@ -11698,6 +12170,7 @@ "ui_smart_btn_prio_down": "➖ Priorità (−10)", "ui_smart_btn_prio_up": "➕ Priorità (+10)", "ui_smart_btn_regex": "🔣 Imposta Regex", + "ui_smart_btn_replace_maps": "🔁 Mappe link e @menzioni", "ui_smart_custom_header": "🏷 **Intestazione di copia personalizzata:** {header}", "ui_smart_hint": "👇 Per modificare le impostazioni, tocca le opzioni qui sotto:", "ui_smart_keywords": "🔎 **Parole chiave del testo:** {keywords}", @@ -11725,6 +12198,7 @@ "ui_src_desc": "I nuovi messaggi da questa chat verranno letti e inoltrati.", "ui_src_sel": "📥 **Seleziona il Canale di origine (Source):**", "ui_src_selected": "✅ Origine: {title}", + "ui_stale_entry": "⚠️ L'elenco è cambiato mentre il menu era aperto — mostra lo stato attuale.", "ui_stat_allowed_word": "Consentito 🟢", "ui_stat_clean_word": "Pulisci 🧹", "ui_stat_errors": "Errori", @@ -11899,10 +12373,10 @@ "ui_wl_count": "Numero di parole consentite: **{count}**\n", "ui_wl_desc": "Vengono inoltrati solo i messaggi che contengono almeno una di queste voci.\n\nTre modalità: **Parola / Frase**, **Regex** o **Corrispondenza di testo lungo**.\n\n", "ui_wl_empty": "❌ Nessuna parola nella lista bianca (tutti i messaggi sono consentiti).", - "ui_wl_only_regex": "🔒 Nessuna parola chiave — ma le regole regex qui sotto sono attive e filtrano.", "ui_wl_entry_deleted_alert": "Eliminato: {entry}", "ui_wl_hint": "👇 Scegli un'azione per gestire la lista bianca:", "ui_wl_more": "\n... e altre {more} parole", + "ui_wl_only_regex": "🔒 Nessuna parola chiave — ma le regole regex qui sotto sono attive e filtrano.", "ui_wl_state_off": "Stato: **disattivato** (le parole sono mantenute ma non applicate).\n\n", "ui_wl_state_on": "Stato: **attivo** (le parole sono applicate).\n\n", "ui_wl_title": "✅ **Lista Bianca di Parole (Whitelist Keywords)**\n\n", @@ -12154,6 +12628,7 @@ "ui_ai_btn_health": "🩺 Verificação (Ping)", "ui_ai_btn_key": "🔑 Mudar chave (API Key)", "ui_ai_btn_model": "🧠 Mudar modelo", + "ui_ai_btn_open_prompts": "🧠 Gerenciar prompts", "ui_ai_btn_pick_config": "🧠 Escolher configuração de IA", "ui_ai_btn_pick_prompt": "📝 Selecionar prompt", "ui_ai_btn_sample": "🧪 Testar reescrita de texto", @@ -12220,6 +12695,7 @@ "ui_ai_prompt_enter_system_instruction": "🤖 Digite as instruções do prompt de sistema para '{name}':\n\nExemplo: `Reescreva esta mensagem em tom formal sem alterar fatos.`", "ui_ai_prompt_label": "Prompt", "ui_ai_prompt_name_invalid": "❌ Nome de prompt inválido. Digite entre 1 e 80 caracteres.", + "ui_ai_prompt_off_tag": "desativado", "ui_ai_prompt_staged_alert": "✅ Prompt selecionado (será salvo ao salvar)", "ui_ai_prompt_sys_empty_error": "❌ O prompt de sistema não pode estar vazio.", "ui_ai_provider": "🧠 Envie o provedor: openai, anthropic, gemini, groq, deepseek, 9router, openrouter, custom", @@ -12325,10 +12801,10 @@ "ui_bl_count": "Quantidade de palavras bloqueadas: **{count}**\n", "ui_bl_desc": "Mensagens que contenham qualquer uma destas entradas **NÃO** serão encaminhadas.\n\nTrês formas de corresponder: **Palavra / Frase**, **Regex** ou **Correspondência de Texto Longo**.\n\n", "ui_bl_empty": "❌ Não há palavras na lista negra.", - "ui_bl_only_regex": "🚫 Sem palavras-chave — mas as regras regex abaixo estão ativas e filtrando.", "ui_bl_entry_deleted_alert": "Excluído: {entry}", "ui_bl_hint": "👇 Escolha uma ação para gerir a lista negra:", "ui_bl_more": "\n... e mais {more} palavras", + "ui_bl_only_regex": "🚫 Sem palavras-chave — mas as regras regex abaixo estão ativas e filtrando.", "ui_bl_state_off": "Status: **desativado** (as palavras são mantidas, mas não aplicadas).\n\n", "ui_bl_state_on": "Status: **ativado** (as palavras são aplicadas).\n\n", "ui_bl_title": "🚫 **Lista Negra de Palavras (Blacklist Keywords)**\n\n", @@ -12346,6 +12822,7 @@ "ui_block_video_on": "Bloqueio de vídeos ativado 🚫", "ui_block_voice_off": "Mensagens de voz permitidas 🟢", "ui_block_voice_on": "Bloqueio de áudios ativado 🚫", + "ui_brand_channel_saved": "📡 Salto só de canais: {state}", "ui_brand_cleared": "🧹 Filtro de branding limpo — todas as mensagens farão o desvio.", "ui_brand_force_saved": "🔁 Reencaminhar de C: {state}", "ui_brand_gate_saved": "🚧 Condição apenas reencaminhadas: {state}", @@ -12359,6 +12836,7 @@ "ui_brand_words_saved": "✅ Palavras da rota guardadas: {value}", "ui_btn_ai_pv_assistant": "💬 Assistente inteligente para PV (AI PV Assistant)", "ui_btn_ar_assistant": "🤖 Resposta automática com IA (mensagens privadas)", + "ui_btn_prompts": "🧠 Biblioteca de prompts", "ui_btn_use_default_host": "✅ Usar padrão (ou avançar)", "ui_bulk_confirm": "Eliminar {n} regra(s) selecionada(s)? Não é possível desfazer.", "ui_bulk_delete": "🗑 Eliminar selecionadas", @@ -12937,6 +13415,48 @@ "ui_plan_status_none": "0 dias", "ui_prev": "◀️ Anterior", "ui_priority_alert": "Prioridade: {priority}", + "ui_prm_btn_back_list": "← Biblioteca", + "ui_prm_btn_edit_desc": "📝 Descrição", + "ui_prm_btn_edit_name": "✏️ Renomear", + "ui_prm_btn_edit_sys": "🧩 Prompt do sistema", + "ui_prm_btn_edit_user": "✉️ Modelo do usuário", + "ui_prm_btn_new": "➕ Novo prompt", + "ui_prm_created": "✅ Prompt “{name}” criado e salvo na sua biblioteca.", + "ui_prm_delete_confirm": "⚠️ Excluir o prompt “{name}”?\nAs tarefas que o usam voltarão ao prompt da sua configuração de IA.", + "ui_prm_delete_yes": "🗑 Sim, excluir", + "ui_prm_deleted": "🗑 Prompt excluído.", + "ui_prm_detail_desc": "📝 {desc}", + "ui_prm_detail_lang": "🌐 Idioma de destino: {code}", + "ui_prm_detail_sys": "🧩 Prompt do sistema:\n{sys}", + "ui_prm_detail_title": "🧠 {name}", + "ui_prm_detail_user": "✉️ Modelo do usuário:\n{user}", + "ui_prm_detail_version": "🔖 Versão: v{version}", + "ui_prm_disabled_alert": "🔴 Prompt desativado — os envios voltam ao prompt da configuração de IA.", + "ui_prm_edit_desc_prompt": "📝 Envie a nova descrição de “{name}” — envie - para limpá-la.", + "ui_prm_edit_name_prompt": "✏️ Envie o novo nome de “{name}”.", + "ui_prm_edit_sys_prompt": "🧩 Envie o novo prompt do sistema de “{name}”.", + "ui_prm_edit_user_prompt": "✉️ Envie o novo modelo do usuário de “{name}”.\nEle deve conter o marcador {text} — envie - para restaurá-lo a “{text}”.", + "ui_prm_enabled_alert": "🟢 Prompt ativado.", + "ui_prm_error_generic": "⚠️ Não foi possível salvar o prompt — tente novamente.", + "ui_prm_hint": "Toque em um prompt para gerenciá-lo. Um prompt desativado continua na biblioteca, mas os envios usam o prompt da configuração de IA da tarefa.", + "ui_prm_needs_text_placeholder": "✋ O modelo do usuário deve conter o marcador {text}.", + "ui_prm_new_name_prompt": "🧠 Novo prompt — passo 1/3\n\n✍️ Envie o nome do prompt.", + "ui_prm_new_sys_prompt": "🧠 Novo prompt — passo 2/3\n\n✍️ Envie o prompt do sistema: as instruções que a IA segue em cada reescrita.", + "ui_prm_new_user_prompt": "🧠 Novo prompt — passo 3/3\n\n✍️ Envie o modelo do usuário. Ele deve conter o marcador {text} — envie - para usar apenas “{text}”.", + "ui_prm_none": "Ainda não há modelos de prompt. Toque em ➕ Novo prompt para adicionar um.", + "ui_prm_not_found": "Prompt não encontrado — talvez tenha sido excluído.", + "ui_prm_saved": "✅ Salvo — agora na v{version}.", + "ui_prm_state_off": "🔴 Desativado — os envios usam o próprio prompt da configuração de IA", + "ui_prm_state_on": "🟢 Ativado — usado pelos envios", + "ui_prm_system_immutable": "Os prompts do sistema são compartilhados por todas as contas desta instalação. Para personalizar um, crie um novo prompt com o texto de que precisa.", + "ui_prm_system_tag": "compartilhado", + "ui_prm_test_ask": "🧪 Envie uma mensagem de exemplo para reescrever com “{name}”.", + "ui_prm_test_failed": "🧪 “{name}” falhou ({code}):\n{message}", + "ui_prm_test_no_config": "Nenhuma configuração de IA ativada encontrada — adicione uma no menu de IA primeiro.", + "ui_prm_test_result": "🧪 “{name}” — resultado ({model}):\n\n{result}", + "ui_prm_title": "🧠 Modelos de prompt — {count} · 🟢 {active}", + "ui_prm_too_long": "✋ Muito longo — encurte o texto e envie novamente.", + "ui_prm_unavailable": "O serviço de prompts não está disponível nesta instalação.", "ui_profile_actions": "👇 Ações da conta:", "ui_profile_reset_config": "⚠️ Redefinir configurações das tarefas", "ui_profile_restart_setup": "🔄 Reiniciar configuração", @@ -12948,7 +13468,6 @@ "ui_prompt_change_base_url": "🌐 **Alterar endereço do servidor:**\n\nEndereço atual: `{current}`\n\n✍️ Envie o novo endereço:\n\nOu envie `default` para redefinir:", "ui_prompt_change_model": "🧠 **Alterar modelo de IA:**\n\nModelo atual: `{model}`\n\n✍️ Digite o nome do novo modelo:\n(Exemplo: `coding` ou `gpt-4o-mini`)", "ui_prompt_edit_entry": "✏️ Envie o novo valor para substituir `{entry}`:", - "ui_stale_entry": "⚠️ A lista mudou enquanto o menu estava aberto — mostrando o estado atual.", "ui_prompt_enter_base_url": "🌐 **Definir endereço do servidor ({provider}):**\n\nEndereço padrão:\n`{default_url}`\n\n✍️ Para usar um host personalizado, envie o endereço:\n\nOu toque no botão para usar o padrão:", "ui_prompt_enter_delay": "⏱ **Insira o atraso desejado (segundos):**\n\nEnvie o número de segundos (ex.: `5` ou `15`).", "ui_prompt_enter_every_n": "🔢 **Informe o intervalo:**\n\nEnvie um número inteiro de 1 a 1000. `1` encaminha tudo; `5` encaminha a 1.ª, 6.ª, 11.ª … mensagem que passar nos filtros.", @@ -13003,6 +13522,7 @@ "ui_reason_branding_always": "o branding está ativo para todas as mensagens", "ui_reason_branding_disabled": "o branding está desligado ({reason})", "ui_reason_branding_no_triggers_configured": "o branding está ligado sem gatilhos, por isso todas as mensagens passam", + "ui_reason_branding_requires_channel_origin": "o branding só salta mensagens que vêm de um canal", "ui_reason_branding_requires_forward_origin": "o branding só encaminha mensagens que já são encaminhamentos", "ui_reason_branding_triggers_matched": "o salto correspondeu aos seus gatilhos ({words})", "ui_reason_branding_triggers_unmatched": "o salto não correspondeu a nenhum gatilho ({words})", @@ -13102,6 +13622,7 @@ "ui_reg_help_branding_match_mode": "any = um gatilho basta, all = todos os gatilhos configurados devem corresponder.", "ui_reg_help_branding_origin_ids": "Passar apenas quando a mensagem foi encaminhada de um destes ids/@nomes.", "ui_reg_help_branding_origin_titles": "Passar apenas quando a mensagem foi encaminhada de um destes canais.", + "ui_reg_help_branding_require_channel": "Recusa o salto a menos que a mensagem venha de um canal — usuários normais são encaminhados diretamente com o próprio nome.", "ui_reg_help_branding_require_forward": "Recusar o salto, a menos que a mensagem seja ela própria um encaminhamento.", "ui_reg_help_branding_triggers_enabled": "Desligado, toda mensagem correspondente passa por C; ligado, os gatilhos abaixo decidem.", "ui_reg_help_clean_custom_words": "Palavras removidas do texto.", @@ -13114,7 +13635,10 @@ "ui_reg_help_footer": "Texto adicionado ao fim de cada mensagem encaminhada.", "ui_reg_help_forward_every_n": "Encaminhar apenas a N-ésima mensagem que passa nos filtros desta tarefa (1 = todas as mensagens, 1000 = uma em cada mil). Um álbum conta como uma mensagem.", "ui_reg_help_header": "Texto adicionado ao início de cada mensagem encaminhada.", + "ui_reg_help_link_replace_enabled": "Reescreve cada link do texto pelo mapa de reescrita de links.", + "ui_reg_help_link_rewrite_map": "Pares de reescrita: {\"*\": \"https://t.me/eu\"} reescreve todos os links, \"dominioantigo.com\": \"dominionovo.com\" reescreve um domínio, \"https://antigo.t.me/x\": \"https://novo.t.me/y\" reescreve uma URL completa.", "ui_reg_help_max_time_edit": "Segundos após os quais uma edição deixa de ser refletida.", + "ui_reg_help_mention_rewrite_map": "Pares de reescrita de menções: {\"@lojaantiga\": \"@lojanova\"} reescreve um perfil (sem diferenciar maiúsculas), \"*\": \"@principal\" reescreve todos os @ID.", "ui_reg_help_ocr_image_ai": "Executar OCR nas imagens e encaminhar o texto extraído.", "ui_reg_help_power_control": "Visão prática de power_schedule.enabled.", "ui_reg_help_process_raw_text": "Processar o texto bruto antes de qualquer análise.", @@ -13176,6 +13700,7 @@ "ui_reg_set_branding_match_mode": "Modo de correspondência de marca", "ui_reg_set_branding_origin_ids": "IDs de origem", "ui_reg_set_branding_origin_titles": "Títulos de origem", + "ui_reg_set_branding_require_channel": "Salto só de canais", "ui_reg_set_branding_require_forward": "Exigir encaminhamento", "ui_reg_set_branding_triggers_enabled": "Gatilhos de marca", "ui_reg_set_button_to_text": "Botão para texto", @@ -13201,8 +13726,11 @@ "ui_reg_set_keyword_confirmation": "Confirmação por palavra", "ui_reg_set_keyword_reply": "Resposta por palavra", "ui_reg_set_link_buttons": "Botões de link", + "ui_reg_set_link_replace_enabled": "Substituir qualquer link encontrado", "ui_reg_set_link_replacement": "Substituição de links", + "ui_reg_set_link_rewrite_map": "Mapa de reescrita de links", "ui_reg_set_max_time_edit": "Tempo máximo para editar", + "ui_reg_set_mention_rewrite_map": "Mapa de reescrita de @menções", "ui_reg_set_news_digest": "Resumo de notícias", "ui_reg_set_ocr_image_ai": "IA de OCR de imagens", "ui_reg_set_paid_addons": "Complementos pagos", @@ -13287,6 +13815,26 @@ "ui_reset_config_done": "✅ Todas as configurações foram redefinidas.", "ui_retry": "🔄 Tentar novamente", "ui_retry_connect": "🚀 Tentar novamente", + "ui_rmp_btn_edit_links": "🔗 Editar mapa de links", + "ui_rmp_btn_edit_mentions": "❕ Editar mapa de @menções", + "ui_rmp_btn_links_off": "🔗 Substituir links: INATIVO", + "ui_rmp_btn_links_on": "🔗 Substituir links: ATIVO", + "ui_rmp_cleared": "✅ Mapa limpo.", + "ui_rmp_link_map": "🔗 Mapa de links ({count}): {pairs}", + "ui_rmp_link_map_empty": "🔗 Mapa de links: vazio", + "ui_rmp_link_state_off": "🔗 Substituição de links: DESATIVADA", + "ui_rmp_link_state_on": "🔗 Substituição de links: ATIVA — os links são reescritos pelo mapa", + "ui_rmp_links_prompt": "🔗 Envie o mapa de reescrita de links — um par por linha:\n`dominioantigo.com dominionovo.com` — reescreve um domínio\n`https://antigo.t.me/x https://novo.t.me/y` — reescreve um link\n`* https://t.me/eu` — reescreve TODOS os links\n\nEnvie - para limpar o mapa.", + "ui_rmp_mention_map": "❕ Mapa de menções ({count}): {pairs}", + "ui_rmp_mention_map_empty": "❕ Mapa de menções: vazio", + "ui_rmp_mentions_prompt": "❕ Envie o mapa de @menções — um par por linha:\n`@lojaantiga @lojanova` — reescreve um perfil\n`* @principal` — reescreve TODOS os @ID\n\nEnvie - para limpar o mapa.", + "ui_rmp_off_hint": "O mapa está definido mas a substituição de links está DESATIVADA — ative-a para aplicar.", + "ui_rmp_rule": "Tarefa: {name}", + "ui_rmp_saved": "✅ Salvo — {count} par(es).", + "ui_rmp_skipped": "✅ Salvo — {count} par(es). {skipped} linha(s) inválida(s) ignorada(s).", + "ui_rmp_title": "🔁 Substituir links e @menções", + "ui_rmp_toggle_off": "🔴 Substituição de links desativada.", + "ui_rmp_toggle_on": "🟢 Substituição de links ativada — cada link encontrado é reescrito pelo mapa.", "ui_route_add_sources": "📥 Origens ({count})", "ui_route_add_targets": "📤 Destinos ({count})", "ui_route_bad_edit": "❌ Envie `índice = valor`, ou `nome -> ` para uma nova rota.", @@ -13664,11 +14212,14 @@ "ui_smart_active_path": "🛣 **Rota ativa do fluxo de mensagens:**\n{path_desc}\n", "ui_smart_antiloop": "🔒 **Anti-loop e anti-envio-duplo:** ativo com chave de idempotência e verificação da origem da mensagem\n", "ui_smart_brand_block": "🔀 **Branding (A → C → B):**", + "ui_smart_brand_channel": "📡 Salto só de canais: {state}", "ui_smart_brand_origins": "🎯 **Canais de origem:** {origins}", "ui_smart_brand_state": "🎛 **Filtro do desvio:** {state}", "ui_smart_brand_state_all": "todas as mensagens", "ui_smart_brand_state_filtered": "filtrado", "ui_smart_brand_words": "🔑 **Palavras da rota:** {words}", + "ui_smart_btn_brand_chan_off": "📡 Salto só de canais: INATIVO", + "ui_smart_btn_brand_chan_on": "📡 Salto só de canais: ATIVO", "ui_smart_btn_brand_force_off": "🔁 Reencaminhar de C: OFF", "ui_smart_btn_brand_force_on": "🔁 Reencaminhar de C: ON", "ui_smart_btn_brand_gate_off": "🚧 Apenas reencaminhadas: OFF", @@ -13685,6 +14236,7 @@ "ui_smart_btn_prio_down": "➖ Prioridade (−10)", "ui_smart_btn_prio_up": "➕ Prioridade (+10)", "ui_smart_btn_regex": "🔣 Definir Regex", + "ui_smart_btn_replace_maps": "🔁 Mapas de links e @menções", "ui_smart_custom_header": "🏷 **Cabeçalho de cópia personalizado:** {header}", "ui_smart_hint": "👇 Para alterar as definições, toque nas opções abaixo:", "ui_smart_keywords": "🔎 **Palavras-chave de texto:** {keywords}", @@ -13712,6 +14264,7 @@ "ui_src_desc": "Novas mensagens deste chat serão lidas e encaminhadas.", "ui_src_sel": "📥 **Selecionar Canal ou Grupo de Origem:**", "ui_src_selected": "✅ Origem: {title}", + "ui_stale_entry": "⚠️ A lista mudou enquanto o menu estava aberto — mostrando o estado atual.", "ui_stat_allowed_word": "Permitido 🟢", "ui_stat_clean_word": "Limpar 🧹", "ui_stat_errors": "Erros", @@ -13886,10 +14439,10 @@ "ui_wl_count": "Quantidade de palavras permitidas: **{count}**\n", "ui_wl_desc": "Apenas mensagens que contenham pelo menos uma destas entradas serão encaminhadas.\n\nTrês formas de corresponder: **Palavra / Frase**, **Regex** ou **Correspondência de Texto Longo**.\n\n", "ui_wl_empty": "❌ Não há palavras na lista branca (todas as mensagens são permitidas).", - "ui_wl_only_regex": "🔒 Sem palavras-chave — mas as regras regex abaixo estão ativas e filtrando.", "ui_wl_entry_deleted_alert": "Excluído: {entry}", "ui_wl_hint": "👇 Escolha uma ação para gerir a lista branca:", "ui_wl_more": "\n... e mais {more} palavras", + "ui_wl_only_regex": "🔒 Sem palavras-chave — mas as regras regex abaixo estão ativas e filtrando.", "ui_wl_state_off": "Status: **desativado** (as palavras são mantidas, mas não aplicadas).\n\n", "ui_wl_state_on": "Status: **ativado** (as palavras são aplicadas).\n\n", "ui_wl_title": "✅ **Lista Branca de Palavras (Whitelist Keywords)**\n\n", @@ -14141,6 +14694,7 @@ "ui_ai_btn_health": "🩺 Проверка (Ping)", "ui_ai_btn_key": "🔑 Изменить ключ (API Key)", "ui_ai_btn_model": "🧠 Изменить модель", + "ui_ai_btn_open_prompts": "🧠 Управление промптами", "ui_ai_btn_pick_config": "🧠 Выбрать конфигурацию AI", "ui_ai_btn_pick_prompt": "📝 Выбрать промпт", "ui_ai_btn_sample": "🧪 Тест перезаписи текста", @@ -14207,6 +14761,7 @@ "ui_ai_prompt_enter_system_instruction": "🤖 Введите системные инструкции для '{name}':\n\nПример: `Перепишите это сообщение в деловом тоне, сохраняя суть.`", "ui_ai_prompt_label": "Промпт", "ui_ai_prompt_name_invalid": "❌ Некорректное имя промпта. Введите от 1 до 80 символов.", + "ui_ai_prompt_off_tag": "выкл", "ui_ai_prompt_staged_alert": "✅ Промпт выбран (будет сохранён при нажатии Сохранить)", "ui_ai_prompt_sys_empty_error": "❌ Системный промпт не может быть пустым.", "ui_ai_provider": "🧠 Отправьте провайдера: openai, anthropic, gemini, groq, deepseek, 9router, openrouter, custom", @@ -14312,10 +14867,10 @@ "ui_bl_count": "Количество заблокированных слов: **{count}**\n", "ui_bl_desc": "Сообщения, содержащие любую из этих записей, **НЕ** будут пересылаться.\n\nТри способа сопоставления: **Ключевое слово / Фраза**, **Regex** или **Поиск по длинному тексту**.\n\n", "ui_bl_empty": "❌ В чёрном списке нет слов.", - "ui_bl_only_regex": "🚫 Нет ключевых слов — но правила regex ниже активны и фильтруют.", "ui_bl_entry_deleted_alert": "Удалено: {entry}", "ui_bl_hint": "👇 Выберите действие для управления чёрным списком:", "ui_bl_more": "\n... и ещё {more} слов", + "ui_bl_only_regex": "🚫 Нет ключевых слов — но правила regex ниже активны и фильтруют.", "ui_bl_state_off": "Статус: **выключен** (слова сохранены, но не применяются).\n\n", "ui_bl_state_on": "Статус: **включён** (слова применяются).\n\n", "ui_bl_title": "🚫 **Чёрный список слов (Blacklist Keywords)**\n\n", @@ -14333,6 +14888,7 @@ "ui_block_video_on": "Блокировка видео включена 🚫", "ui_block_voice_off": "Голосовые сообщения разрешены 🟢", "ui_block_voice_on": "Блокировка голосовых сообщений включена 🚫", + "ui_brand_channel_saved": "📡 Переход только для каналов: {state}", "ui_brand_cleared": "🧹 Фильтр брендирования очищен — переход для всех сообщений.", "ui_brand_force_saved": "🔁 Репост из C: {state}", "ui_brand_gate_saved": "🚧 Условие «только репосты»: {state}", @@ -14346,6 +14902,7 @@ "ui_brand_words_saved": "✅ Слова маршрута сохранены: {value}", "ui_btn_ai_pv_assistant": "💬 Умный автоответчик в ЛС (AI PV Assistant)", "ui_btn_ar_assistant": "🤖 Автоответ ИИ (личные сообщения)", + "ui_btn_prompts": "🧠 Библиотека промптов", "ui_btn_use_default_host": "✅ Использовать по умолчанию (или далее)", "ui_bulk_confirm": "Удалить выбранные правила ({n})? Действие необратимо.", "ui_bulk_delete": "🗑 Удалить выбранные", @@ -14924,6 +15481,48 @@ "ui_plan_status_none": "0 дней", "ui_prev": "◀️ Назад", "ui_priority_alert": "Приоритет: {priority}", + "ui_prm_btn_back_list": "← Библиотека", + "ui_prm_btn_edit_desc": "📝 Описание", + "ui_prm_btn_edit_name": "✏️ Переименовать", + "ui_prm_btn_edit_sys": "🧩 Системный промпт", + "ui_prm_btn_edit_user": "✉️ Пользовательский шаблон", + "ui_prm_btn_new": "➕ Новый промпт", + "ui_prm_created": "✅ Промпт «{name}» создан и сохранён в вашу библиотеку.", + "ui_prm_delete_confirm": "⚠️ Удалить промпт «{name}»?\nЗадачи, которые на него ссылаются, вернутся к собственному промпту ИИ-конфигурации.", + "ui_prm_delete_yes": "🗑 Да, удалить", + "ui_prm_deleted": "🗑 Промпт удалён.", + "ui_prm_detail_desc": "📝 {desc}", + "ui_prm_detail_lang": "🌐 Целевой язык: {code}", + "ui_prm_detail_sys": "🧩 Системный промпт:\n{sys}", + "ui_prm_detail_title": "🧠 {name}", + "ui_prm_detail_user": "✉️ Пользовательский шаблон:\n{user}", + "ui_prm_detail_version": "🔖 Версия: v{version}", + "ui_prm_disabled_alert": "🔴 Промпт выключен — при отправке используется промпт ИИ-конфигурации.", + "ui_prm_edit_desc_prompt": "📝 Отправьте новое описание для «{name}» — отправьте -, чтобы очистить его.", + "ui_prm_edit_name_prompt": "✏️ Отправьте новое название для «{name}».", + "ui_prm_edit_sys_prompt": "🧩 Отправьте новый системный промпт для «{name}».", + "ui_prm_edit_user_prompt": "✉️ Отправьте новый пользовательский шаблон для «{name}».\nОн должен содержать подстановку {text} — отправьте -, чтобы сбросить его на «{text}».", + "ui_prm_enabled_alert": "🟢 Промпт включён.", + "ui_prm_error_generic": "⚠️ Не удалось сохранить промпт — попробуйте ещё раз.", + "ui_prm_hint": "Нажмите на промпт, чтобы управлять им. Выключенный промпт остаётся в библиотеке, но при отправке используется промпт ИИ-конфигурации задачи.", + "ui_prm_needs_text_placeholder": "✋ Пользовательский шаблон должен содержать подстановку {text}.", + "ui_prm_new_name_prompt": "🧠 Новый промпт — шаг 1/3\n\n✍️ Отправьте название промпта.", + "ui_prm_new_sys_prompt": "🧠 Новый промпт — шаг 2/3\n\n✍️ Отправьте системный промпт: инструкции, которым ИИ следует при каждой перезаписи.", + "ui_prm_new_user_prompt": "🧠 Новый промпт — шаг 3/3\n\n✍️ Отправьте пользовательский шаблон. Он должен содержать подстановку {text} — отправьте -, чтобы использовать просто «{text}».", + "ui_prm_none": "Шаблонов промптов пока нет. Нажмите ➕ Новый промпт, чтобы добавить.", + "ui_prm_not_found": "Промпт не найден — возможно, он был удалён.", + "ui_prm_saved": "✅ Сохранено — теперь v{version}.", + "ui_prm_state_off": "🔴 Выключен — при отправке используется собственный промпт ИИ-конфигурации", + "ui_prm_state_on": "🟢 Включён — используется при отправке", + "ui_prm_system_immutable": "Системные промпты общие для всех аккаунтов этой установки. Чтобы настроить свой, создайте новый промпт с нужной формулировкой.", + "ui_prm_system_tag": "общий", + "ui_prm_test_ask": "🧪 Отправьте пример сообщения для перезаписи через «{name}».", + "ui_prm_test_failed": "🧪 «{name}» — сбой ({code}):\n{message}", + "ui_prm_test_no_config": "Активная ИИ-конфигурация не найдена — сначала добавьте её в меню ИИ.", + "ui_prm_test_result": "🧪 «{name}» — результат ({model}):\n\n{result}", + "ui_prm_title": "🧠 Шаблоны промптов — {count} · 🟢 {active}", + "ui_prm_too_long": "✋ Слишком длинно — сократите текст и отправьте снова.", + "ui_prm_unavailable": "Сервис промптов недоступен на этой установке.", "ui_profile_actions": "👇 Действия с аккаунтом:", "ui_profile_reset_config": "⚠️ Сбросить параметры задач", "ui_profile_restart_setup": "🔄 Перезапустить настройку", @@ -14935,7 +15534,6 @@ "ui_prompt_change_base_url": "🌐 **Изменить адрес сервера:**\n\nТекущий адрес: `{current}`\n\n✍️ Отправьте новый адрес:\n\nИли введите `default` для сброса:", "ui_prompt_change_model": "🧠 **Изменить модель AI:**\n\nТекущая модель: `{model}`\n\n✍️ Введите название новой модели:\n(Например: `coding` или `gpt-4o-mini`)", "ui_prompt_edit_entry": "✏️ Отправьте новое значение для замены `{entry}`:", - "ui_stale_entry": "⚠️ Список изменился, пока меню было открыто — отображается текущее состояние.", "ui_prompt_enter_base_url": "🌐 **Настройка адреса сервера ({provider}):**\n\nАдрес по умолчанию:\n`{default_url}`\n\n✍️ Для использования другого хоста отправьте адрес:\n\nИли нажмите кнопку для использования стандартного:", "ui_prompt_enter_delay": "⏱ **Введите желаемую задержку (в секундах):**\n\nОтправьте число секунд (например, `5` или `15`).", "ui_prompt_enter_every_n": "🔢 **Введите интервал:**\n\nОтправьте целое число от 1 до 1000. `1` — пересылать всё; `5` — пересылать 1-е, 6-е, 11-е … сообщение, прошедшее фильтры.", @@ -14990,6 +15588,7 @@ "ui_reason_branding_always": "брендинг включён для всех сообщений", "ui_reason_branding_disabled": "брендинг выключен ({reason})", "ui_reason_branding_no_triggers_configured": "брендинг включён без триггеров, поэтому проходят все сообщения", + "ui_reason_branding_requires_channel_origin": "брендинг пересылает только сообщения, пришедшие из канала", "ui_reason_branding_requires_forward_origin": "брендинг пересылает только сами пересланные сообщения", "ui_reason_branding_triggers_matched": "переход совпал со своими триггерами ({words})", "ui_reason_branding_triggers_unmatched": "переход не совпал ни с одним триггером ({words})", @@ -15089,6 +15688,7 @@ "ui_reg_help_branding_match_mode": "any = достаточно одного триггера, all = должны совпасть все настроенные.", "ui_reg_help_branding_origin_ids": "Переход только если сообщение переслано из одного из этих id/@имён.", "ui_reg_help_branding_origin_titles": "Переход только если сообщение переслано из одного из этих каналов.", + "ui_reg_help_branding_require_channel": "Отказывает в переходе, если сообщение не из канала — обычные пользователи пересылаются напрямую со своим именем.", "ui_reg_help_branding_require_forward": "Переход отклоняется, если сообщение само не является пересланным.", "ui_reg_help_branding_triggers_enabled": "Когда выключено, каждое подходящее сообщение идёт через C; когда включено, решают триггеры ниже.", "ui_reg_help_clean_custom_words": "Слова, удаляемые из текста.", @@ -15101,7 +15701,10 @@ "ui_reg_help_footer": "Текст, добавляемый в конец каждого пересылаемого сообщения.", "ui_reg_help_forward_every_n": "Пересылать только каждое N-е сообщение, прошедшее фильтры задачи (1 = все сообщения, 1000 = одно из тысячи). Альбом считается одним сообщением.", "ui_reg_help_header": "Текст, добавляемый в начало каждого пересылаемого сообщения.", + "ui_reg_help_link_replace_enabled": "Переписывает каждую ссылку в тексте по таблице перезаписи ссылок.", + "ui_reg_help_link_rewrite_map": "Пары перезаписи: {\"*\": \"https://t.me/ja\"} перезаписывает все ссылки, \"старыйдомен.com\": \"новыйдомен.com\" перезаписывает один домен, \"https://old.t.me/x\": \"https://new.t.me/y\" перезаписывает полный URL.", "ui_reg_help_max_time_edit": "Через сколько секунд правка перестаёт отражаться.", + "ui_reg_help_mention_rewrite_map": "Пары перезаписи упоминаний: {\"@staryimagazin\": \"@novyimagazin\"} перезаписывает один аккаунт (без учёта регистра), \"*\": \"@glavnyi\" перезаписывает все @упоминания.", "ui_reg_help_ocr_image_ai": "Распознавать текст на изображениях и пересылать его.", "ui_reg_help_power_control": "Удобное представление power_schedule.enabled.", "ui_reg_help_process_raw_text": "Обрабатывать исходный текст до любого разбора.", @@ -15163,6 +15766,7 @@ "ui_reg_set_branding_match_mode": "Режим сопоставления брендинга", "ui_reg_set_branding_origin_ids": "ID источников", "ui_reg_set_branding_origin_titles": "Названия источников", + "ui_reg_set_branding_require_channel": "Переход только для каналов", "ui_reg_set_branding_require_forward": "Требовать пересылку", "ui_reg_set_branding_triggers_enabled": "Триггеры брендинга", "ui_reg_set_button_to_text": "Кнопки в текст", @@ -15188,8 +15792,11 @@ "ui_reg_set_keyword_confirmation": "Подтверждение словом", "ui_reg_set_keyword_reply": "Ответ по слову", "ui_reg_set_link_buttons": "Кнопки-ссылки", + "ui_reg_set_link_replace_enabled": "Заменять любую найденную ссылку", "ui_reg_set_link_replacement": "Замена ссылок", + "ui_reg_set_link_rewrite_map": "Таблица перезаписи ссылок", "ui_reg_set_max_time_edit": "Максимум времени на правку", + "ui_reg_set_mention_rewrite_map": "Таблица перезаписи @упоминаний", "ui_reg_set_news_digest": "Дайджест новостей", "ui_reg_set_ocr_image_ai": "ИИ-распознавание текста с картинок", "ui_reg_set_paid_addons": "Платные надстройки", @@ -15274,6 +15881,26 @@ "ui_reset_config_done": "✅ Все настройки успешно сброшены.", "ui_retry": "🔄 Попробовать снова", "ui_retry_connect": "🚀 Повторить подключение", + "ui_rmp_btn_edit_links": "🔗 Изменить таблицу ссылок", + "ui_rmp_btn_edit_mentions": "❕ Изменить таблицу @упоминаний", + "ui_rmp_btn_links_off": "🔗 Заменять ссылки: ВЫКЛ", + "ui_rmp_btn_links_on": "🔗 Заменять ссылки: ВКЛ", + "ui_rmp_cleared": "✅ Таблица очищена.", + "ui_rmp_link_map": "🔗 Таблица ссылок ({count}): {pairs}", + "ui_rmp_link_map_empty": "🔗 Таблица ссылок: пуста", + "ui_rmp_link_state_off": "🔗 Замена ссылок: ВЫКЛ", + "ui_rmp_link_state_on": "🔗 Замена ссылок: ВКЛ — ссылки переписываются по таблице", + "ui_rmp_links_prompt": "🔗 Отправьте таблицу перезаписи ссылок — одна пара в строке:\n`старыйдомен.ком новыйдомен.ком` — перезаписывает один домен\n`https://staryi.t.me/x https://novyi.t.me/y` — перезаписывает одну ссылку\n`* https://t.me/ja` — перезаписывает ВСЕ ссылки\n\nОтправьте -, чтобы очистить таблицу.", + "ui_rmp_mention_map": "❕ Таблица упоминаний ({count}): {pairs}", + "ui_rmp_mention_map_empty": "❕ Таблица упоминаний: пуста", + "ui_rmp_mentions_prompt": "❕ Отправьте таблицу @упоминаний — одна пара в строке:\n`@staryimagazin @novyimagazin` — перезаписывает один аккаунт\n`* @glavnyi` — перезаписывает ВСЕ @упоминания\n\nОтправьте -, чтобы очистить таблицу.", + "ui_rmp_off_hint": "Таблица задана, но замена ссылок ВЫКЛЮЧЕНА — включите её, чтобы применить.", + "ui_rmp_rule": "Задача: {name}", + "ui_rmp_saved": "✅ Сохранено — пар: {count}.", + "ui_rmp_skipped": "✅ Сохранено — пар: {count}. Пропущено неверных строк: {skipped}.", + "ui_rmp_title": "🔁 Замена ссылок и @упоминаний", + "ui_rmp_toggle_off": "🔴 Замена ссылок выключена.", + "ui_rmp_toggle_on": "🟢 Замена ссылок включена — каждая найденная ссылка переписывается по таблице.", "ui_route_add_sources": "📥 Источники ({count})", "ui_route_add_targets": "📤 Цели ({count})", "ui_route_bad_edit": "❌ Отправьте `index = значение` или `name -> ` для нового маршрута.", @@ -15651,11 +16278,14 @@ "ui_smart_active_path": "🛣 **Активный маршрут потока сообщений:**\n{path_desc}\n", "ui_smart_antiloop": "🔒 **Анти-цикл и анти-дубль:** активно с ключ идемпотентности и проверкой источника сообщения\n", "ui_smart_brand_block": "🔀 **Брендирование (A → C → B):**", + "ui_smart_brand_channel": "📡 Переход только для каналов: {state}", "ui_smart_brand_origins": "🎯 **Каналы-источники:** {origins}", "ui_smart_brand_state": "🎛 **Фильтр перехода:** {state}", "ui_smart_brand_state_all": "все сообщения", "ui_smart_brand_state_filtered": "с фильтром", "ui_smart_brand_words": "🔑 **Слова маршрута:** {words}", + "ui_smart_btn_brand_chan_off": "📡 Переход только для каналов: ВЫКЛ", + "ui_smart_btn_brand_chan_on": "📡 Переход только для каналов: ВКЛ", "ui_smart_btn_brand_force_off": "🔁 Репост из C: ВЫКЛ", "ui_smart_btn_brand_force_on": "🔁 Репост из C: ВКЛ", "ui_smart_btn_brand_gate_off": "🚧 Только репосты: ВЫКЛ", @@ -15672,6 +16302,7 @@ "ui_smart_btn_prio_down": "➖ Приоритет (−10)", "ui_smart_btn_prio_up": "➕ Приоритет (+10)", "ui_smart_btn_regex": "🔣 Задать Regex", + "ui_smart_btn_replace_maps": "🔁 Таблицы ссылок и @упоминаний", "ui_smart_custom_header": "🏷 **Пользовательский заголовок копии:** {header}", "ui_smart_hint": "👇 Чтобы изменить настройки, нажмите варианты ниже:", "ui_smart_keywords": "🔎 **Текстовые ключевые слова:** {keywords}", @@ -15699,6 +16330,7 @@ "ui_src_desc": "Новые сообщения из этого чата будут считываться и пересылаться.", "ui_src_sel": "📥 **Выбор исходного канала или группы (Source):**", "ui_src_selected": "✅ Источник: {title}", + "ui_stale_entry": "⚠️ Список изменился, пока меню было открыто — отображается текущее состояние.", "ui_stat_allowed_word": "Разрешено 🟢", "ui_stat_clean_word": "Очистка 🧹", "ui_stat_errors": "Ошибки", @@ -15873,10 +16505,10 @@ "ui_wl_count": "Количество разрешённых слов: **{count}**\n", "ui_wl_desc": "Пересылаются только сообщения, содержащие хотя бы одну из этих записей.\n\nТри способа сопоставления: **Ключевое слово / Фраза**, **Regex** или **Поиск по длинному тексту**.\n\n", "ui_wl_empty": "❌ В белом списке нет слов (разрешены все сообщения).", - "ui_wl_only_regex": "🔒 Нет ключевых слов — но правила regex ниже активны и фильтруют.", "ui_wl_entry_deleted_alert": "Удалено: {entry}", "ui_wl_hint": "👇 Выберите действие для управления белым списком:", "ui_wl_more": "\n... и ещё {more} слов", + "ui_wl_only_regex": "🔒 Нет ключевых слов — но правила regex ниже активны и фильтруют.", "ui_wl_state_off": "Статус: **выключен** (слова сохранены, но не применяются).\n\n", "ui_wl_state_on": "Статус: **включён** (слова применяются).\n\n", "ui_wl_title": "✅ **Белый список слов (Whitelist Keywords)**\n\n", @@ -16128,6 +16760,7 @@ "ui_ai_btn_health": "🩺 Sog'liqni tekshirish (Ping)", "ui_ai_btn_key": "🔑 Kalitni o'zgartirish (API Key)", "ui_ai_btn_model": "🧠 Modelni o'zgartirish", + "ui_ai_btn_open_prompts": "🧠 Promptlarni boshqarish", "ui_ai_btn_pick_config": "🧠 AI konfiguratsiyasini tanlash", "ui_ai_btn_pick_prompt": "📝 Prompt tanlash", "ui_ai_btn_sample": "🧪 Matnni qayta yozishni sinash", @@ -16194,6 +16827,7 @@ "ui_ai_prompt_enter_system_instruction": "🤖 '{name}' uchun tizimli prompt ko'rsatmalarini kiriting:\n\nMasalan: `Ushbu xabarni rasmiy ohangda qayta yozing.`", "ui_ai_prompt_label": "Prompt", "ui_ai_prompt_name_invalid": "❌ Yaroqsiz prompt nomi. 1 dan 80 gacha belgi kiriting.", + "ui_ai_prompt_off_tag": "o‘chirilgan", "ui_ai_prompt_staged_alert": "✅ Prompt tanlandi (Saqlash tugmasi bosilganda saqlanadi)", "ui_ai_prompt_sys_empty_error": "❌ Tizim prompti bo'sh bo'lishi mumkin emas.", "ui_ai_provider": "🧠 Provayderni yuboring: openai, anthropic, gemini, groq, deepseek, 9router, openrouter, custom", @@ -16299,10 +16933,10 @@ "ui_bl_count": "Bloklangan so'zlar soni: **{count}**\n", "ui_bl_desc": "Bu yozuvlardan birini o'z ichiga olgan xabarlar yuborilmaydi.\n\nUchta moslash usuli: **Kalit so'z / Ibora**, **Regex** yoki **Uzun matnni moslash**.\n\n", "ui_bl_empty": "❌ Qora ro'yxatda so'zlar yo'q.", - "ui_bl_only_regex": "🚫 Kalit so‘zlar yo‘q — lekin quyidagi regex qoidalari faol va filtrlamoqda.", "ui_bl_entry_deleted_alert": "Oʻchirildi: {entry}", "ui_bl_hint": "👇 Qora ro'yxatni boshqarish uchun amalni tanlang:", "ui_bl_more": "\n... yana {more} ta so'z", + "ui_bl_only_regex": "🚫 Kalit so‘zlar yo‘q — lekin quyidagi regex qoidalari faol va filtrlamoqda.", "ui_bl_state_off": "Holat: **oʻchirilgan** (soʻlar saqlanadi, lekin qoʻllanmaydi).\n\n", "ui_bl_state_on": "Holat: **yoqilgan** (soʻlar qoʻllanadi).\n\n", "ui_bl_title": "🚫 **Qora so'zlar ro'yxati (Blacklist Keywords)**\n\n", @@ -16320,6 +16954,7 @@ "ui_block_video_on": "Video bloklash yoqildi 🚫", "ui_block_voice_off": "Ovozli xabarlar ruxsat etiladi 🟢", "ui_block_voice_on": "Ovozli xabarlar bloklash yoqildi 🚫", + "ui_brand_channel_saved": "📡 Faqat kanallar o‘tishi: {state}", "ui_brand_cleared": "🧹 Brending filtri tozalandi — barcha xabarlar o'tadi.", "ui_brand_force_saved": "🔁 C dan forward: {state}", "ui_brand_gate_saved": "🚧 Faqat forward sharti: {state}", @@ -16333,6 +16968,7 @@ "ui_brand_words_saved": "✅ Yo'l so'zlari saqlandi: {value}", "ui_btn_ai_pv_assistant": "💬 Shaxsiy xabarlar uchun aqlli yordamchi (AI PV Assistant)", "ui_btn_ar_assistant": "🤖 AI avtojavob (shaxsiy xabarlar)", + "ui_btn_prompts": "🧠 Prompt kutubxonasi", "ui_btn_use_default_host": "✅ Standartdan foydalanish (yoki keyingisi)", "ui_bulk_confirm": "Tanlangan {n} ta qoida o‘chirilsinmi? Qaytarib bo‘lmaydi.", "ui_bulk_delete": "🗑 Tanlanganlarni o‘chirish", @@ -16911,6 +17547,48 @@ "ui_plan_status_none": "0 kun", "ui_prev": "◀️ Oldingi", "ui_priority_alert": "Ustuvorlik: {priority}", + "ui_prm_btn_back_list": "← Kutubxona", + "ui_prm_btn_edit_desc": "📝 Tavsif", + "ui_prm_btn_edit_name": "✏️ Nomini o‘zgartirish", + "ui_prm_btn_edit_sys": "🧩 Tizim prompti", + "ui_prm_btn_edit_user": "✉️ Foydalanuvchi shabloni", + "ui_prm_btn_new": "➕ Yangi prompt", + "ui_prm_created": "✅ “{name}” prompti yaratildi va kutubxonangizga saqlandi.", + "ui_prm_delete_confirm": "⚠️ “{name}” prompti o‘chirilsinmi?\nUnga ishorat qilgan vazifalar AI konfiguratsiyaning o‘z promptiga qaytadi.", + "ui_prm_delete_yes": "🗑 Ha, o‘chirish", + "ui_prm_deleted": "🗑 Prompt o‘chirildi.", + "ui_prm_detail_desc": "📝 {desc}", + "ui_prm_detail_lang": "🌐 Maqsadli til: {code}", + "ui_prm_detail_sys": "🧩 Tizim prompti:\n{sys}", + "ui_prm_detail_title": "🧠 {name}", + "ui_prm_detail_user": "✉️ Foydalanuvchi shabloni:\n{user}", + "ui_prm_detail_version": "🔖 Versiya: v{version}", + "ui_prm_disabled_alert": "🔴 Prompt o‘chirildi — yetkazishlar AI konfiguratsiyaning o‘z promptiga qaytadi.", + "ui_prm_edit_desc_prompt": "📝 “{name}” uchun yangi tavsifni yuboring — tozalash uchun - yuboring.", + "ui_prm_edit_name_prompt": "✏️ “{name}” uchun yangi nom yuboring.", + "ui_prm_edit_sys_prompt": "🧩 “{name}” uchun yangi tizim promptini yuboring.", + "ui_prm_edit_user_prompt": "✉️ “{name}” uchun yangi foydalanuvchi shablonini yuboring.\nUnda {text} joylovchisi bo‘lishi shart — “{text}” ga qaytarish uchun - yuboring.", + "ui_prm_enabled_alert": "🟢 Prompt yoqildi.", + "ui_prm_error_generic": "⚠️ Promptni saqlab bo‘lmadi — qayta urinib ko‘ring.", + "ui_prm_hint": "Boshqarish uchun promptni bosing. O‘chirilgan prompt kutubxonada qoladi, lekin yetkazishlar vazifaning AI konfiguratsiya promptidan foydalanadi.", + "ui_prm_needs_text_placeholder": "✋ Foydalanuvchi shablonida {text} joylovchisi bo‘lishi shart.", + "ui_prm_new_name_prompt": "🧠 Yangi prompt — 1/3 qadam\n\n✍️ Prompt nomini yuboring.", + "ui_prm_new_sys_prompt": "🧠 Yangi prompt — 2/3 qadam\n\n✍️ Tizim promptini yuboring: AI har qayta yozishda bajadigan ko‘rsatma.", + "ui_prm_new_user_prompt": "🧠 Yangi prompt — 3/3 qadam\n\n✍️ Foydalanuvchi shablonini yuboring. Unda {text} joylovchisi bo‘lishi shart — faqat “{text}” dan foydalanish uchun - yuboring.", + "ui_prm_none": "Hozircha prompt shabloni yo‘q. Qo‘shish uchun ➕ Yangi prompt ni bosing.", + "ui_prm_not_found": "Prompt topilmadi — balki o‘chirilgandir.", + "ui_prm_saved": "✅ Saqlandi — endi v{version}.", + "ui_prm_state_off": "🔴 O‘chirilgan — yetkazishlar AI konfiguratsiyaning o‘z promptidan foydalanadi", + "ui_prm_state_on": "🟢 Yoqilgan — yetkazishlarda ishlatiladi", + "ui_prm_system_immutable": "Tizim promptlari bu o‘rnatishdagi barcha hisoblar uchun umumiy. O‘zingiznikini sozlash uchun kerakli matn bilan yangi prompt yarating.", + "ui_prm_system_tag": "umumiy", + "ui_prm_test_ask": "🧪 “{name}” bilan qayta yozish uchun namuna xabar yuboring.", + "ui_prm_test_failed": "🧪 “{name}” muvaffaqiyatsiz ({code}):\n{message}", + "ui_prm_test_no_config": "Faol AI konfiguratsiya topilmadi — avval AI menyusundan qo‘shing.", + "ui_prm_test_result": "🧪 “{name}” — natija ({model}):\n\n{result}", + "ui_prm_title": "🧠 Prompt shablonlari — {count} · 🟢 {active}", + "ui_prm_too_long": "✋ Juda uzun — matnni qisqartirib qayta yuboring.", + "ui_prm_unavailable": "Prompt xizati bu o‘rnatishda mavjud emas.", "ui_profile_actions": "👇 Hisob amallari:", "ui_profile_reset_config": "⚠️ Vazifa sozlamalarini tiklash", "ui_profile_restart_setup": "🔄 Sozlashni qayta boshlash", @@ -16922,7 +17600,6 @@ "ui_prompt_change_base_url": "🌐 **Server manzilini o'zgartirish:**\n\nJoriy manzil: `{current}`\n\n✍️ Yangi manzilni yuboring:\n\nYoki standartga qaytarish uchun `default` yuboring:", "ui_prompt_change_model": "🧠 **AI modelini o'zgartirish:**\n\nJoriy model: `{model}`\n\n✍️ Yangi model nomini kiriting:\n(Misol: `coding` yoki `gpt-4o-mini`)", "ui_prompt_edit_entry": "✏️ `{entry}` ni almashtirish uchun yangi qiymat yuboring:", - "ui_stale_entry": "⚠️ Menyu ochiq turganda roʻyxat oʻzgardi — joriy holat koʻrsatilmoqda.", "ui_prompt_enter_base_url": "🌐 **Server manzilini sozlash ({provider}):**\n\nStandart manzil:\n`{default_url}`\n\n✍️ Maxsus xost ishlatmoqchi bo'lsangiz, manzilni yuboring:\n\nYoki standartdan foydalanish uchun tugmani bosing:", "ui_prompt_enter_delay": "⏱ **Kechikish miqdorini kiriting (soniyalarda):**\n\nIltimos kechikish soniyasini yuboring (masalan `5` yoki `15`).", "ui_prompt_enter_every_n": "🔢 **Oraliqni kiriting:**\n\n1 dan 1000 gacha butun son yuboring. `1` — hammasi uzatiladi; `5` — filtrlardan o'tgan 1-, 6-, 11-… xabar uzatiladi.", @@ -16977,6 +17654,7 @@ "ui_reason_branding_always": "branding barcha xabarlar uchun yoqilgan", "ui_reason_branding_disabled": "branding o'chirilgan ({reason})", "ui_reason_branding_no_triggers_configured": "branding triggersiz yoqilgan, shuning uchun hamma xabar o'tadi", + "ui_reason_branding_requires_channel_origin": "brending faqat kanaldan kelgan xabarlarni o‘tkazadi", "ui_reason_branding_requires_forward_origin": "branding faqat o'zi forward bo'lgan xabarlarni yuboradi", "ui_reason_branding_triggers_matched": "o'tish o'z triggerlariga mos keldi ({words})", "ui_reason_branding_triggers_unmatched": "o'tish hech bir triggeriga mos kelmadi ({words})", @@ -17076,6 +17754,7 @@ "ui_reg_help_branding_match_mode": "any = bitta trigger yetarli, all = barcha sozlangan triggerlar mos kelishi kerak.", "ui_reg_help_branding_origin_ids": "Faqat xabar shu chat id/@username lardan biridan yo'naltirilgan bo'lsa o'tadi.", "ui_reg_help_branding_origin_titles": "Faqat xabar shu kanal nomlaridan biridan yo'naltirilgan bo'lsa o'tadi.", + "ui_reg_help_branding_require_channel": "Xabar kanaldan kelmasa, o‘tishni rad etadi — oddiy foydalanuvchilar o‘z nomi bilan to‘g‘ridan-to‘g‘ri yuboriladi.", "ui_reg_help_branding_require_forward": "Xabarning o'zi yo'naltirilgan bo'lmasa, o'tish rad etiladi.", "ui_reg_help_branding_triggers_enabled": "O'chiq bo'lsa, har bir mos xabar C orqali o'tadi; yoqilganda quyidagi triggerlar hal qiladi.", "ui_reg_help_clean_custom_words": "Matndan olib tashlanadigan so'zlar.", @@ -17088,7 +17767,10 @@ "ui_reg_help_footer": "Har bir yo'naltirilgan xabar oxiriga qo'shiladigan matn.", "ui_reg_help_forward_every_n": "Faqat shu vazifa filtrlaridan o'tgan har N-xabarni uzatish (1 = barcha xabarlar, 1000 = mingdan bittasi). Albom bitta xabar hisoblanadi.", "ui_reg_help_header": "Har bir yo'naltirilgan xabar boshiga qo'shiladigan matn.", + "ui_reg_help_link_replace_enabled": "Matndagi har bir havolani havolalarni qayta yozish jadvali bo‘yicha qayta yozadi.", + "ui_reg_help_link_rewrite_map": "Qayta yozish juftliklari: {\"*\": \"https://t.me/men\"} hamma havolani qayta yozadi, \"eski-domen.com\": \"yangi-domen.com\" bitta domenni, \"https://eski.t.me/x\": \"https://yangi.t.me/y\" to‘liq URLni qayta yozadi.", "ui_reg_help_max_time_edit": "Shu soniyadan keyin tahrirlar aks ettirilmaydi.", + "ui_reg_help_mention_rewrite_map": "Eslatma qayta yozish juftliklari: {\"@eski-dokon\": \"@yangi-dokon\"} bitta hisobni qayta yozadi (katta-kichik harf farqi hisobga olinmaydi), \"*\": \"@asosiy\" hamma @ID ni qayta yozadi.", "ui_reg_help_ocr_image_ai": "Rasmlarda OCR ishlatib, olingan matnni yo'naltirish.", "ui_reg_help_power_control": "power_schedule.enabled uchun qulay ko'rinish.", "ui_reg_help_process_raw_text": "Har qanday tahlildan oldin xom matnni qayta ishlash.", @@ -17150,6 +17832,7 @@ "ui_reg_set_branding_match_mode": "Brend moslik rejimi", "ui_reg_set_branding_origin_ids": "Manba ID lari", "ui_reg_set_branding_origin_titles": "Manba sarlavhalari", + "ui_reg_set_branding_require_channel": "Faqat kanallar o‘tishi", "ui_reg_set_branding_require_forward": "Yo'naltirishni talab qilish", "ui_reg_set_branding_triggers_enabled": "Brend triggerlari", "ui_reg_set_button_to_text": "Tugmani matnga aylantirish", @@ -17175,8 +17858,11 @@ "ui_reg_set_keyword_confirmation": "Kalit so'z bilan tasdiqlash", "ui_reg_set_keyword_reply": "Kalit so'zga javob", "ui_reg_set_link_buttons": "Havola tugmalari", + "ui_reg_set_link_replace_enabled": "Topilgan har qanday havolani almashtirish", "ui_reg_set_link_replacement": "Havolani almashtirish", + "ui_reg_set_link_rewrite_map": "Havolalarni qayta yozish jadvali", "ui_reg_set_max_time_edit": "Tahrirlash uchun maksimal vaqt", + "ui_reg_set_mention_rewrite_map": "@Eslatmalarni qayta yozish jadvali", "ui_reg_set_news_digest": "Yangiliklar dayjesti", "ui_reg_set_ocr_image_ai": "Rasmdan matn o'qish AI", "ui_reg_set_paid_addons": "Pullik qo'shimchalar", @@ -17261,6 +17947,26 @@ "ui_reset_config_done": "✅ Barcha sozlamalar muvaffaqiyatli tiklandi.", "ui_retry": "🔄 Qayta urinish", "ui_retry_connect": "🚀 Qayta ulanish", + "ui_rmp_btn_edit_links": "🔗 Havolalar jadvalini tahrirlash", + "ui_rmp_btn_edit_mentions": "❕ @Eslatmalar jadvalini tahrirlash", + "ui_rmp_btn_links_off": "🔗 Havolalarni almashtirish: O‘CHIRILGAN", + "ui_rmp_btn_links_on": "🔗 Havolalarni almashtirish: YOQIL", + "ui_rmp_cleared": "✅ Jadval tozalandi.", + "ui_rmp_link_map": "🔗 Havolalar jadvali ({count}): {pairs}", + "ui_rmp_link_map_empty": "🔗 Havolalar jadvali: bo‘sh", + "ui_rmp_link_state_off": "🔗 Havolani almashtirish: O‘CHIRILGAN", + "ui_rmp_link_state_on": "🔗 Havolani almashtirish: YOQIL — havolalar jadval bo‘yicha qayta yoziladi", + "ui_rmp_links_prompt": "🔗 Havolalarni qayta yozish jadvalini yuboring — har bir juftlik alohida qatorda:\n`eski-domen.com yangi-domen.com` — bitta domenni qayta yozadi\n`https://eski.t.me/x https://yangi.t.me/y` — bitta havolani qayta yozadi\n`* https://t.me/men` — HAMMA havolani qayta yozadi\n\nJadvalni tozalash uchun - yuboring.", + "ui_rmp_mention_map": "❕ Eslatmalar jadvali ({count}): {pairs}", + "ui_rmp_mention_map_empty": "❕ Eslatmalar jadvali: bo‘sh", + "ui_rmp_mentions_prompt": "❕ @Eslatmalar jadvalini yuboring — har bir juftlik alohida qatorda:\n`@eski-dokon @yangi-dokon` — bitta hisobni qayta yozadi\n`* @asosiy` — HAMMA @ID ni qayta yozadi\n\nJadvalni tozalash uchun - yuboring.", + "ui_rmp_off_hint": "Jadval to‘ldirilgan, lekin havolani almashtirish O‘CHIRILGAN — qo‘llash uchun yoqing.", + "ui_rmp_rule": "Vazifa: {name}", + "ui_rmp_saved": "✅ Saqlandi — {count} juftlik.", + "ui_rmp_skipped": "✅ Saqlandi — {count} juftlik. {skipped} yaroqsiz qator tashlab ketildi.", + "ui_rmp_title": "🔁 Havolalar va @eslatmalarni almashtirish", + "ui_rmp_toggle_off": "🔴 Havolani almashtirish o‘chirildi.", + "ui_rmp_toggle_on": "🟢 Havolani almashtirish yoqildi — topilgan har bir havola jadval bo‘yicha qayta yoziladi.", "ui_route_add_sources": "📥 Manbalar ({count})", "ui_route_add_targets": "📤 Nishonlar ({count})", "ui_route_bad_edit": "❌ `index = value` yuboring yoki yangi marshrut uchun `name -> `.", @@ -17638,11 +18344,14 @@ "ui_smart_active_path": "🛣 **Faol xabar oqimi marshruti:**\n{path_desc}\n", "ui_smart_antiloop": "🔒 **Anti-tsikl va anti-qayta-yuborish:** idempotentlik kaliti va xabar manbaini tekshirish bilan faol\n", "ui_smart_brand_block": "🔀 **Brending (A → C → B):**", + "ui_smart_brand_channel": "📡 Faqat kanallar o‘tishi: {state}", "ui_smart_brand_origins": "🎯 **Manba kanallari:** {origins}", "ui_smart_brand_state": "🎛 **O'tish filtri:** {state}", "ui_smart_brand_state_all": "barcha xabarlar", "ui_smart_brand_state_filtered": "filtrlangan", "ui_smart_brand_words": "🔑 **Yo'l so'zlari:** {words}", + "ui_smart_btn_brand_chan_off": "📡 Faqat kanallar o‘tishi: O‘CHIRILGAN", + "ui_smart_btn_brand_chan_on": "📡 Faqat kanallar o‘tishi: YOQIL", "ui_smart_btn_brand_force_off": "🔁 C dan forward: O'CH", "ui_smart_btn_brand_force_on": "🔁 C dan forward: YON", "ui_smart_btn_brand_gate_off": "🚧 Faqat forward: O'CH", @@ -17659,6 +18368,7 @@ "ui_smart_btn_prio_down": "➖ Ustuvorlik (−10)", "ui_smart_btn_prio_up": "➕ Ustuvorlik (+10)", "ui_smart_btn_regex": "🔣 Regex o'rnatish", + "ui_smart_btn_replace_maps": "🔁 Havola va @eslatma jadvallari", "ui_smart_custom_header": "🏷 **Maxsus nusxa sarlavhasi:** {header}", "ui_smart_hint": "👇 Sozlamalarni o'zgartirish uchun quyidagi variantlarni bosing:", "ui_smart_keywords": "🔎 **Matn kalit so'zlari:** {keywords}", @@ -17686,6 +18396,7 @@ "ui_src_desc": "Yangi xabarlar ushbu suhbatdan o'qiladi va yo'naltiriladi.", "ui_src_sel": "📥 **Manba kanal yoki guruhini tanlash (Source):**", "ui_src_selected": "✅ Manba: {title}", + "ui_stale_entry": "⚠️ Menyu ochiq turganda roʻyxat oʻzgardi — joriy holat koʻrsatilmoqda.", "ui_stat_allowed_word": "Ruxsat berilgan 🟢", "ui_stat_clean_word": "Tozalash 🧹", "ui_stat_errors": "Xatolar", @@ -17860,10 +18571,10 @@ "ui_wl_count": "Ruxsat etilgan so'zlar soni: **{count}**\n", "ui_wl_desc": "Faqat ushbu yozuvlardan kamida birini o'z ichiga olgan xabarlar yuboriladi.\n\nUchta moslash usuli: **Kalit so'z / Ibora**, **Regex** yoki **Uzun matnni moslash**.\n\n", "ui_wl_empty": "❌ Oq ro'yxatda so'zlar yo'q (barcha xabarlarga ruxsat berilgan).", - "ui_wl_only_regex": "🔒 Kalit so‘zlar yo‘q — lekin quyidagi regex qoidalari faol va filtrlamoqda.", "ui_wl_entry_deleted_alert": "Oʻchirildi: {entry}", "ui_wl_hint": "👇 Oq ro'yxatni boshqarish uchun amalni tanlang:", "ui_wl_more": "\n... yana {more} ta so'z", + "ui_wl_only_regex": "🔒 Kalit so‘zlar yo‘q — lekin quyidagi regex qoidalari faol va filtrlamoqda.", "ui_wl_state_off": "Holat: **oʻchirilgan** (soʻlar saqlanadi, lekin qoʻllanmaydi).\n\n", "ui_wl_state_on": "Holat: **yoqilgan** (soʻlar qoʻllanadi).\n\n", "ui_wl_title": "✅ **Oq so'zlar ro'yxati (Whitelist Keywords)**\n\n", @@ -18115,6 +18826,7 @@ "ui_ai_btn_health": "🩺 健康检查 (Ping)", "ui_ai_btn_key": "🔑 更改密钥 (API Key)", "ui_ai_btn_model": "🧠 更改模型", + "ui_ai_btn_open_prompts": "🧠 管理提示词", "ui_ai_btn_pick_config": "🧠 选择 AI 配置", "ui_ai_btn_pick_prompt": "📝 选择提示词", "ui_ai_btn_sample": "🧪 测试文本重写", @@ -18181,6 +18893,7 @@ "ui_ai_prompt_enter_system_instruction": "🤖 请为 '{name}' 输入系统提示词指令:\n\n例如:`请用商务正式语气重写此消息,不改变核心事实。`", "ui_ai_prompt_label": "提示词", "ui_ai_prompt_name_invalid": "❌ 无效的提示词名称。请输入 1 到 80 个字符的名称。", + "ui_ai_prompt_off_tag": "已停用", "ui_ai_prompt_staged_alert": "✅ 提示词已选择(点击保存时生效)", "ui_ai_prompt_sys_empty_error": "❌ 系统提示词不能为空。请输入重写指令。", "ui_ai_provider": "🧠 发送供应商:openai, anthropic, gemini, groq, deepseek, 9router, openrouter, custom", @@ -18286,10 +18999,10 @@ "ui_bl_count": "已屏蔽关键词数量:**{count}**\n", "ui_bl_desc": "包含以下任一条目的消息**将不会**被转发。\n\n三种匹配方式:**关键词/短语**、**正则表达式**或**长文本匹配**。\n\n", "ui_bl_empty": "❌ 黑名单中没有关键词。", - "ui_bl_only_regex": "🚫 没有关键词 — 但下方的 regex 规则正在生效并过滤。", "ui_bl_entry_deleted_alert": "已删除:{entry}", "ui_bl_hint": "👇 选择操作以管理黑名单:", "ui_bl_more": "\n... 还有 {more} 个词", + "ui_bl_only_regex": "🚫 没有关键词 — 但下方的 regex 规则正在生效并过滤。", "ui_bl_state_off": "状态:**已停用**(词语保留但不生效)。\n\n", "ui_bl_state_on": "状态:**已启用**(词语生效)。\n\n", "ui_bl_title": "🚫 **关键词黑名单(Blacklist Keywords)**\n\n", @@ -18307,6 +19020,7 @@ "ui_block_video_on": "视频阻止已启用 🚫", "ui_block_voice_off": "语音消息已允许 🟢", "ui_block_voice_on": "语音阻止已启用 🚫", + "ui_brand_channel_saved": "📡 仅频道跳转:{state}", "ui_brand_cleared": "🧹 品牌过滤已清除 — 所有消息都会跳转。", "ui_brand_force_saved": "🔁 从 C 转发:{state}", "ui_brand_gate_saved": "🚧 仅转发条件:{state}", @@ -18320,6 +19034,7 @@ "ui_brand_words_saved": "✅ 路径关键词已保存:{value}", "ui_btn_ai_pv_assistant": "💬 智能私聊助手 (AI PV Assistant)", "ui_btn_ar_assistant": "🤖 AI 自动回复(私聊消息)", + "ui_btn_prompts": "🧠 提示词库", "ui_btn_use_default_host": "✅ 使用默认(或下一步)", "ui_bulk_confirm": "删除选中的 {n} 条规则?此操作无法撤销。", "ui_bulk_delete": "🗑 删除所选", @@ -18898,6 +19613,48 @@ "ui_plan_status_none": "0 天", "ui_prev": "◀️ 上一页", "ui_priority_alert": "优先级:{priority}", + "ui_prm_btn_back_list": "← 提示词库", + "ui_prm_btn_edit_desc": "📝 描述", + "ui_prm_btn_edit_name": "✏️ 重命名", + "ui_prm_btn_edit_sys": "🧩 系统提示词", + "ui_prm_btn_edit_user": "✉️ 用户模板", + "ui_prm_btn_new": "➕ 新建提示词", + "ui_prm_created": "✅ 提示词“{name}”已创建并保存到您的库中。", + "ui_prm_delete_confirm": "⚠️ 删除提示词“{name}”?\n引用它的任务将回退到其 AI 配置自带的提示词。", + "ui_prm_delete_yes": "🗑 是,删除", + "ui_prm_deleted": "🗑 提示词已删除。", + "ui_prm_detail_desc": "📝 {desc}", + "ui_prm_detail_lang": "🌐 目标语言:{code}", + "ui_prm_detail_sys": "🧩 系统提示词:\n{sys}", + "ui_prm_detail_title": "🧠 {name}", + "ui_prm_detail_user": "✉️ 用户模板:\n{user}", + "ui_prm_detail_version": "🔖 版本:v{version}", + "ui_prm_disabled_alert": "🔴 提示词已停用 — 投递将回退到 AI 配置自带的提示词。", + "ui_prm_edit_desc_prompt": "📝 请发送“{name}”的新描述 — 发送 - 可清除。", + "ui_prm_edit_name_prompt": "✏️ 请发送“{name}”的新名称。", + "ui_prm_edit_sys_prompt": "🧩 请发送“{name}”的新系统提示词。", + "ui_prm_edit_user_prompt": "✉️ 请发送“{name}”的新用户模板。\n必须包含 {text} 占位符 — 发送 - 可重置为“{text}”。", + "ui_prm_enabled_alert": "🟢 提示词已启用。", + "ui_prm_error_generic": "⚠️ 无法保存提示词 — 请重试。", + "ui_prm_hint": "点按提示词进行管理。停用的提示词仍保留在库中,但投递会使用任务 AI 配置自带的提示词。", + "ui_prm_needs_text_placeholder": "✋ 用户模板必须包含 {text} 占位符。", + "ui_prm_new_name_prompt": "🧠 新建提示词 — 第 1/3 步\n\n✍️ 请发送提示词名称。", + "ui_prm_new_sys_prompt": "🧠 新建提示词 — 第 2/3 步\n\n✍️ 请发送系统提示词:AI 在每次改写时遵循的指令。", + "ui_prm_new_user_prompt": "🧠 新建提示词 — 第 3/3 步\n\n✍️ 请发送用户模板。必须包含 {text} 占位符 — 发送 - 表示仅使用“{text}”。", + "ui_prm_none": "还没有提示词模板。点按 ➕ 新建提示词 来添加。", + "ui_prm_not_found": "未找到提示词 — 它可能已被删除。", + "ui_prm_saved": "✅ 已保存 — 现为 v{version}。", + "ui_prm_state_off": "🔴 已停用 — 投递会使用 AI 配置自带的提示词", + "ui_prm_state_on": "🟢 已启用 — 投递时使用", + "ui_prm_system_immutable": "系统提示词由本安装的所有账号共享。如需自定义,请新建一个包含所需内容的提示词。", + "ui_prm_system_tag": "共享", + "ui_prm_test_ask": "🧪 请发送一条示例消息,用“{name}”进行改写。", + "ui_prm_test_failed": "🧪 “{name}” 失败({code}):\n{message}", + "ui_prm_test_no_config": "未找到已启用的 AI 配置 — 请先在 AI 菜单中添加。", + "ui_prm_test_result": "🧪 “{name}” — 结果({model}):\n\n{result}", + "ui_prm_title": "🧠 提示词模板 — 共 {count} · 🟢 {active}", + "ui_prm_too_long": "✋ 太长了 — 请缩短文本后重新发送。", + "ui_prm_unavailable": "此安装上提示词服务不可用。", "ui_profile_actions": "👇 账户操作:", "ui_profile_reset_config": "⚠️ 重置任务设置", "ui_profile_restart_setup": "🔄 重新运行设置", @@ -18909,7 +19666,6 @@ "ui_prompt_change_base_url": "🌐 **更改服务器地址:**\n\n当前地址:`{current}`\n\n✍️ 请发送新地址:\n\n或发送 `default` 重置为默认:", "ui_prompt_change_model": "🧠 **更改 AI 模型:**\n\n当前模型:`{model}`\n\n✍️ 请输入新模型名称:\n(例如:`coding` 或 `gpt-4o-mini`)", "ui_prompt_edit_entry": "✏️ 发送新值以替换 `{entry}`:", - "ui_stale_entry": "⚠️ 菜单打开时列表已更改 — 正在显示当前状态。", "ui_prompt_enter_base_url": "🌐 **设置服务器地址({provider}):**\n\n默认地址:\n`{default_url}`\n\n✍️ 如需使用自定义主机或端口,请发送地址:\n\n或点击按钮使用默认地址:", "ui_prompt_enter_delay": "⏱ **输入所需延迟(秒):**\n\n请发送以秒为单位的延迟数(例如 `5` 或 `15`)。", "ui_prompt_enter_every_n": "🔢 **请输入间隔:**\n\n发送 1 到 1000 之间的整数。`1` 表示全部转发;`5` 表示转发第 1、6、11 … 条通过过滤的消息。", @@ -18964,6 +19720,7 @@ "ui_reason_branding_always": "所有消息都启用品牌跳转", "ui_reason_branding_disabled": "品牌跳转已关闭({reason})", "ui_reason_branding_no_triggers_configured": "品牌跳转已开启但没有触发器,因此所有消息都跳转", + "ui_reason_branding_requires_channel_origin": "品牌化仅对来自频道的消息执行跳转", "ui_reason_branding_requires_forward_origin": "品牌跳转只处理本身就是转发的消息", "ui_reason_branding_triggers_matched": "跳转符合其触发器({words})", "ui_reason_branding_triggers_unmatched": "跳转不符合任何触发器({words})", @@ -19063,6 +19820,7 @@ "ui_reg_help_branding_match_mode": "any = 一个触发条件即可,all = 所有已配置的条件都必须匹配。", "ui_reg_help_branding_origin_ids": "仅当消息转发自这些聊天 ID/@用户名之一时才中转。", "ui_reg_help_branding_origin_titles": "仅当消息转发自这些频道名称之一时才中转。", + "ui_reg_help_branding_require_channel": "除非消息来自频道,否则拒绝跳转 — 普通用户将以自己的名字直接转发。", "ui_reg_help_branding_require_forward": "除非消息本身是转发,否则拒绝中转。", "ui_reg_help_branding_triggers_enabled": "关闭时,所有匹配的消息都经由 C 中转;开启时,由下面的触发条件决定。", "ui_reg_help_clean_custom_words": "从文本中删除的词。", @@ -19075,7 +19833,10 @@ "ui_reg_help_footer": "添加到每条转发消息末尾的文本。", "ui_reg_help_forward_every_n": "只转发通过本任务过滤条件的第 N 条消息(1 = 全部消息,1000 = 千分之一)。相册算作一条消息。", "ui_reg_help_header": "添加到每条转发消息开头的文本。", + "ui_reg_help_link_replace_enabled": "按链接改写映射表改写文本中的每个链接。", + "ui_reg_help_link_rewrite_map": "改写键值对:{\"*\": \"https://t.me/me\"} 改写所有链接,\"旧域名.com\": \"新域名.com\" 改写一个域名,\"https://old.t.me/x\": \"https://new.t.me/y\" 改写完整 URL。", "ui_reg_help_max_time_edit": "超过该秒数后的编辑不再同步。", + "ui_reg_help_mention_rewrite_map": "提及改写键值对:{\"@旧店铺\": \"@新店铺\"} 改写一个账号(不区分大小写),\"*\": \"@主页\" 改写所有 @ID。", "ui_reg_help_ocr_image_ai": "对图片运行 OCR 并转发识别出的文字。", "ui_reg_help_power_control": "power_schedule.enabled 的便捷视图。", "ui_reg_help_process_raw_text": "在任何解析之前先处理原始文本。", @@ -19137,6 +19898,7 @@ "ui_reg_set_branding_match_mode": "品牌匹配模式", "ui_reg_set_branding_origin_ids": "来源 ID", "ui_reg_set_branding_origin_titles": "来源标题", + "ui_reg_set_branding_require_channel": "仅频道跳转", "ui_reg_set_branding_require_forward": "要求转发", "ui_reg_set_branding_triggers_enabled": "品牌触发", "ui_reg_set_button_to_text": "按钮转文字", @@ -19162,8 +19924,11 @@ "ui_reg_set_keyword_confirmation": "关键词确认", "ui_reg_set_keyword_reply": "关键词回复", "ui_reg_set_link_buttons": "链接按钮", + "ui_reg_set_link_replace_enabled": "替换找到的任何链接", "ui_reg_set_link_replacement": "链接替换", + "ui_reg_set_link_rewrite_map": "链接改写映射表", "ui_reg_set_max_time_edit": "最长可编辑时间", + "ui_reg_set_mention_rewrite_map": "@提及改写映射表", "ui_reg_set_news_digest": "新闻摘要", "ui_reg_set_ocr_image_ai": "图片 OCR 识别", "ui_reg_set_paid_addons": "付费附加功能", @@ -19248,6 +20013,26 @@ "ui_reset_config_done": "✅ 所有设置已成功重置。", "ui_retry": "🔄 重试", "ui_retry_connect": "🚀 重试连接", + "ui_rmp_btn_edit_links": "🔗 编辑链接映射", + "ui_rmp_btn_edit_mentions": "❕ 编辑 @提及映射", + "ui_rmp_btn_links_off": "🔗 替换链接:已关闭", + "ui_rmp_btn_links_on": "🔗 替换链接:已开启", + "ui_rmp_cleared": "✅ 映射已清空。", + "ui_rmp_link_map": "🔗 链接映射({count}):{pairs}", + "ui_rmp_link_map_empty": "🔗 链接映射:空", + "ui_rmp_link_state_off": "🔗 链接替换:已关闭", + "ui_rmp_link_state_on": "🔗 链接替换:已开启 — 链接将按映射表改写", + "ui_rmp_links_prompt": "🔗 发送链接改写映射 — 每行一对:\n`旧域名.com 新域名.com` — 改写一个域名\n`https://old.t.me/x https://new.t.me/y` — 改写一个链接\n`* https://t.me/me` — 改写所有链接\n\n发送 - 清空映射。", + "ui_rmp_mention_map": "❕ 提及映射({count}):{pairs}", + "ui_rmp_mention_map_empty": "❕ 提及映射:空", + "ui_rmp_mentions_prompt": "❕ 发送 @提及映射 — 每行一对:\n`@旧店铺 @新店铺` — 改写一个账号\n`* @主页` — 改写所有 @ID\n\n发送 - 清空映射。", + "ui_rmp_off_hint": "映射表已设置但链接替换已关闭 — 请开启以生效。", + "ui_rmp_rule": "任务:{name}", + "ui_rmp_saved": "✅ 已保存 — {count} 对。", + "ui_rmp_skipped": "✅ 已保存 — {count} 对。已跳过 {skipped} 个无效行。", + "ui_rmp_title": "🔁 替换链接与 @提及", + "ui_rmp_toggle_off": "🔴 链接替换已关闭。", + "ui_rmp_toggle_on": "🟢 链接替换已启用 — 找到的每个链接都会按映射表改写。", "ui_route_add_sources": "📥 来源({count})", "ui_route_add_targets": "📤 目标({count})", "ui_route_bad_edit": "❌ 发送 `序号 = 值`,或 `名称 <来源> -> <目标>` 来新建路由。", @@ -19625,11 +20410,14 @@ "ui_smart_active_path": "🛣 **当前消息流路线:**\n{path_desc}\n", "ui_smart_antiloop": "🔒 **防循环与防重复发送:**已启用,通过 幂等键 和消息来源检查\n", "ui_smart_brand_block": "🔀 **品牌跳转(A → C → B):**", + "ui_smart_brand_channel": "📡 仅频道跳转:{state}", "ui_smart_brand_origins": "🎯 **来源频道:**{origins}", "ui_smart_brand_state": "🎛 **跳转过滤:**{state}", "ui_smart_brand_state_all": "所有消息", "ui_smart_brand_state_filtered": "已过滤", "ui_smart_brand_words": "🔑 **路径关键词:**{words}", + "ui_smart_btn_brand_chan_off": "📡 仅频道跳转:已关闭", + "ui_smart_btn_brand_chan_on": "📡 仅频道跳转:已开启", "ui_smart_btn_brand_force_off": "🔁 从 C 转发:关", "ui_smart_btn_brand_force_on": "🔁 从 C 转发:开", "ui_smart_btn_brand_gate_off": "🚧 仅转发:关", @@ -19646,6 +20434,7 @@ "ui_smart_btn_prio_down": "➖ 优先级 (−10)", "ui_smart_btn_prio_up": "➕ 优先级 (+10)", "ui_smart_btn_regex": "🔣 设置 Regex", + "ui_smart_btn_replace_maps": "🔁 链接与 @替换映射", "ui_smart_custom_header": "🏷 **自定义副本标题:**{header}", "ui_smart_hint": "👇 要更改设置,请点击下方选项:", "ui_smart_keywords": "🔎 **文本关键词:**{keywords}", @@ -19673,6 +20462,7 @@ "ui_src_desc": "来自此对话的新消息将被读取并转发。", "ui_src_sel": "📥 **选择源频道或群组 (Source):**", "ui_src_selected": "✅ 来源: {title}", + "ui_stale_entry": "⚠️ 菜单打开时列表已更改 — 正在显示当前状态。", "ui_stat_allowed_word": "允许 🟢", "ui_stat_clean_word": "清理 🧹", "ui_stat_errors": "错误", @@ -19847,10 +20637,10 @@ "ui_wl_count": "已允许关键词数量:**{count}**\n", "ui_wl_desc": "仅转发包含以下至少一个条目的消息。\n\n三种匹配方式:**关键词/短语**、**正则表达式**或**长文本匹配**。\n\n", "ui_wl_empty": "❌ 白名单中没有关键词(允许所有消息)。", - "ui_wl_only_regex": "🔒 没有关键词 — 但下方的 regex 规则正在生效并过滤。", "ui_wl_entry_deleted_alert": "已删除:{entry}", "ui_wl_hint": "👇 选择操作以管理白名单:", "ui_wl_more": "\n... 还有 {more} 个词", + "ui_wl_only_regex": "🔒 没有关键词 — 但下方的 regex 规则正在生效并过滤。", "ui_wl_state_off": "状态:**已停用**(词语保留但不生效)。\n\n", "ui_wl_state_on": "状态:**已启用**(词语生效)。\n\n", "ui_wl_title": "✅ **关键词白名单(Whitelist Keywords)**\n\n", @@ -19869,4 +20659,4 @@ "ui_yes": "✅ 是", "unknown_command": "未知命令,请使用 /help。" } -} +} \ No newline at end of file diff --git a/core/infrastructure/telegram/pro_bot_ui.py b/core/infrastructure/telegram/pro_bot_ui.py index 68c49c9e..d15acd75 100644 --- a/core/infrastructure/telegram/pro_bot_ui.py +++ b/core/infrastructure/telegram/pro_bot_ui.py @@ -424,6 +424,8 @@ def cancel_aliases() -> frozenset: from core.infrastructure.telegram.handlers.discovery import DiscoveryHandlers from core.infrastructure.telegram.handlers.forwarding import ForwardingHandlers from core.infrastructure.telegram.replace_bot_ui import ReplaceBotMixin +from core.infrastructure.telegram.replace_maps_bot_ui import ReplaceMapsBotMixin +from core.infrastructure.telegram.prompt_bot_ui import PromptBotMixin from core.infrastructure.telegram.command_router import TextCommandMixin from core.application.command_registry import ARBITRATED_COMMANDS from core.infrastructure.telegram import fsm_arbiter @@ -442,7 +444,8 @@ def cancel_aliases() -> frozenset: ) -class ProBotUI(TextCommandMixin, AutoReplyBotMixin, ReplaceBotMixin, DiscoveryHandlers, +class ProBotUI(TextCommandMixin, AutoReplyBotMixin, ReplaceBotMixin, ReplaceMapsBotMixin, + PromptBotMixin, DiscoveryHandlers, ForwardingHandlers, ForwardManageHandlers, ForwardSelectHandlers): """Renders and routes the entire button-based control panel. @@ -4157,6 +4160,8 @@ def _brand_display(values: List[str]) -> str: words_display = _brand_display(brand_words) gate_label = self._t("ui_smart_btn_brand_gate_on" if brand_cfg.get("require_forward") else "ui_smart_btn_brand_gate_off", lang=lang) + chan_label = self._t("ui_smart_btn_brand_chan_on" if brand_cfg.get("require_channel") + else "ui_smart_btn_brand_chan_off", lang=lang) force_label = self._t("ui_smart_btn_brand_force_on" if brand_cfg.get("force_forward") else "ui_smart_btn_brand_force_off", lang=lang) @@ -4168,6 +4173,8 @@ def _brand_display(values: List[str]) -> str: self._t("ui_smart_mid_channel", lang=lang, mid=mid_display), self._t("ui_smart_brand_block", lang=lang), self._t("ui_smart_brand_state", lang=lang, state=brand_state), + self._t("ui_smart_brand_channel", lang=lang, + state=self._t("ui_brand_on" if brand_cfg.get("require_channel") else "ui_brand_off", lang=lang)), self._t("ui_smart_brand_origins", lang=lang, origins=origins_display), self._t("ui_smart_brand_words", lang=lang, words=words_display), # The description says which logic is in force; the raw `ANY`/`ALL` @@ -4200,8 +4207,10 @@ def _brand_display(values: List[str]) -> str: ], [ (gate_label, f"rsmbr:{rule.id}"), + (chan_label, f"rsmrc:{rule.id}"), (force_label, f"rsmbf:{rule.id}"), ], + [(self._t("ui_smart_btn_replace_maps", lang=lang), f"rmap:open:{rule.id}")], [ (self._t("ui_smart_btn_header", lang=lang), f"rsmh:{rule.id}"), (self._t("ui_smart_btn_del_header", lang=lang), f"rsmhd:{rule.id}"), @@ -4465,6 +4474,9 @@ def _ai_menu(self, cfgs: Optional[list] = None) -> InlineKeyboardMarkup: # bot so a phone-only user has the full surface, not a status line. rows.append([(self._t("ui_btn_ar_assistant"), "ar:menu")]) rows.append([(self._btn("ui_ai_add"), CB["ai_add"])]) + # The prompt library: one list where every template can be renamed, + # reworded, tested, disabled or deleted (prompt_bot_ui.py). + rows.append([(self._btn("ui_btn_prompts"), CB["p_list"])]) rows.append([(self._btn("ui_back"), CB["main"])]) return self._kbd(rows) @@ -4701,6 +4713,10 @@ def _register(self) -> None: self.register_autoreply_handlers() # The unified /replace manager: list, detail, create/edit, tasks, test. self.register_replace_handlers() + # The per-task link & @mention replace-map editor (smart menu entry). + self.register_replace_maps_handlers() + # The global prompt-template manager (AI -> Prompts). + self.register_prompt_handlers() # v29 text-command surface: the commands the registry says no earlier # handler owns. Mounted at group -2 so a command always reaches the @@ -8683,7 +8699,8 @@ async def _render_ai_menu(rule_id: str, draft: Dict[str, Any]) -> Tuple[str, Inl if prompt_id and self._prompts: try: tpl = await self._prompts.get_template(prompt_id) - lines.append(f"📝 {self._t('ui_ai_prompt_label')}: `{tpl.name}` (v{tpl.current_version})" if tpl else f"📝 {self._t('ui_ai_prompt_label')}: `{prompt_id[:8]}`") + off_tag = "" if (tpl is None or bool(getattr(tpl, "enabled", True))) else f" — {self._t('ui_ai_prompt_off_tag')}" + lines.append(f"📝 {self._t('ui_ai_prompt_label')}: `{tpl.name}` (v{tpl.current_version}{off_tag})" if tpl else f"📝 {self._t('ui_ai_prompt_label')}: `{prompt_id[:8]}`") except Exception: lines.append(f"📝 {self._t('ui_ai_prompt_label')}: `{prompt_id[:8]}`") else: @@ -8908,7 +8925,12 @@ async def _cb_rule_ai_pick_prompt(_, cq: CallbackQuery): templates = await self._prompts.list_templates_by_owner(uid) if hasattr(self._prompts, "list_templates_by_owner") else await self._prompts.list_templates() for t in templates: mark = "✅" if t.id == current else ("⭐" if t.is_system else "👤") - rows.append([(f"{mark} {t.name} (v{t.current_version})", f"r_aipset:{t.id}")]) + # A disabled template is selectable but will not steer + # deliveries — the badge says so before the operator + # wonders why the rewrite "does nothing". + off = "" if bool(getattr(t, "enabled", True)) else f" {self._t('ui_ai_prompt_off_tag')}" + rows.append([(f"{mark} {t.name} (v{t.current_version}){off}", f"r_aipset:{t.id}")]) + rows.append([(self._t("ui_ai_btn_open_prompts"), CB["p_list"])]) rows.append([(self._t("ui_back"), f"r_ai:{rule_id}")]) await self._safe_edit(cq, self._t("ui_ai_pick_prompt_title"), reply_markup=self._kbd(rows)) await cq.answer() @@ -9732,6 +9754,35 @@ async def _cb_smart_brand_force_toggle(_, cq: CallbackQuery): text, kbd = self._render_smart_menu(rule) await self._safe_edit(cq, text, reply_markup=kbd) + @b.on_callback_query(filters.regex(r"^rsmrc:(.+)$")) + @self._localized_handler + async def _cb_smart_brand_channel_toggle(_, cq: CallbackQuery): + """The "channel-only hop" gate: normal users keep their own name. + + On: only channel-originated messages (a channel post in A, or a + forward whose origin is a channel) take the branded hop; a + message written by a person is delivered directly, so B still + shows "Forwarded from ". Off: every matched message + takes the hop. + """ + uid = cq.from_user.id if cq.from_user else 0 + raw = cq.data.decode("utf-8") if isinstance(cq.data, (bytes, bytearray)) else str(cq.data or "") + rule_id = raw.split(":", 1)[1] + rule = await self._owned_rule(rule_id, uid) + if not rule: + return await cq.answer(self._t("ui_rule_not_found"), show_alert=True) + cfg = branding_policy.read_config(rule) + enable = not bool(cfg.get("require_channel")) + branding_policy.write_config(rule, {"require_channel": enable}) + await self._save_owned_rule(rule, uid) + await cq.answer(self._t( + "ui_brand_channel_saved", + lang=self._resolve_lang(uid), + state=self._t("ui_brand_on" if enable else "ui_brand_off"), + )) + text, kbd = self._render_smart_menu(rule) + await self._safe_edit(cq, text, reply_markup=kbd) + @b.on_callback_query(filters.regex(r"^rsmbt:(.+)$")) @self._localized_handler async def _cb_smart_brand_triggers_toggle(_, cq: CallbackQuery): @@ -11016,7 +11067,8 @@ async def _step_ai_prompt_sys(self, message: Message, st: "UiState", text: str, if prompt_id and self._prompts: try: tpl = await self._prompts.get_template(prompt_id) - lines.append(f"📝 {self._t('ui_ai_prompt_label')}: `{tpl.name}` (v{tpl.current_version})" if tpl else f"📝 {self._t('ui_ai_prompt_label')}: `{prompt_id[:8]}`") + off_tag = "" if (tpl is None or bool(getattr(tpl, "enabled", True))) else f" — {self._t('ui_ai_prompt_off_tag')}" + lines.append(f"📝 {self._t('ui_ai_prompt_label')}: `{tpl.name}` (v{tpl.current_version}{off_tag})" if tpl else f"📝 {self._t('ui_ai_prompt_label')}: `{prompt_id[:8]}`") except Exception: lines.append(f"📝 {self._t('ui_ai_prompt_label')}: `{prompt_id[:8]}`") else: diff --git a/core/infrastructure/telegram/prompt_bot_ui.py b/core/infrastructure/telegram/prompt_bot_ui.py new file mode 100644 index 00000000..8c45fc01 --- /dev/null +++ b/core/infrastructure/telegram/prompt_bot_ui.py @@ -0,0 +1,592 @@ +"""The global prompt-template manager — the bot face of the AI prompt library. + +What this file is +----------------- +The Telegram face of :class:`core.application.prompt_service.PromptService`. +It renders and routes; every rule it applies ("what a valid prompt is", +"who may touch it", "what disabling means") is that service's, which the +dashboard and the mobile app also reach through gRPC. There is no second +implementation here. + +Until this panel existed the callback verbs were declared in the bot's +callback map with no handlers attached — the per-task prompt *picker* could +create a template, but the library itself (rename, edit wording, delete, +enable/disable, test) was unreachable from a phone. This mixin closes that: +``AI → 🧠 Prompts`` opens one list where every template can be managed. + +The screens +----------- +``pl`` opens a paginated-free list (template counts are small): + + 🧠 Prompt templates — 3 · 🟢 2 + Tap a prompt to manage it. + + 🟢 Professional Clean · shared + 🔴 My Shop Translator + +Every row opens the detail screen, where the whole set of actions lives: +enable/disable, rename, edit the system prompt / user template / description, +test against a real provider, delete (with confirmation). A disabled template +stays in the library — tasks pointing at it are rewritten with their AI +config's own prompt — so disabling is the safe "park it" switch, and +re-enabling restores exactly what was there. + +System prompts (``is_system``) are shared by every account on the +installation: they can be viewed and tested here, but editing, disabling and +deleting are refused — a per-tenant toggle on a shared row would silently +change every other tenant's deliveries. + +Callback data +------------- +``p[: