优化前端,优化前端指令,优化自定义控制
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -13,6 +14,7 @@ import yaml
|
||||
|
||||
_WHITESPACE = 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}$")
|
||||
|
||||
|
||||
def _normalize_match_text(value: str) -> str:
|
||||
@@ -28,6 +30,37 @@ def _clean_name(value: str) -> str:
|
||||
return cleaned
|
||||
|
||||
|
||||
def _clean_mode_id(value: str) -> str:
|
||||
cleaned = str(value).strip()
|
||||
if not _MODE_ID.fullmatch(cleaned):
|
||||
raise ValueError(
|
||||
"mode id must start with a lowercase letter or number and contain "
|
||||
"only lowercase letters, numbers, underscores, or hyphens"
|
||||
)
|
||||
return cleaned
|
||||
|
||||
|
||||
def _clean_instruction(value: str) -> str:
|
||||
cleaned = str(value).strip()
|
||||
if len(cleaned) > 1000:
|
||||
raise ValueError("mode instruction must be at most 1000 characters")
|
||||
return cleaned
|
||||
|
||||
|
||||
def _clean_command_flow(value: Any) -> dict[str, Any] | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("mode command_flow must be an object")
|
||||
try:
|
||||
encoded = json.dumps(value, ensure_ascii=False, sort_keys=True)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError("mode command_flow must be JSON serializable") from exc
|
||||
if len(encoded.encode("utf-8")) > 1024 * 1024:
|
||||
raise ValueError("mode command_flow must be at most 1 MiB")
|
||||
return json.loads(encoded)
|
||||
|
||||
|
||||
def _mode_aliases(name: str, aliases: list[str] | None = None) -> list[str]:
|
||||
values = [name, *(aliases or [])]
|
||||
if name.endswith("模式") and len(name) > 2:
|
||||
@@ -304,6 +337,8 @@ class ModeProfile:
|
||||
vehicle_ids: tuple[str, ...] = ()
|
||||
timeline: ModeTimeline | None = None
|
||||
user_defined: bool = False
|
||||
instruction: str = ""
|
||||
command_flow: dict[str, Any] | None = None
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, path: Path, profile_root: Path | None = None) -> "ModeProfile":
|
||||
@@ -338,6 +373,8 @@ class ModeProfile:
|
||||
else None
|
||||
),
|
||||
user_defined=_is_user_profile_path(path, profile_root),
|
||||
instruction=_clean_instruction(document.get("instruction", "")),
|
||||
command_flow=_clean_command_flow(document.get("command_flow")),
|
||||
)
|
||||
|
||||
def public_dict(self) -> dict[str, Any]:
|
||||
@@ -356,6 +393,8 @@ class ModeProfile:
|
||||
"behavior": "cyclic" if self.timeline is not None else "static",
|
||||
"origin": "user" if self.user_defined else "preset",
|
||||
"deletable": self.user_defined,
|
||||
"instruction": self.instruction,
|
||||
"command_flow": self.command_flow,
|
||||
}
|
||||
if self.timeline is not None:
|
||||
result["timeline"] = self.timeline.public_dict()
|
||||
@@ -397,10 +436,19 @@ class ModeProfileRegistry:
|
||||
def public_modes(self, vehicle_id: str | None = None) -> list[dict[str, Any]]:
|
||||
profiles = self._profiles_for_vehicle(vehicle_id)
|
||||
profiles.sort(key=lambda profile: 0 if profile.user_defined else 1)
|
||||
return [
|
||||
profile.public_dict()
|
||||
for profile in profiles
|
||||
]
|
||||
preset_ids = {
|
||||
profile.id
|
||||
for profile in self._visible_candidates_for_vehicle(vehicle_id)
|
||||
if not profile.user_defined
|
||||
}
|
||||
public = []
|
||||
for profile in profiles:
|
||||
item = profile.public_dict()
|
||||
if profile.user_defined and profile.id in preset_ids:
|
||||
item["origin"] = "customized"
|
||||
item["preset_override"] = True
|
||||
public.append(item)
|
||||
return public
|
||||
|
||||
def match_text(
|
||||
self,
|
||||
@@ -442,6 +490,12 @@ class ModeProfileRegistry:
|
||||
mode_id: str | None = None,
|
||||
aliases: list[str] | None = None,
|
||||
vehicle_id: str | None = None,
|
||||
*,
|
||||
description: str | None = None,
|
||||
icon: str = "bookmark",
|
||||
instruction: str = "",
|
||||
command_flow: dict[str, Any] | None = None,
|
||||
timeline: ModeTimeline | None = None,
|
||||
) -> ModeProfile:
|
||||
clean_name = _clean_name(name)
|
||||
canonical_operations = [
|
||||
@@ -452,7 +506,9 @@ class ModeProfileRegistry:
|
||||
]
|
||||
if not canonical_operations:
|
||||
raise ValueError("mode profile must contain at least one operation")
|
||||
profile_id = mode_id or _slugify_mode_id(clean_name)
|
||||
profile_id = _clean_mode_id(mode_id or _slugify_mode_id(clean_name))
|
||||
clean_instruction = _clean_instruction(instruction)
|
||||
clean_command_flow = _clean_command_flow(command_flow)
|
||||
scope = (
|
||||
{"vehicle_ids": [vehicle_id]}
|
||||
if vehicle_id
|
||||
@@ -462,13 +518,19 @@ class ModeProfileRegistry:
|
||||
"id": profile_id,
|
||||
"name": clean_name,
|
||||
"aliases": _mode_aliases(clean_name, aliases),
|
||||
"description": "用户固化的长期模式配置。",
|
||||
"icon": "bookmark",
|
||||
"description": str(description or "用户固化的长期模式配置。")[:500],
|
||||
"icon": str(icon or "bookmark")[:64],
|
||||
"scope": scope,
|
||||
"operations": [
|
||||
operation.to_yaml() for operation in canonical_operations
|
||||
],
|
||||
}
|
||||
if clean_instruction:
|
||||
document["instruction"] = clean_instruction
|
||||
if clean_command_flow is not None:
|
||||
document["command_flow"] = clean_command_flow
|
||||
if timeline is not None:
|
||||
document["timeline"] = timeline.to_yaml()
|
||||
path = (
|
||||
self.user_root / "vehicles" / vehicle_id / "user" / f"{profile_id}.yaml"
|
||||
if vehicle_id
|
||||
@@ -653,6 +715,10 @@ def _mode_profile_document(
|
||||
}
|
||||
if profile.timeline is not None:
|
||||
document["timeline"] = profile.timeline.to_yaml()
|
||||
if profile.instruction:
|
||||
document["instruction"] = profile.instruction
|
||||
if profile.command_flow is not None:
|
||||
document["command_flow"] = profile.command_flow
|
||||
return document
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user