From 659c470eba542383987aad227b7f918100f131fa Mon Sep 17 00:00:00 2001
From: wangzhibo <wangzhibo@shsening.com>
Date: 星期五, 11 九月 2026 15:09:05 +0800
Subject: [PATCH] 小智终端 5种 场景模式 OK

---
 xiaozhi-esp32-server/main/xiaozhi-server/core/app_runtime/registry.py |  101 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 100 insertions(+), 1 deletions(-)

diff --git a/xiaozhi-esp32-server/main/xiaozhi-server/core/app_runtime/registry.py b/xiaozhi-esp32-server/main/xiaozhi-server/core/app_runtime/registry.py
index 42c4436..22dbcb3 100644
--- a/xiaozhi-esp32-server/main/xiaozhi-server/core/app_runtime/registry.py
+++ b/xiaozhi-esp32-server/main/xiaozhi-server/core/app_runtime/registry.py
@@ -1,4 +1,10 @@
-"""Load the Phase 1 App / Task registry from JSON."""
+"""Load the Phase 1 App / Task registry from JSON.
+
+Registry lives on xiaozhi-server only (bundled apps.json, optional data/apps.json).
+Task voice_enter / voice_exit / enter_prompt live here so new Apps do not need a
+firmware flash. The device only keeps a small offline MultiNet set (mode switch,
+repeater, exit repeater, return to chat) plus NVS pending / last-repeater state.
+"""
 
 import json
 import os
@@ -11,8 +17,14 @@
 logger = setup_logging()
 
 _REGISTRY: Optional[Dict[str, Any]] = None
+_VOICE_INDEX: Optional[list] = None
 
 _BUNDLED_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "apps.json")
+_MENU_PRIO = 0
+_EXIT_PRIO = 1
+_CHAT_ENTER_PRIO = 2
+_ENTER_PRIO = 3
+_RESERVED_PRIO = 4
 
 
 def _load_registry() -> Dict[str, Any]:
@@ -54,6 +66,93 @@
     return _REGISTRY
 
 
+def reset_registry_cache() -> None:
+    """Tests may swap the registry; drop derived indexes."""
+    global _VOICE_INDEX
+    _VOICE_INDEX = None
+
+
+def iter_tasks():
+    apps = get_registry().get("apps") or {}
+    for app_id, app in apps.items():
+        if not isinstance(app, dict):
+            continue
+        for task_id, task in (app.get("tasks") or {}).items():
+            if isinstance(task, dict):
+                yield app_id, app, task_id, task
+
+
+def _phrase_list(raw) -> list:
+    if raw is None:
+        return []
+    if isinstance(raw, str):
+        text = raw.strip()
+        return [text] if text else []
+    phrases = []
+    for item in raw:
+        text = str(item or "").strip()
+        if text:
+            phrases.append(text)
+    return phrases
+
+
+def _build_voice_index() -> list:
+    """Longer phrase first, then menu/exit/chat-enter/enter/reserved."""
+    entries = []
+    registry = get_registry()
+    for phrase in _phrase_list(registry.get("voice_menu_phrases")):
+        entries.append((phrase, ("__menu__", ""), _MENU_PRIO))
+    for app_id, app, task_id, task in iter_tasks():
+        default_task = app.get("default_task") or task_id
+        for phrase in _phrase_list(task.get("voice_exit")):
+            entries.append((phrase, ("chat", "conversation"), _EXIT_PRIO))
+        for phrase in _phrase_list(task.get("voice_enter")):
+            if app_id == "chat":
+                entries.append((phrase, ("chat", default_task or task_id), _CHAT_ENTER_PRIO))
+            else:
+                entries.append((phrase, (app_id, task_id), _ENTER_PRIO))
+    for phrase in _phrase_list(registry.get("reserved_voice_enter")):
+        entries.append((phrase, ("__reserved__", ""), _RESERVED_PRIO))
+    entries.sort(key=lambda item: (-len(item[0]), item[2]))
+    return entries
+
+
+def _voice_index() -> list:
+    global _VOICE_INDEX
+    if _VOICE_INDEX is None:
+        _VOICE_INDEX = _build_voice_index()
+    return _VOICE_INDEX
+
+
+def match_registry_voice(filtered: str) -> Optional[Tuple[str, str]]:
+    """Map stripped ASR text to (app_id, task_id) or ('__menu__'|'__reserved__', '')."""
+    if not filtered:
+        return None
+    for phrase, target, _prio in _voice_index():
+        if phrase and phrase in filtered:
+            return target
+    return None
+
+
+def voice_menu_text() -> str:
+    labels = []
+    chat_hint = "鎴戜滑鏉ヨ亰澶╁惂"
+    for app_id, _app, task_id, task in iter_tasks():
+        enters = _phrase_list(task.get("voice_enter"))
+        if app_id == "chat" and task_id == "conversation" and enters:
+            chat_hint = enters[0]
+        name = str(task.get("menu_name") or "").strip()
+        if name:
+            labels.append(name)
+    if not labels:
+        return f"鎯宠亰澶╋紵涔熷彲浠ヨ{chat_hint}銆�"
+    if len(labels) == 1:
+        body = labels[0]
+    else:
+        body = "銆�".join(labels[:-1]) + f"锛岃繕鏄瘂labels[-1]}"
+    return f"鎯硔body}锛熶篃鍙互璇磠chat_hint}銆�"
+
+
 def default_app_id() -> str:
     return get_registry().get("default_app_id") or "chat"
 

--
Gitblit v1.9.1