feat: integrate production cockpit AI assistant

This commit is contained in:
vee
2026-09-08 17:46:07 +08:00
parent 5e228e12ae
commit 93103c2579
66 changed files with 11046 additions and 374 deletions
+139 -6
View File
@@ -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()