feat: integrate production cockpit AI assistant
This commit is contained in:
@@ -12,14 +12,107 @@ from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
_WHITESPACE = re.compile(r"\s+")
|
||||
_MATCH_FORMATTING = re.compile(r"[\s()()]+")
|
||||
_NON_ASCII_SLUG = re.compile(r"[^a-z0-9]+")
|
||||
_MODE_ID = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$")
|
||||
_MODE_REFERENCE_VERBS = (
|
||||
"切换进入",
|
||||
"切换到",
|
||||
"切换为",
|
||||
"帮我进入",
|
||||
"开始执行",
|
||||
"进入",
|
||||
"启动",
|
||||
"开启",
|
||||
"打开",
|
||||
"启用",
|
||||
"执行",
|
||||
"运行",
|
||||
"调用",
|
||||
"应用",
|
||||
"使用",
|
||||
"恢复",
|
||||
"切换",
|
||||
"选择",
|
||||
"退出",
|
||||
"停止",
|
||||
"关闭",
|
||||
"取消",
|
||||
"结束",
|
||||
"来一个",
|
||||
"来个",
|
||||
)
|
||||
_MODE_REFERENCE_TAILS = (
|
||||
"吧",
|
||||
"呀",
|
||||
"啊",
|
||||
"哦",
|
||||
"呢",
|
||||
"一下",
|
||||
"看看",
|
||||
"试试",
|
||||
",",
|
||||
",",
|
||||
"。",
|
||||
".",
|
||||
"!",
|
||||
"!",
|
||||
"?",
|
||||
"?",
|
||||
";",
|
||||
";",
|
||||
":",
|
||||
":",
|
||||
"然后",
|
||||
"接着",
|
||||
"同时",
|
||||
"并且",
|
||||
)
|
||||
|
||||
|
||||
def _normalize_match_text(value: str) -> str:
|
||||
return _WHITESPACE.sub("", value.casefold())
|
||||
# Parentheses in display names are presentation-only. For example,
|
||||
# ``女王副驾(行驶)`` is naturally spoken as ``女王副驾行驶模式``.
|
||||
# Removing those separators lets the complete name match while the
|
||||
# short-alias boundary guard below still rejects device compounds.
|
||||
return _MATCH_FORMATTING.sub("", value.casefold())
|
||||
|
||||
|
||||
def _mode_candidate_matches(utterance: str, candidate: object) -> bool:
|
||||
"""Match a mode reference without treating device nouns as mode aliases.
|
||||
|
||||
Names that explicitly contain ``模式`` are unambiguous enough for normal
|
||||
substring matching. Short aliases are different: a user-created
|
||||
``阅读模式`` automatically has the alias ``阅读``, but ``打开阅读灯`` is a
|
||||
light command, not a request to run that mode. A short alias therefore
|
||||
requires an invocation verb and a bounded tail, or an explicit ``模式``
|
||||
suffix. This retains natural requests such as ``启动阅读`` and
|
||||
``进入阅读模式`` while failing closed for compound device names.
|
||||
"""
|
||||
|
||||
needle = _normalize_match_text(str(candidate).strip())
|
||||
if not needle:
|
||||
return False
|
||||
if "模式" in needle:
|
||||
return needle in utterance
|
||||
|
||||
start = 0
|
||||
while True:
|
||||
index = utterance.find(needle, start)
|
||||
if index < 0:
|
||||
return False
|
||||
tail = utterance[index + len(needle) :]
|
||||
if tail.startswith("模式"):
|
||||
return True
|
||||
prefix = utterance[:index]
|
||||
tail_is_bounded = not tail or any(
|
||||
tail.startswith(boundary) for boundary in _MODE_REFERENCE_TAILS
|
||||
)
|
||||
if any(
|
||||
prefix.endswith(verb) for verb in _MODE_REFERENCE_VERBS
|
||||
) and tail_is_bounded:
|
||||
return True
|
||||
start = index + 1
|
||||
|
||||
|
||||
def _clean_name(value: str) -> str:
|
||||
@@ -141,6 +234,17 @@ class ModeOperation:
|
||||
"reason": self.reason,
|
||||
}
|
||||
|
||||
def public_dict(self) -> dict[str, Any]:
|
||||
"""Return the executable fields that are safe to expose as UI metadata."""
|
||||
|
||||
return {
|
||||
"domain": self.domain,
|
||||
"capability": self.capability,
|
||||
"target_scope": self.target_scope,
|
||||
"operation": self.operation,
|
||||
"parameters": dict(self.parameters),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ModeTimelinePhase:
|
||||
@@ -397,6 +501,7 @@ class ModeProfile:
|
||||
"icon": self.icon,
|
||||
"domains": sorted({operation.domain for operation in self.operations}),
|
||||
"operation_count": len(self.operations),
|
||||
"operations": [operation.public_dict() for operation in self.operations],
|
||||
"scope": {
|
||||
"common": self.common,
|
||||
"vehicle_ids": list(self.vehicle_ids),
|
||||
@@ -498,8 +603,7 @@ class ModeProfileRegistry:
|
||||
for profile in self._visible_candidates_for_vehicle(vehicle_id):
|
||||
candidates = [profile.id, profile.name, *profile.aliases]
|
||||
if any(
|
||||
candidate
|
||||
and _normalize_match_text(str(candidate)) in haystack
|
||||
candidate and _mode_candidate_matches(haystack, candidate)
|
||||
for candidate in candidates
|
||||
):
|
||||
if profile.id in matched_ids:
|
||||
@@ -740,6 +844,36 @@ class ModeProfileRegistry:
|
||||
if not canonical_updates:
|
||||
raise ValueError("mode profile update must contain at least one operation")
|
||||
|
||||
merged_operations = _merge_operations(profile.operations, canonical_updates)
|
||||
if self.session_user_profiles and not profile.user_defined:
|
||||
# Runtime edits must never rewrite repository-owned presets. Save
|
||||
# a same-id user overlay instead; normal registry ranking makes it
|
||||
# the effective profile, while deleting the overlay reveals the
|
||||
# untouched preset again.
|
||||
updated = self.save_mode(
|
||||
profile.name,
|
||||
merged_operations,
|
||||
mode_id=profile.id,
|
||||
aliases=list(profile.aliases),
|
||||
vehicle_id=(
|
||||
profile.vehicle_ids[0]
|
||||
if len(profile.vehicle_ids) == 1
|
||||
else None
|
||||
),
|
||||
description=profile.description,
|
||||
icon=profile.icon,
|
||||
instruction=profile.instruction,
|
||||
command_flow=profile.command_flow,
|
||||
timeline=profile.timeline,
|
||||
)
|
||||
backup_path = _initial_backup_path(Path(updated.source))
|
||||
if not backup_path.exists():
|
||||
_replace_yaml(
|
||||
backup_path,
|
||||
_mode_profile_document(profile, list(profile.operations)),
|
||||
)
|
||||
return updated
|
||||
|
||||
path = Path(profile.source)
|
||||
if not path.is_file():
|
||||
raise ValueError(f"mode profile source does not exist: {path}")
|
||||
@@ -747,7 +881,6 @@ class ModeProfileRegistry:
|
||||
if not backup_path.exists():
|
||||
backup_path.write_text(path.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
|
||||
merged_operations = _merge_operations(profile.operations, canonical_updates)
|
||||
document = _mode_profile_document(profile, merged_operations)
|
||||
_replace_yaml(path, document)
|
||||
self.reload()
|
||||
|
||||
Reference in New Issue
Block a user