-
Notifications
You must be signed in to change notification settings - Fork 53k
feat(weixin): tag transcribed voice messages and enforce echo on outbound #27235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1001,7 +1001,10 @@ def _extract_text(item_list: List[Dict[str, Any]]) -> str: | |
| if item.get("type") == ITEM_VOICE: | ||
| voice_text = str((item.get("voice_item") or {}).get("text") or "") | ||
| if voice_text: | ||
| return voice_text | ||
| # Prefix so the agent can distinguish auto-transcribed voice | ||
| # messages from typed text (Weixin's callback delivers them as | ||
| # indistinguishable plain strings otherwise). | ||
| return f"[voice:transcribed] {voice_text}" | ||
| return "" | ||
|
|
||
|
|
||
|
|
@@ -1193,6 +1196,14 @@ def __init__(self, config: PlatformConfig): | |
| self._send_session: Optional[aiohttp.ClientSession] = None | ||
| self._poll_task: Optional[asyncio.Task] = None | ||
| self._dedup = MessageDeduplicator(ttl_seconds=MESSAGE_DEDUP_TTL_SECONDS) | ||
| # Voice echo enforcement: cache last voice transcript per chat_id so the | ||
| # outbound send() can guarantee the `> 🎤 你说:"…"` echo prefix even when | ||
| # the LLM forgets to add it. One-shot: consumed on next outbound send. | ||
| self._pending_voice_echo: Dict[str, str] = {} | ||
| _voice_echo_raw = extra.get("voice_echo_enforce") | ||
| if _voice_echo_raw is None: | ||
| _voice_echo_raw = os.getenv("WEIXIN_VOICE_ECHO_ENFORCE") | ||
| self._voice_echo_enforce: bool = _coerce_bool(_voice_echo_raw, default=True) | ||
|
|
||
| self._account_id = str(extra.get("account_id") or os.getenv("WEIXIN_ACCOUNT_ID", "")).strip() | ||
| self._token = str(config.token or extra.get("token") or os.getenv("WEIXIN_TOKEN", "")).strip() | ||
|
|
@@ -1396,6 +1407,12 @@ async def _process_message(self, message: Dict[str, Any]) -> None: | |
| if self._dedup.is_duplicate(content_key): | ||
| logger.debug("[%s] Content-dedup: skipping duplicate message from %s", self.name, sender_id) | ||
| return | ||
| # Voice echo enforcement: stash transcript keyed on the eventual | ||
| # outbound chat_id so send() can guarantee the echo prefix. | ||
| if self._voice_echo_enforce and text.startswith("[voice:transcribed] "): | ||
| transcript = text[len("[voice:transcribed] "):].strip() | ||
| if transcript: | ||
| self._pending_voice_echo[sender_id] = transcript | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is keyed by |
||
|
|
||
| chat_type, effective_chat_id = _guess_chat_type(message, self._account_id) | ||
| if chat_type == "group": | ||
|
|
@@ -1674,6 +1691,24 @@ async def send( | |
| ) -> SendResult: | ||
| if not self._send_session or not self._token: | ||
| return SendResult(success=False, error="Not connected") | ||
| # Voice echo enforcement (mechanical guarantee, not LLM-dependent): | ||
| # if the inbound message for this chat was a voice transcript and the | ||
| # outbound reply does NOT already start with the `> 🎤 你说:"…"` echo, | ||
| # auto-prepend it. Consumed (popped) so it never bleeds into a later | ||
| # turn. Skips media-only sends (empty content after MEDIA: extraction | ||
| # would re-trigger on the same transcript next turn — popping here | ||
| # is one-shot regardless). | ||
| if self._voice_echo_enforce and content and content.strip(): | ||
| transcript = self._pending_voice_echo.pop(chat_id, None) | ||
| if transcript and not content.lstrip().startswith("> 🎤 你说"): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This consumes process-local state on the next generic nonblank |
||
| # Escape any embedded double quotes in the transcript so the | ||
| # blockquote parses cleanly. | ||
| safe = transcript.replace('"', '\\"') | ||
| content = f'> 🎤 你说:"{safe}"\n\n{content}' | ||
| logger.info( | ||
| "[%s] voice-echo enforcer prepended prefix for %s (LLM forgot)", | ||
| self.name, _safe_id(chat_id), | ||
| ) | ||
| context_token = self._token_store.get(self._account_id, chat_id) | ||
| last_message_id: Optional[str] = None | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please do not add a new user-facing environment-variable setting for this behavior.
AGENTS.md:102-107requires non-secret behavioral configuration to live inconfig.yaml; retainplatforms.weixin.extra.voice_echo_enforce, document it, and remove this fallback.