feat: 完善 N7 智能座舱交互与控制链路
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -384,6 +384,10 @@ class ModeProfile:
|
||||
)
|
||||
|
||||
def public_dict(self) -> dict[str, Any]:
|
||||
is_cyclic = self.timeline is not None or any(
|
||||
operation.capability == "seat.start_dynamic_mode"
|
||||
for operation in self.operations
|
||||
)
|
||||
result = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
@@ -396,7 +400,7 @@ class ModeProfile:
|
||||
"common": self.common,
|
||||
"vehicle_ids": list(self.vehicle_ids),
|
||||
},
|
||||
"behavior": "cyclic" if self.timeline is not None else "static",
|
||||
"behavior": "cyclic" if is_cyclic else "static",
|
||||
"origin": "user" if self.user_defined else "preset",
|
||||
"deletable": self.user_defined,
|
||||
"instruction": self.instruction,
|
||||
|
||||
@@ -48,12 +48,13 @@ DEFAULT_DRIVER_NATIVE_SEAT_CAPABILITIES = {
|
||||
"seat.set_heating": "seat.set_driver_heating_native",
|
||||
}
|
||||
EXPLICIT_DRIVER_SEAT_PATTERN = re.compile(
|
||||
r"主驾|驾驶位|司机位|左前(?:的)?(?:座椅|座位)|"
|
||||
r"主驾|驾驶位|驾驶员|司机位|左前(?:的)?(?:座椅|座位)|"
|
||||
r"seat\s+(?:driver|front-left)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
EXPLICIT_NON_DRIVER_SEAT_PATTERN = re.compile(
|
||||
r"副驾|乘客|右前|后排|前排|所有座椅|全部座椅|"
|
||||
r"副驾|乘客|乘员|右前|后排|后座|二排|第二排|中排|第三排|"
|
||||
r"前排|前舱|后舱|左侧|右侧|左边|右边|所有座椅|全部座椅|全车座椅|"
|
||||
r"seat\s+(?:passenger|front-right)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
@@ -277,7 +278,18 @@ class ModelPlanAdapter:
|
||||
"""Normalize omitted model targets to driver-console CH1 defaults."""
|
||||
|
||||
request = deepcopy(raw_request)
|
||||
# Repository-backed mode and speech payloads already carry a validated
|
||||
# structured target. An empty utterance is not authorization to
|
||||
# reinterpret that persisted scope as a driver-console default.
|
||||
if not utterance.strip():
|
||||
return request
|
||||
capability = str(request.get("capability", ""))
|
||||
current_target = str(request.get("target_scope") or "").strip().lower()
|
||||
if current_target not in {"", "unknown", "unspecified", "default"}:
|
||||
# The plan guard has already tied this explicit scope to the
|
||||
# server-owned utterance. Rewriting it afterwards is a TOCTOU
|
||||
# semantic change and can widen a local request to another zone.
|
||||
return request
|
||||
native_seat_capability = DEFAULT_DRIVER_NATIVE_SEAT_CAPABILITIES.get(
|
||||
capability
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ from cognitive_agent.application.chinese_values import (
|
||||
normalize_chinese_unit_values,
|
||||
parse_percentage_value,
|
||||
)
|
||||
from cognitive_agent.application.semantic_ir import resolve_explicit_seat_scopes
|
||||
|
||||
WINDOW_TARGETS = {
|
||||
"全部": "all",
|
||||
@@ -20,7 +21,19 @@ WINDOW_TARGETS = {
|
||||
"四个": "all",
|
||||
"全车": "all",
|
||||
"前排": "front",
|
||||
"前座": "front",
|
||||
"后排": "rear",
|
||||
"后座": "rear",
|
||||
"二排": "rear",
|
||||
"第二排": "rear",
|
||||
"后排左侧": "rear_left",
|
||||
"后座左侧": "rear_left",
|
||||
"二排左侧": "rear_left",
|
||||
"第二排左侧": "rear_left",
|
||||
"后排右侧": "rear_right",
|
||||
"后座右侧": "rear_right",
|
||||
"二排右侧": "rear_right",
|
||||
"第二排右侧": "rear_right",
|
||||
"左前": "front_left",
|
||||
"前左": "front_left",
|
||||
"右前": "front_right",
|
||||
@@ -54,8 +67,17 @@ OUTLET_TARGETS = {
|
||||
LIGHT_TARGETS = {
|
||||
"全部": "all",
|
||||
"所有": "all",
|
||||
"前排": "front",
|
||||
"后排": "rear",
|
||||
"左前": "front_left",
|
||||
"右前": "front_right",
|
||||
"主驾驶": "front_left",
|
||||
"驾驶位": "front_left",
|
||||
"司机位": "front_left",
|
||||
"主驾": "front_left",
|
||||
"副驾驶": "front_right",
|
||||
"乘客位": "front_right",
|
||||
"副驾": "front_right",
|
||||
"中排左旋钮": "middle_left_rotary",
|
||||
"中排右旋钮": "middle_right_rotary",
|
||||
"中排左": "middle_left",
|
||||
@@ -63,6 +85,13 @@ LIGHT_TARGETS = {
|
||||
"左后": "rear_left",
|
||||
"右后": "rear_right",
|
||||
}
|
||||
_EXPLICIT_LOCATION_HINT = re.compile(
|
||||
r"全部|所有|四个|全车|整车|前排|前座|后排|后座|二排|第二排|"
|
||||
r"中排|第三排|主驾|主驾驶|驾驶位|驾驶员|司机位|副驾|副驾驶|"
|
||||
r"乘客位|乘员|左前|前左|右前|前右|左后|后左|右后|后右|"
|
||||
r"左侧|右侧|左边|右边"
|
||||
)
|
||||
_WHOLE_VEHICLE_HINT = re.compile(r"全部|所有|四个|全车|整车")
|
||||
CLAUSE_SEPARATOR = re.compile(r"[,,;;。!?!?\n]+")
|
||||
OUTLET_MODE_DISABLE_PATTERN = re.compile(
|
||||
r"(?:关闭|关掉|停止|取消)(?:一下)?(?:空调)?(?:的)?"
|
||||
@@ -100,6 +129,150 @@ def _feature_context(text: str, feature: str, radius: int = 24) -> str:
|
||||
return f"{prefix}{match.group(0)}{suffix}"
|
||||
|
||||
|
||||
def _allows_vehicle_wide_target(text: str) -> bool:
|
||||
"""Allow a whole-vehicle action only when no narrower scope was stated."""
|
||||
|
||||
return bool(_WHOLE_VEHICLE_HINT.search(text)) or not bool(
|
||||
_EXPLICIT_LOCATION_HINT.search(text)
|
||||
)
|
||||
|
||||
|
||||
_CLIMATE_TEMPERATURE = re.compile(
|
||||
r"(?:空调(?:温度)?|温度)(?:都)?"
|
||||
r"(?:并且|并|然后|同时|再)?"
|
||||
r"(?:调整到|调整为|调整至|调整成|调节到|调节为|调节至|"
|
||||
r"调到|调成|调为|调至|设到|设为|设成|设置到|设置为|设置至|到|为)?"
|
||||
r"(1[89]|2\d|3[0-2])(?:摄氏)?(?:℃|度)?"
|
||||
r"(?![\d.]|小时|分钟|秒|天|档|级)"
|
||||
)
|
||||
_CLIMATE_NUMERIC_REQUEST = re.compile(
|
||||
r"(?:空调(?:温度)?|温度)(?:都)?"
|
||||
r"(?:并且|并|然后|同时|再)?"
|
||||
r"(?:调整到|调整为|调整至|调整成|调节到|调节为|调节至|"
|
||||
r"调到|调成|调为|调至|设到|设为|设成|设置到|设置为|设置至|到|为)?"
|
||||
r"(?P<value>\d+(?:\.\d+)?)"
|
||||
r"(?P<unit>摄氏度|摄氏℃|℃|度|小时|分钟|秒|天|档|级)?"
|
||||
)
|
||||
_UNSUPPORTED_EXACT_HVAC_ZONE = re.compile(
|
||||
r"第三排|三排|前舱|后舱|左前|右前|前左|前右|左后|右后|后左|后右|"
|
||||
r"后排左|后排右|后座左|后座右|二排左|二排右|第二排左|第二排右|"
|
||||
r"左侧|右侧|左边|右边|左右|两侧"
|
||||
)
|
||||
|
||||
|
||||
def _climate_temperature_ambiguities(text: str) -> list[dict[str, Any]]:
|
||||
"""Keep malformed or multiply-valued HVAC numbers from being half-saved."""
|
||||
|
||||
compact = re.sub(r"\s+", "", normalize_chinese_unit_values(text))
|
||||
ambiguities: list[dict[str, Any]] = []
|
||||
for clause in (item for item in CLAUSE_SEPARATOR.split(compact) if item):
|
||||
request = _CLIMATE_NUMERIC_REQUEST.search(clause)
|
||||
if request is None:
|
||||
continue
|
||||
value = request.group("value")
|
||||
unit = request.group("unit") or ""
|
||||
request_tail = clause[request.start() :]
|
||||
degree_values = re.findall(
|
||||
r"(?<!\d)(\d+(?:\.\d+)?)(?:摄氏)?(?:℃|度)",
|
||||
request_tail,
|
||||
)
|
||||
|
||||
if len(degree_values) > 1:
|
||||
ambiguities.append(
|
||||
{
|
||||
"field": "climate.temperature_c",
|
||||
"question": "同一空调温度设置包含多个目标值,请只保留一个温度。",
|
||||
"reason": "native_can_temperature_multiple_values",
|
||||
}
|
||||
)
|
||||
elif "." in value:
|
||||
ambiguities.append(
|
||||
{
|
||||
"field": "climate.temperature_c",
|
||||
"question": "当前空调控制仅支持整数温度,请改为18到32度之间的整数。",
|
||||
"reason": "native_can_temperature_fraction_unsupported",
|
||||
}
|
||||
)
|
||||
elif unit in {"小时", "分钟", "秒", "天", "档", "级"}:
|
||||
ambiguities.append(
|
||||
{
|
||||
"field": "climate.temperature_c",
|
||||
"question": "空调数字的单位不是温度,无法安全生成温度动作,请明确写成例如24度。",
|
||||
"reason": "native_can_temperature_invalid_unit",
|
||||
}
|
||||
)
|
||||
elif not 18 <= int(value) <= 32:
|
||||
ambiguities.append(
|
||||
{
|
||||
"field": "climate.temperature_c",
|
||||
"question": "空调目标温度超出当前支持的18到32度范围。",
|
||||
"reason": "native_can_temperature_out_of_range",
|
||||
}
|
||||
)
|
||||
|
||||
if _UNSUPPORTED_EXACT_HVAC_ZONE.search(clause):
|
||||
ambiguities.append(
|
||||
{
|
||||
"field": "climate.target",
|
||||
"question": "该空调温区不能安全映射,请明确指定主驾、副驾、前排、后排或全车。",
|
||||
"reason": "native_can_temperature_target_unsupported",
|
||||
}
|
||||
)
|
||||
return ambiguities
|
||||
|
||||
|
||||
def _parse_climate_temperature_actions(text: str) -> list[dict[str, Any]]:
|
||||
"""Bind every HVAC temperature value to the target in its own clause."""
|
||||
|
||||
actions: list[dict[str, Any]] = []
|
||||
for clause in (item for item in CLAUSE_SEPARATOR.split(text) if item):
|
||||
matches = list(_CLIMATE_TEMPERATURE.finditer(clause))
|
||||
explicit_degree_values = re.findall(
|
||||
r"(?<!\d)(\d+(?:\.\d+)?)(?:摄氏)?(?:℃|度)",
|
||||
clause,
|
||||
)
|
||||
# Multiple values without a clause boundary cannot be paired safely.
|
||||
if len(matches) != 1 or len(explicit_degree_values) > 1:
|
||||
continue
|
||||
temperature = int(matches[0][1])
|
||||
if _UNSUPPORTED_EXACT_HVAC_ZONE.search(clause):
|
||||
targets: tuple[str, ...] = ()
|
||||
elif re.search(r"全车|整车", clause):
|
||||
targets = ("driver", "passenger", "rear")
|
||||
elif (
|
||||
re.search(r"前排|前座|主副驾", clause)
|
||||
or ("主驾" in clause and "副驾" in clause)
|
||||
):
|
||||
targets = ("driver", "passenger")
|
||||
elif re.search(r"副驾|副驾驶|乘客|乘员", clause):
|
||||
targets = ("passenger",)
|
||||
elif re.search(r"后排|后座|二排|第二排", clause):
|
||||
targets = ("rear",)
|
||||
elif re.search(r"主驾|主驾驶|驾驶位|驾驶员|司机位", clause):
|
||||
targets = ("driver",)
|
||||
elif _EXPLICIT_LOCATION_HINT.search(clause):
|
||||
targets = ()
|
||||
else:
|
||||
targets = ("driver",)
|
||||
explicit_target = bool(_EXPLICIT_LOCATION_HINT.search(clause))
|
||||
reason = (
|
||||
"用户指定了空调温区和温度。"
|
||||
if explicit_target
|
||||
else "用户未指定空调温区,按驾驶台默认选择主驾温区。"
|
||||
)
|
||||
actions.extend(
|
||||
_action(
|
||||
"climate",
|
||||
"climate.set_temperature",
|
||||
target,
|
||||
{"temperature_c": temperature},
|
||||
reason,
|
||||
)
|
||||
for target in targets
|
||||
)
|
||||
return actions
|
||||
|
||||
|
||||
def _grade_alias(text: str, *, include_numeric: bool = True) -> str | None:
|
||||
"""Normalize common low/middle/high wording without guessing a value."""
|
||||
|
||||
@@ -163,32 +336,44 @@ CH1_DEFAULT_TARGETS = {
|
||||
|
||||
_EXPLICIT_TARGET_PATTERNS = {
|
||||
"window": re.compile(
|
||||
r"(?:全部|所有|四个|全车|前排|后排|左前|前左|右前|前右|左后|后左|右后|后右|"
|
||||
r"主驾驶|驾驶位|司机位|主驾|副驾驶|乘客位|副驾)"
|
||||
r"(?:全部|所有|四个|全车|前排|前座|后排|后座|二排|第二排|"
|
||||
r"左前|前左|右前|前右|左后|后左|右后|后右|左侧|右侧|"
|
||||
r"主驾驶|驾驶位|驾驶员|司机位|主驾|副驾驶|乘客位|乘员|副驾)"
|
||||
r"(?:的)?(?:车窗|窗)|window\s+(?:all|front|rear|front-left|front-right|rear-left|rear-right)",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
"climate": re.compile(
|
||||
r"(?:主驾|驾驶位|司机位|副驾|乘客|后排|前排|全车)(?:的)?.{0,4}(?:空调|温度)|"
|
||||
r"(?:空调|温度).{0,4}(?:主驾|驾驶位|司机位|副驾|乘客|后排|前排|全车)|"
|
||||
r"(?:主驾|驾驶位|驾驶员|司机位|副驾|乘客|乘员|后排|后座|二排|第二排|"
|
||||
r"前排|前座|全车|整车|左前|右前|左后|右后|前左|前右|后左|后右|"
|
||||
r"后排左|后排右|第三排|三排|前舱|后舱|左侧|右侧|左边|右边)"
|
||||
r"(?:的)?.{0,4}(?:空调|温度)|"
|
||||
r"(?:空调|温度).{0,4}(?:主驾|驾驶位|驾驶员|司机位|副驾|乘客|乘员|"
|
||||
r"后排|后座|二排|第二排|前排|前座|全车|整车|左前|右前|"
|
||||
r"左后|右后|前左|前右|后左|后右|后排左|后排右|第三排|三排|"
|
||||
r"前舱|后舱|左侧|右侧|左边|右边)|"
|
||||
r"ac\s+temp\s+(?:driver|passenger|rear)",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
"outlet": re.compile(
|
||||
r"(?:主驾左|主驾右|副驾左|副驾右)(?:的)?风口|"
|
||||
r"(?:主驾|主驾驶|驾驶位|驾驶员|司机位|"
|
||||
r"副驾|副驾驶|乘客位|乘员|前排|后排|后座|二排|"
|
||||
r"第二排|第三排|左侧|右侧)(?:左|右)?(?:的)?(?:出)?风口|"
|
||||
r"outlet\s+(?:driver-left|driver-right|passenger-left|passenger-right)",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
"seat": re.compile(
|
||||
r"主驾|主驾驶(?:座|座椅|席)?|驾驶位|驾驶席|驾驶座(?:椅)?|"
|
||||
r"主驾|主驾驶(?:座|座椅|席)?|驾驶位|驾驶员|驾驶席|驾驶座(?:椅)?|"
|
||||
r"司机位|司机座(?:椅)?|左前(?:座|座椅|座位)?|"
|
||||
r"副驾(?:驶|位)?|乘客(?:位|座|座椅|座位)?|右前(?:座|座椅|座位)?|"
|
||||
r"前排|后排|左后|右后|所有座椅|全部座椅|全车座椅|"
|
||||
r"副驾(?:驾驶|位)?|乘客(?:位|座|座椅|座位)?|乘员(?:位|座|座椅|座位)?|"
|
||||
r"右前(?:座|座椅|座位)?|前排|后排|后座|二排|第二排|中排|第三排|"
|
||||
r"左后|右后|所有座椅|全部座椅|全车座椅|"
|
||||
r"seat\s+(?:driver|front-left|passenger|front-right)",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
"interior_light": re.compile(
|
||||
r"(?:全部|所有|左前|右前|中排左|中排右|左后|右后)(?:的)?阅读灯|"
|
||||
r"(?:全部|所有|前排|后排|后座|二排|第二排|主驾|主驾驶|驾驶位|"
|
||||
r"驾驶员|司机位|副驾|副驾驶|乘客位|乘员|左前|右前|中排左|"
|
||||
r"中排右|左后|右后|左侧|右侧)(?:的)?阅读灯|"
|
||||
r"light\s+reading\s+(?:all|front-left|front-right)",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
@@ -273,6 +458,10 @@ def _window_targets(text: str) -> tuple[list[str], bool]:
|
||||
return ([target for target in explicit if target not in exclusions], True)
|
||||
if broad:
|
||||
return ([broad], True)
|
||||
if _EXPLICIT_LOCATION_HINT.search(text):
|
||||
# An explicit but unknown or unsupported location must never inherit
|
||||
# the driver-window default.
|
||||
return ([], True)
|
||||
return (["front_left"], False)
|
||||
|
||||
|
||||
@@ -304,6 +493,19 @@ def _looks_like_seat_request(text: str) -> bool:
|
||||
|
||||
def _implicit_driver_seat_request(text: str) -> bool:
|
||||
return _looks_like_seat_request(text) and not _EXPLICIT_TARGET_PATTERNS["seat"].search(text)
|
||||
|
||||
|
||||
def _seat_target_scope(text: str) -> str | None:
|
||||
"""Resolve seat words without ever widening an explicit scope."""
|
||||
|
||||
scopes = resolve_explicit_seat_scopes(text)
|
||||
if scopes == {"driver"}:
|
||||
return "driver"
|
||||
if scopes == {"passenger"}:
|
||||
return "passenger"
|
||||
return "unsupported" if scopes else None
|
||||
|
||||
|
||||
def _action(
|
||||
domain: str,
|
||||
capability: str,
|
||||
@@ -565,13 +767,10 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
|
||||
clause = f"{clause}{continuation}"
|
||||
continuation_index += 1
|
||||
index = continuation_index - 1
|
||||
if re.search(
|
||||
r"主驾|主驾驶(?:座|座椅|席)?|驾驶位|驾驶席|"
|
||||
r"驾驶座(?:椅)?|司机位|司机座(?:椅)?|左前座椅",
|
||||
clause,
|
||||
):
|
||||
scope = _seat_target_scope(clause)
|
||||
if scope == "driver":
|
||||
inherited_driver_scope = True
|
||||
elif re.search(r"副驾|副驾驶|乘客|右前", clause):
|
||||
elif scope is not None:
|
||||
inherited_driver_scope = False
|
||||
elif inherited_driver_scope:
|
||||
clause = f"主驾座椅{clause}"
|
||||
@@ -592,21 +791,10 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
|
||||
index += 1
|
||||
return combined_actions
|
||||
|
||||
if re.search(
|
||||
r"副驾(?:驶|位)?|乘客(?:位|座|座椅|座位)?|右前(?:座|座椅|座位)?|"
|
||||
r"前排|后排|左后|右后|所有座椅|全部座椅|全车座椅|"
|
||||
r"seat\s+(?:passenger|front-right)",
|
||||
compact,
|
||||
re.IGNORECASE,
|
||||
):
|
||||
seat_scope = _seat_target_scope(compact)
|
||||
if seat_scope in {"passenger", "unsupported"}:
|
||||
return []
|
||||
explicit_driver = bool(
|
||||
re.search(
|
||||
r"主驾|主驾驶(?:座|座椅|席)?|驾驶位|驾驶席|"
|
||||
r"驾驶座(?:椅)?|司机位|司机座(?:椅)?|左前座椅",
|
||||
compact,
|
||||
)
|
||||
)
|
||||
explicit_driver = seat_scope == "driver"
|
||||
if not explicit_driver and not _looks_like_seat_request(compact):
|
||||
return []
|
||||
default_reason = "用户未指定座椅目标,按驾驶台 CH1 安全默认选择主驾座椅。"
|
||||
@@ -625,13 +813,20 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
|
||||
if memory is not None:
|
||||
slot_token = memory.group("after") or memory.group("before")
|
||||
slot = {"一": 1, "二": 2, "三": 3}.get(slot_token, slot_token)
|
||||
memory_capability = (
|
||||
"seat.recall_driver_memory_native"
|
||||
if "召回" in compact
|
||||
else "seat.set_driver_position_native"
|
||||
)
|
||||
actions.append(_action(
|
||||
"seat",
|
||||
"seat.set_driver_position_native",
|
||||
memory_capability,
|
||||
"driver",
|
||||
{"slot": int(slot)},
|
||||
(
|
||||
"用户明确指定了 CH1 主驾座椅记忆位置。"
|
||||
"用户明确要求召回 CH1 主驾座椅记忆位置。"
|
||||
if memory_capability == "seat.recall_driver_memory_native"
|
||||
else "用户明确指定了 CH1 主驾座椅记忆位置。"
|
||||
if explicit_driver
|
||||
else default_reason
|
||||
),
|
||||
@@ -977,61 +1172,69 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
|
||||
climate_text = _scoped_text(
|
||||
compact,
|
||||
r"空调|温度|风量|风速|内循环|外循环|循环模式|吹面|吹脸|吹脚|除霜|(?:AC|A/C)",
|
||||
r"空调|压缩机|温度|风量|风速|内循环|外循环|循环模式|吹面|吹脸|吹脚|除霜|(?:AC|A/C)",
|
||||
)
|
||||
|
||||
if (
|
||||
"空调" in climate_text
|
||||
or "压缩机" in climate_text
|
||||
or re.search(r"(?:AC|A/C)", text, re.IGNORECASE)
|
||||
or re.search(
|
||||
r"温度|风量|风速|内循环|外循环|循环模式|吹面|吹脸|吹脚|除霜",
|
||||
climate_text,
|
||||
)
|
||||
):
|
||||
if re.search(r"(?:打开|开启|启动)空调", climate_text):
|
||||
actions.append(_action("climate", "climate.set_power", "vehicle", {"state": "on"}, "用户要求打开空调。"))
|
||||
elif re.search(r"(?:关闭|关掉|关上)空调", climate_text):
|
||||
actions.append(_action("climate", "climate.set_power", "vehicle", {"state": "off"}, "用户要求关闭空调。"))
|
||||
|
||||
temperature = re.search(
|
||||
r"(?:空调(?:温度)?|温度)(?:都)?"
|
||||
r"(?:调整到|调整为|调到|调成|调为|设到|设为|设置为|调至|到|为)?"
|
||||
r"(1[89]|2\d|3[0-2])(?:摄氏)?(?:℃|度)?(?![\d.])",
|
||||
compressor_on = re.search(
|
||||
r"(?:(?:打开|开启|启动)(?:空调)?(?:的)?(?:压缩机|A/?C)|"
|
||||
r"(?:空调)?(?:的)?(?:压缩机|A/?C)(?:设为|设置为|调为|切换为)?"
|
||||
r"(?:打开|开启|启动|开|on)(?:状态)?)",
|
||||
climate_text,
|
||||
re.IGNORECASE,
|
||||
)
|
||||
if temperature:
|
||||
if (
|
||||
re.search(r"全车|整车|前排|主副驾", climate_text)
|
||||
or ("主驾" in climate_text and "副驾" in climate_text)
|
||||
):
|
||||
temperature_targets = ("driver", "passenger")
|
||||
elif re.search(r"副驾|副驾驶|乘客", climate_text):
|
||||
temperature_targets = ("passenger",)
|
||||
elif "后排" in climate_text:
|
||||
temperature_targets = ("rear",)
|
||||
else:
|
||||
temperature_targets = ("driver",)
|
||||
explicit_temperature_target = bool(
|
||||
re.search(
|
||||
r"主驾|副驾|副驾驶|乘客|后排|前排|全车|整车|主副驾",
|
||||
climate_text,
|
||||
)
|
||||
)
|
||||
reason = (
|
||||
"用户指定了空调温区和温度。"
|
||||
if explicit_temperature_target
|
||||
else "用户未指定空调温区,按驾驶台默认选择主驾温区。"
|
||||
)
|
||||
actions.extend(
|
||||
compressor_off = re.search(
|
||||
r"(?:(?:关闭|关掉|关上|停止)(?:空调)?(?:的)?(?:压缩机|A/?C)|"
|
||||
r"(?:空调)?(?:的)?(?:压缩机|A/?C)(?:设为|设置为|调为|切换为)?"
|
||||
r"(?:关闭|关掉|关上|停止|关|off)(?:状态)?)",
|
||||
climate_text,
|
||||
re.IGNORECASE,
|
||||
)
|
||||
compressor_text = _feature_context(
|
||||
climate_text,
|
||||
r"压缩机|A/?C",
|
||||
)
|
||||
compressor_vehicle_wide = _allows_vehicle_wide_target(compressor_text)
|
||||
if compressor_on and not compressor_off and compressor_vehicle_wide:
|
||||
actions.append(
|
||||
_action(
|
||||
"climate",
|
||||
"climate.set_temperature",
|
||||
target,
|
||||
{"temperature_c": int(temperature[1])},
|
||||
reason,
|
||||
"climate.set_ac",
|
||||
"vehicle",
|
||||
{"state": "on"},
|
||||
"用户要求开启空调压缩机。",
|
||||
)
|
||||
for target in temperature_targets
|
||||
)
|
||||
elif compressor_off and not compressor_on and compressor_vehicle_wide:
|
||||
actions.append(
|
||||
_action(
|
||||
"climate",
|
||||
"climate.set_ac",
|
||||
"vehicle",
|
||||
{"state": "off"},
|
||||
"用户要求关闭空调压缩机。",
|
||||
)
|
||||
)
|
||||
|
||||
power_text = _feature_context(climate_text, r"空调(?!的?压缩机)")
|
||||
if re.search(
|
||||
r"(?:打开|开启|启动)空调(?!的?压缩机)", climate_text
|
||||
) and _allows_vehicle_wide_target(power_text):
|
||||
actions.append(_action("climate", "climate.set_power", "vehicle", {"state": "on"}, "用户要求打开空调。"))
|
||||
elif re.search(
|
||||
r"(?:关闭|关掉|关上)空调(?!的?压缩机)", climate_text
|
||||
) and _allows_vehicle_wide_target(power_text):
|
||||
actions.append(_action("climate", "climate.set_power", "vehicle", {"state": "off"}, "用户要求关闭空调。"))
|
||||
|
||||
actions.extend(_parse_climate_temperature_actions(climate_text))
|
||||
|
||||
fan = re.search(
|
||||
r"(?:风量|风速)(?:调整到|调整为|调到|调为|设为|设置为|到)?"
|
||||
@@ -1044,17 +1247,20 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
fan_level = {"low": 1, "medium": 4, "high": 7}.get(
|
||||
fan_grade or ""
|
||||
)
|
||||
if fan_level is not None:
|
||||
fan_text = _feature_context(climate_text, r"风量|风速")
|
||||
if fan_level is not None and _allows_vehicle_wide_target(fan_text):
|
||||
actions.append(_action("climate", "climate.set_fan_level", "vehicle", {"level": fan_level}, "用户指定了空调风量。"))
|
||||
|
||||
if re.search(r"自动(?:控制|模式)", climate_text):
|
||||
control_mode_text = _feature_context(climate_text, r"自动(?:控制|模式)|手动(?:控制|模式)")
|
||||
if re.search(r"自动(?:控制|模式)", climate_text) and _allows_vehicle_wide_target(control_mode_text):
|
||||
actions.append(_action("climate", "climate.set_control_mode", "vehicle", {"mode": "auto"}, "用户指定空调自动控制。"))
|
||||
elif re.search(r"手动(?:控制|模式)", climate_text):
|
||||
elif re.search(r"手动(?:控制|模式)", climate_text) and _allows_vehicle_wide_target(control_mode_text):
|
||||
actions.append(_action("climate", "climate.set_control_mode", "vehicle", {"mode": "manual"}, "用户指定空调手动控制。"))
|
||||
|
||||
if re.search(r"内循环", climate_text):
|
||||
cycle_text = _feature_context(climate_text, r"内循环|外循环")
|
||||
if re.search(r"内循环", climate_text) and _allows_vehicle_wide_target(cycle_text):
|
||||
actions.append(_action("climate", "climate.set_cycle_mode", "vehicle", {"mode": "internal"}, "用户指定空调内循环。"))
|
||||
elif re.search(r"外循环", climate_text):
|
||||
elif re.search(r"外循环", climate_text) and _allows_vehicle_wide_target(cycle_text):
|
||||
actions.append(_action("climate", "climate.set_cycle_mode", "vehicle", {"mode": "external"}, "用户指定空调外循环。"))
|
||||
|
||||
# A phrase such as “打开副驾左吹面出风口” controls one motorized
|
||||
@@ -1086,7 +1292,12 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
if has_feet
|
||||
else None
|
||||
)
|
||||
if outlet_mode:
|
||||
outlet_mode_text = _feature_context(
|
||||
climate_mode_text,
|
||||
r"吹面|吹脸|吹脚|除霜",
|
||||
radius=48,
|
||||
)
|
||||
if outlet_mode and _allows_vehicle_wide_target(outlet_mode_text):
|
||||
actions.append(
|
||||
_action(
|
||||
"climate",
|
||||
@@ -1102,9 +1313,9 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
door_text = _scoped_text(compact, r"门锁|车门|后门|锁车|解锁车辆")
|
||||
rear_lock_request = bool(
|
||||
re.search(
|
||||
r"(?:后门|后排车门|后排门锁|二排车门|二排门锁)", door_text
|
||||
r"(?:后门|后排车门|后座车门|后排门锁|二排车门|二排门锁)", door_text
|
||||
)
|
||||
)
|
||||
) and not bool(re.search(r"左后|右后|后排左|后排右|后座左|后座右", door_text))
|
||||
if rear_lock_request:
|
||||
state = (
|
||||
"unlock"
|
||||
@@ -1124,7 +1335,7 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
door_text,
|
||||
)
|
||||
)
|
||||
if vehicle_lock_request:
|
||||
if vehicle_lock_request and _allows_vehicle_wide_target(door_text):
|
||||
state = (
|
||||
"unlock"
|
||||
if re.search(r"解锁|开锁", door_text)
|
||||
@@ -1136,7 +1347,7 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
actions.append(_action("door", "door.set_vehicle_lock", "vehicle", {"state": state}, "用户指定了整车门锁状态。"))
|
||||
|
||||
shade_text = _scoped_text(compact, r"遮阳帘|天幕帘")
|
||||
if shade_text:
|
||||
if shade_text and _allows_vehicle_wide_target(shade_text):
|
||||
position = re.search(
|
||||
r"(?:位置|开度|调整到|调到|设为|设置为|开到|打开(?:到)?|开启(?:到)?)(\d{1,3})%",
|
||||
shade_text,
|
||||
@@ -1158,13 +1369,18 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
for label, capability in (("室内灯", "interior_light.set_indoor_power"), ("车内灯", "interior_light.set_indoor_power")):
|
||||
if label in indoor_text:
|
||||
state = "off" if re.search(r"关闭|关掉|关上", indoor_text) else "on" if re.search(r"打开|开启", indoor_text) else None
|
||||
if state:
|
||||
if state and _allows_vehicle_wide_target(indoor_text):
|
||||
actions.append(_action("interior_light", capability, "vehicle", {"state": state}, "用户指定了车内灯状态。"))
|
||||
break
|
||||
|
||||
reading_text = _scoped_text(compact, r"阅读灯")
|
||||
if reading_text:
|
||||
target = next((value for label, value in LIGHT_TARGETS.items() if label in reading_text), "all")
|
||||
target = next(
|
||||
(value for label, value in LIGHT_TARGETS.items() if label in reading_text),
|
||||
None,
|
||||
)
|
||||
if target is None:
|
||||
target = "unsupported" if _EXPLICIT_LOCATION_HINT.search(reading_text) else "all"
|
||||
state = "off" if re.search(r"关闭|关掉|关上", reading_text) else "on" if re.search(r"打开|开启", reading_text) else None
|
||||
if state and target == "all":
|
||||
actions.append(_action("interior_light", "interior_light.set_reading_power", target, {"state": state}, "用户指定了阅读灯区域和状态。"))
|
||||
@@ -1174,7 +1390,7 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
r"氛围灯",
|
||||
r"^(?:亮度|强度)(?:调整|调节|调|设置|设)?",
|
||||
)
|
||||
if ambient_text:
|
||||
if ambient_text and _allows_vehicle_wide_target(ambient_text):
|
||||
state = (
|
||||
"off"
|
||||
if re.search(r"关闭|关掉|熄灭", ambient_text)
|
||||
@@ -1218,6 +1434,49 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
if brightness_level is not None:
|
||||
actions.append(_action("ambient_light", "ambient_light.set_brightness_level", "vehicle", {"brightness_level": brightness_level}, "用户指定了已验证的氛围灯亮度档位。"))
|
||||
|
||||
# “全部灯光关闭” is a common scene-building phrase. It names the
|
||||
# supported cabin-light group rather than one individual lamp, so expand
|
||||
# only the explicit all+off form into the three verified lighting
|
||||
# capabilities. Other generic “灯光” requests stay unresolved instead of
|
||||
# being widened to exterior lighting or an arbitrary single lamp.
|
||||
generic_light_clauses = [
|
||||
clause
|
||||
for clause in CLAUSE_SEPARATOR.split(compact)
|
||||
if "灯光" in clause
|
||||
and not re.search(r"室内灯|车内灯|阅读灯|氛围灯", clause)
|
||||
]
|
||||
for light_clause in generic_light_clauses:
|
||||
if not (
|
||||
re.search(r"全部|所有|全车", light_clause)
|
||||
and re.search(r"关闭|关掉|关上|熄灭", light_clause)
|
||||
):
|
||||
continue
|
||||
actions.extend(
|
||||
(
|
||||
_action(
|
||||
"interior_light",
|
||||
"interior_light.set_indoor_power",
|
||||
"vehicle",
|
||||
{"state": "off"},
|
||||
"用户明确要求关闭全部受支持的座舱灯光。",
|
||||
),
|
||||
_action(
|
||||
"interior_light",
|
||||
"interior_light.set_reading_power",
|
||||
"all",
|
||||
{"state": "off"},
|
||||
"用户明确要求关闭全部受支持的座舱灯光。",
|
||||
),
|
||||
_action(
|
||||
"ambient_light",
|
||||
"ambient_light.set_power",
|
||||
"vehicle",
|
||||
{"state": "off"},
|
||||
"用户明确要求关闭全部受支持的座舱灯光。",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
outlet_text = _scoped_text(compact, r"风口")
|
||||
outlet_target = next(
|
||||
(
|
||||
@@ -1269,7 +1528,12 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
"用户指定了已标定风口的双轴原始坐标。",
|
||||
)
|
||||
)
|
||||
elif outlet_text and outlet_position and all(int(value) <= 100 for value in outlet_position.groups()):
|
||||
elif (
|
||||
outlet_text
|
||||
and outlet_position
|
||||
and all(int(value) <= 100 for value in outlet_position.groups())
|
||||
and (outlet_target is not None or not _EXPLICIT_LOCATION_HINT.search(outlet_text))
|
||||
):
|
||||
target = outlet_target or "driver_left"
|
||||
reason = (
|
||||
"用户指定了已标定风口的双轴百分比位置。"
|
||||
@@ -1432,6 +1696,7 @@ def merge_native_can_intent(
|
||||
|
||||
relative_actions = list(relative_actions or [])
|
||||
relative_ambiguities = list(relative_ambiguities or [])
|
||||
relative_ambiguities.extend(_climate_temperature_ambiguities(utterance))
|
||||
relative_claimed_controls = set(relative_claimed_controls or set())
|
||||
|
||||
def is_claimed(candidate: dict[str, Any]) -> bool:
|
||||
|
||||
@@ -34,10 +34,19 @@ CHINESE_DIGITS = {
|
||||
"六": 6, "七": 7, "八": 8, "九": 9, "十": 10,
|
||||
}
|
||||
WINDOW_TARGET_LABELS = {
|
||||
"后排左侧": ("rear_left",), "后座左侧": ("rear_left",),
|
||||
"二排左侧": ("rear_left",), "第二排左侧": ("rear_left",),
|
||||
"后排右侧": ("rear_right",), "后座右侧": ("rear_right",),
|
||||
"二排右侧": ("rear_right",), "第二排右侧": ("rear_right",),
|
||||
"左前": ("front_left",), "前左": ("front_left",),
|
||||
"右前": ("front_right",), "前右": ("front_right",),
|
||||
"左后": ("rear_left",), "后左": ("rear_left",),
|
||||
"右后": ("rear_right",), "后右": ("rear_right",),
|
||||
"主驾驶": ("front_left",), "驾驶位": ("front_left",),
|
||||
"驾驶员": ("front_left",), "司机位": ("front_left",),
|
||||
"主驾": ("front_left",), "副驾驶": ("front_right",),
|
||||
"乘客位": ("front_right",), "乘员": ("front_right",),
|
||||
"副驾": ("front_right",),
|
||||
"前排": ("front_left", "front_right"),
|
||||
"后排": ("rear_left", "rear_right"),
|
||||
"全部": ("front_left", "front_right", "rear_left", "rear_right"),
|
||||
@@ -45,6 +54,16 @@ WINDOW_TARGET_LABELS = {
|
||||
"四个": ("front_left", "front_right", "rear_left", "rear_right"),
|
||||
"全车": ("front_left", "front_right", "rear_left", "rear_right"),
|
||||
}
|
||||
NARROW_LOCATION = re.compile(
|
||||
r"主驾|主驾驶|驾驶位|驾驶员|司机位|副驾|副驾驶|乘客位|乘员|"
|
||||
r"前排|前座|后排|后座|二排|第二排|中排|第三排|三排|前舱|后舱|"
|
||||
r"左前|前左|右前|前右|左后|后左|右后|后右|左侧|右侧|左边|右边"
|
||||
)
|
||||
UNSUPPORTED_SINGLE_HVAC_ZONE = re.compile(
|
||||
r"第三排|三排|前舱|后舱|左前|前左|右前|前右|左后|后左|右后|后右|"
|
||||
r"后排左|后排右|后座左|后座右|二排左|二排右|第二排左|第二排右|"
|
||||
r"左侧|右侧|左边|右边"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -296,14 +315,29 @@ class NativeCanRelativeResolver:
|
||||
if operation is None:
|
||||
return
|
||||
direction, match = operation
|
||||
target = "passenger" if re.search(r"副驾|乘客位", clause) else "driver"
|
||||
if UNSUPPORTED_SINGLE_HVAC_ZONE.search(clause) or re.search(r"前排|前座", clause):
|
||||
self._ambiguity(
|
||||
resolution,
|
||||
field_name="climate.target",
|
||||
question="该空调温区没有可保持原目标的相对控制绑定,请指定主驾、副驾或后排。",
|
||||
reason="native_can_relative_target_unsupported",
|
||||
)
|
||||
return
|
||||
target = (
|
||||
"passenger"
|
||||
if re.search(r"副驾|副驾驶|乘客位|乘员", clause)
|
||||
else "rear"
|
||||
if re.search(r"后排|后座|二排|第二排", clause)
|
||||
else "driver"
|
||||
)
|
||||
self._claim(resolution, "climate.set_temperature", target)
|
||||
zone = state.get("climate", {}).get("zones", {}).get(target, {})
|
||||
target_label = {"passenger": "副驾", "rear": "后排"}.get(target, "主驾")
|
||||
current = self._value(
|
||||
resolution,
|
||||
{"value": zone.get("temperature_c"), "feedback_source": state.get("climate", {}).get("feedback_source")},
|
||||
field_name=f"climate.{target}_temperature_c",
|
||||
label=f"{'副驾' if target == 'passenger' else '主驾'}温度",
|
||||
label=f"{target_label}温度",
|
||||
require_physical=self._require_physical(state),
|
||||
)
|
||||
if current is None:
|
||||
@@ -319,6 +353,14 @@ class NativeCanRelativeResolver:
|
||||
operation = self._operation(clause, GENERIC_INCREASE, GENERIC_DECREASE)
|
||||
if operation is None:
|
||||
return
|
||||
if NARROW_LOCATION.search(clause):
|
||||
self._ambiguity(
|
||||
resolution,
|
||||
field_name="climate.target",
|
||||
question="风量物理控制仅支持全车目标,不会把局部温区请求扩大到全车。",
|
||||
reason="native_can_relative_target_unsupported",
|
||||
)
|
||||
return
|
||||
direction, match = operation
|
||||
self._claim(resolution, "climate.set_fan_level", "vehicle")
|
||||
climate = state.get("climate", {})
|
||||
@@ -336,6 +378,14 @@ class NativeCanRelativeResolver:
|
||||
operation = self._operation(clause, AMBIENT_INCREASE, AMBIENT_DECREASE)
|
||||
if operation is None:
|
||||
return
|
||||
if NARROW_LOCATION.search(clause):
|
||||
self._ambiguity(
|
||||
resolution,
|
||||
field_name="ambient_light.target",
|
||||
question="氛围灯亮度物理控制仅支持全车目标,不会扩大局部请求。",
|
||||
reason="native_can_relative_target_unsupported",
|
||||
)
|
||||
return
|
||||
direction, match = operation
|
||||
capability = "ambient_light.set_brightness_level"
|
||||
self._claim(resolution, capability, "vehicle")
|
||||
@@ -358,6 +408,14 @@ class NativeCanRelativeResolver:
|
||||
operation = self._operation(clause, OPENING_INCREASE, OPENING_DECREASE)
|
||||
if operation is None:
|
||||
return
|
||||
if NARROW_LOCATION.search(clause):
|
||||
self._ambiguity(
|
||||
resolution,
|
||||
field_name="roof.target",
|
||||
question="遮阳帘物理控制仅支持整帘目标,不会扩大局部请求。",
|
||||
reason="native_can_relative_target_unsupported",
|
||||
)
|
||||
return
|
||||
direction, match = operation
|
||||
self._claim(resolution, "roof.set_shade_position", "vehicle")
|
||||
roof = state.get("roof", {})
|
||||
@@ -374,7 +432,9 @@ class NativeCanRelativeResolver:
|
||||
@staticmethod
|
||||
def _window_targets(clause: str) -> tuple[str, ...]:
|
||||
individual = []
|
||||
for label, targets in tuple(WINDOW_TARGET_LABELS.items())[:8]:
|
||||
for label, targets in WINDOW_TARGET_LABELS.items():
|
||||
if len(targets) != 1:
|
||||
continue
|
||||
if label in clause and targets[0] not in individual:
|
||||
individual.append(targets[0])
|
||||
if individual:
|
||||
|
||||
@@ -6,6 +6,7 @@ import hashlib
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
@@ -73,9 +74,65 @@ DRIVER_SEAT_CAPABILITIES = frozenset(
|
||||
}
|
||||
)
|
||||
DRIVER_SEAT_TARGETS = frozenset({"driver", "front_left"})
|
||||
NATIVE_TARGET_SCOPES: dict[str, frozenset[str]] = {
|
||||
"ambient_light.set_brightness_level": frozenset({"vehicle"}),
|
||||
"ambient_light.set_color_index": frozenset({"vehicle"}),
|
||||
"ambient_light.set_power": frozenset({"vehicle"}),
|
||||
"climate.set_ac": frozenset({"vehicle"}),
|
||||
"climate.set_control_mode": frozenset({"vehicle"}),
|
||||
"climate.set_cycle_mode": frozenset({"vehicle"}),
|
||||
"climate.set_fan_level": frozenset({"vehicle"}),
|
||||
"climate.set_outlet_mode": frozenset({"vehicle"}),
|
||||
"climate.set_power": frozenset({"vehicle"}),
|
||||
"climate.set_temperature": frozenset(
|
||||
{"driver", "passenger", "rear", "rear_row", "vehicle", "front_row"}
|
||||
),
|
||||
"door.set_rear_lock": frozenset({"rear"}),
|
||||
"interior_light.set_indoor_power": frozenset({"vehicle"}),
|
||||
"interior_light.set_reading_power": frozenset({"all"}),
|
||||
"outlet.set_calibrated_position": frozenset(
|
||||
{"driver_left", "driver_right", "passenger_left", "passenger_right"}
|
||||
),
|
||||
"outlet.set_position_percent": frozenset(
|
||||
{"driver_left", "driver_right", "passenger_left", "passenger_right"}
|
||||
),
|
||||
"outlet.set_switch_state": frozenset(
|
||||
{"driver_left", "driver_right", "passenger_left", "passenger_right"}
|
||||
),
|
||||
"roof.set_shade_motion": frozenset({"vehicle"}),
|
||||
"roof.set_shade_position": frozenset({"vehicle"}),
|
||||
"seat.recall_driver_memory_native": DRIVER_SEAT_TARGETS,
|
||||
"seat.set_driver_axis_native": DRIVER_SEAT_TARGETS,
|
||||
"seat.set_driver_heating_native": DRIVER_SEAT_TARGETS,
|
||||
"seat.set_driver_massage_native": DRIVER_SEAT_TARGETS,
|
||||
"seat.set_driver_position_native": DRIVER_SEAT_TARGETS,
|
||||
"seat.set_driver_ventilation_native": DRIVER_SEAT_TARGETS,
|
||||
"window.set_motion": frozenset(
|
||||
{
|
||||
"all",
|
||||
"front",
|
||||
"rear",
|
||||
"front_left",
|
||||
"front_right",
|
||||
"rear_left",
|
||||
"rear_right",
|
||||
}
|
||||
),
|
||||
"window.set_position": frozenset(
|
||||
{"front_left", "front_right", "rear_left", "rear_right"}
|
||||
),
|
||||
}
|
||||
UNSUPPORTED_CH1_DOOR_LOCK_CAPABILITIES = frozenset(
|
||||
{"door.set_vehicle_lock"}
|
||||
)
|
||||
UNVERIFIED_NATIVE_CAPABILITIES = frozenset(
|
||||
{
|
||||
# The current generated CH1 rule aliases this to HVAC power bit 28.
|
||||
# The source matrix defines a distinct A/C request at bit 8 with 0x0DB
|
||||
# feedback, but that path has not completed vehicle HIL verification.
|
||||
"climate.set_ac",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _unsupported_ch1_door_lock_message(capability: str) -> str:
|
||||
@@ -117,19 +174,24 @@ class NativeCanRuntime:
|
||||
|
||||
@property
|
||||
def supported_capabilities(self) -> frozenset[str]:
|
||||
return CAN1_SUPPORTED_CAPABILITIES
|
||||
return CAN1_SUPPORTED_CAPABILITIES - UNVERIFIED_NATIVE_CAPABILITIES
|
||||
|
||||
def target_scopes_for(self, capability: str) -> frozenset[str]:
|
||||
"""Return only targets the byte-level CH1 compiler can preserve exactly."""
|
||||
|
||||
if capability not in self.supported_capabilities:
|
||||
return frozenset()
|
||||
return NATIVE_TARGET_SCOPES.get(capability, frozenset())
|
||||
|
||||
def supports(self, action: dict[str, Any]) -> bool:
|
||||
capability = str(action.get("capability"))
|
||||
if capability not in self.supported_capabilities:
|
||||
return False
|
||||
if capability in DRIVER_SEAT_CAPABILITIES:
|
||||
target = str(action.get("target", action.get("target_scope", "")))
|
||||
return target in DRIVER_SEAT_TARGETS
|
||||
if capability == "interior_light.set_reading_power":
|
||||
target = str(action.get("target", action.get("target_scope", "")))
|
||||
return target == "all"
|
||||
return True
|
||||
raw_target = action.get("target", action.get("target_scope", ""))
|
||||
if isinstance(raw_target, Mapping):
|
||||
raw_target = raw_target.get("zone", raw_target.get("instance", ""))
|
||||
target = str(raw_target)
|
||||
return target in self.target_scopes_for(capability)
|
||||
|
||||
def build_function_config(
|
||||
self,
|
||||
@@ -150,10 +212,13 @@ class NativeCanRuntime:
|
||||
f"CAN_DEV does not implement {capability_name}"
|
||||
)
|
||||
capability = self.capabilities[capability_name]
|
||||
target = str(action.get("target", action.get("target_scope", "vehicle")))
|
||||
if target not in capability.get("target_scopes", []):
|
||||
raw_target = action.get("target", action.get("target_scope", "vehicle"))
|
||||
if isinstance(raw_target, Mapping):
|
||||
raw_target = raw_target.get("zone", raw_target.get("instance", ""))
|
||||
target = str(raw_target)
|
||||
if target not in self.target_scopes_for(capability_name):
|
||||
raise NativeCanCompileError(
|
||||
f"{capability_name} does not support target {target}"
|
||||
f"{capability_name} has no exact CH1 binding for target {target}"
|
||||
)
|
||||
suffix = capability_name.split(".", 1)[1]
|
||||
operation_id = f"{target}_{suffix}_{index}"[:63]
|
||||
@@ -477,6 +542,10 @@ class NativeCanRuntime:
|
||||
|
||||
def _frame_specs(self, operation: dict[str, Any]) -> list[NativeFrameSpec]:
|
||||
capability = str(operation["capability"])
|
||||
if capability in UNVERIFIED_NATIVE_CAPABILITIES:
|
||||
raise NativeCanCompileError(
|
||||
f"{capability} has no HIL-verified physical CH1 binding"
|
||||
)
|
||||
if capability in UNSUPPORTED_CH1_DOOR_LOCK_CAPABILITIES:
|
||||
raise NativeCanCompileError(
|
||||
_unsupported_ch1_door_lock_message(capability)
|
||||
@@ -498,7 +567,7 @@ class NativeCanRuntime:
|
||||
)
|
||||
]
|
||||
|
||||
if capability in {"climate.set_power", "climate.set_ac"}:
|
||||
if capability == "climate.set_power":
|
||||
state = self._enum(parameters, "state", {"on": 1, "off": 2})
|
||||
payload = bytearray(8)
|
||||
_set_bits(payload, 28, 2, state)
|
||||
|
||||
@@ -38,6 +38,9 @@ from seat_domain import ( # noqa: E402
|
||||
validate_semantics,
|
||||
)
|
||||
|
||||
from cognitive_agent.application.assistant_plan_guard import ( # noqa: E402
|
||||
guard_assistant_plan,
|
||||
)
|
||||
from cognitive_agent.application.capability_registry import CapabilityRegistry # noqa: E402
|
||||
from cognitive_agent.application.conversation_state import ConversationStateStore # noqa: E402
|
||||
from cognitive_agent.application.domain_runtime import ConfiguredDomainRuntime # noqa: E402
|
||||
@@ -68,6 +71,7 @@ from cognitive_agent.application.native_can_relative import ( # noqa: E402
|
||||
)
|
||||
from cognitive_agent.application.semantic_ir import ( # noqa: E402
|
||||
apply_language_safety_guards,
|
||||
resolve_explicit_seat_scopes,
|
||||
)
|
||||
from cognitive_agent.application.native_can_feedback import ( # noqa: E402
|
||||
VehicleSafetyPreflightError,
|
||||
@@ -79,6 +83,7 @@ from cognitive_agent.application.native_can_feedback import ( # noqa: E402
|
||||
from cognitive_agent.application.native_can_runtime import ( # noqa: E402
|
||||
NativeCanExecutor,
|
||||
NativeCanRuntime,
|
||||
UNVERIFIED_NATIVE_CAPABILITIES,
|
||||
)
|
||||
from cognitive_agent.application.seat_calibration import position_pct_to_angle # noqa: E402
|
||||
from cognitive_agent.application.vehicle_profile import ( # noqa: E402
|
||||
@@ -134,12 +139,12 @@ MODE_EDITOR_REQUEST_ID_PATTERN = re.compile(r"^req-[a-z0-9-]{6,100}$")
|
||||
SEAT_SCOPE_PATTERN = re.compile(r"\bseats?\b|座椅|坐椅", re.IGNORECASE)
|
||||
DRIVER_SEAT_SCOPE_PATTERN = re.compile(
|
||||
r"\bdriver\b|\bfront[-_ ]?left\b|\bleft[-_ ]?front\b|"
|
||||
r"主驾|主驾驶(?:座|座椅|席)?|驾驶位|驾驶席|驾驶座(?:椅)?|"
|
||||
r"主驾|主驾驶(?:座|座椅|席)?|驾驶位|驾驶员|驾驶席|驾驶座(?:椅)?|"
|
||||
r"司机位|司机座(?:椅)?|左前",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
PASSENGER_SEAT_SCOPE_PATTERN = re.compile(
|
||||
r"\bpassenger\b|\bfront[-_ ]?right\b|\bright[-_ ]?front\b|副驾|乘客|右前",
|
||||
r"\bpassenger\b|\bfront[-_ ]?right\b|\bright[-_ ]?front\b|副驾|乘客|乘员|右前",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
WIRED_CAN_TRANSPORT = "wired_can"
|
||||
@@ -258,10 +263,38 @@ SAVE_MODE_PROFILE_NAME = re.compile(
|
||||
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
|
||||
)
|
||||
CREATE_MODE_PROFILE_NAME = re.compile(
|
||||
r"^(?:请(?:帮我)?|帮我)?(?:构建|创建|新建|新增|生成|添加)"
|
||||
r"(?:一个|个|一套|套)?"
|
||||
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
|
||||
r"(?:配置)?(?:并(?:添加|加入)(?:到)?功能模式(?:栏)?)?[。.!!]?$"
|
||||
r"^\s*(?:(?:请|麻烦)\s*(?:帮我|帮忙|给我)?\s*|"
|
||||
r"(?:帮我|帮忙|给我)\s*)?"
|
||||
r"(?:构建|创建|新建|新增|生成|添加)\s*(?:一下)?\s*"
|
||||
r"(?:一个|个|一套|套)?\s*"
|
||||
r"(?:名为|叫做|叫)?\s*[“\"'「『]?"
|
||||
r"(?P<name>[^,。,.!!::\s“”\"'「」『』]{1,64}?模式)"
|
||||
r"[”\"'」』]?"
|
||||
r"\s*(?:配置)?(?:并(?:添加|加入)(?:到)?功能模式(?:栏)?)?"
|
||||
r"(?:[,,::]\s*(?P<instruction>[\s\S]{1,800}))?[。.!!]?\s*$"
|
||||
)
|
||||
|
||||
# ``、`` is used both as a command separator (``车窗关闭、空调24度``)
|
||||
# and as an enumeration separator (``主驾、副驾车窗关闭``). Treat it as a
|
||||
# boundary only when both sides already look like self-contained controls;
|
||||
# otherwise keep it intact for the target/feature parsers below.
|
||||
CONTROL_CLAUSE_SUBJECT_PATTERN = re.compile(
|
||||
r"座椅|座位|靠背|椅背|坐盆|坐垫|腿托|脚托|滑轨|"
|
||||
r"通风|加热|按摩|零重力|半躺|女王副驾|休闲躺椅|"
|
||||
r"遮阳帘|天幕帘|天幕|车窗|窗户|"
|
||||
r"灯光|室内灯|车内灯|阅读灯|氛围灯|"
|
||||
r"空调|压缩机|温度|风量|风速|内循环|外循环|"
|
||||
r"车门|门锁|后备箱|前大灯|近光灯|远光灯|雾灯|转向灯|双闪|"
|
||||
r"出风口"
|
||||
)
|
||||
CONTROL_CLAUSE_PREDICATE_PATTERN = re.compile(
|
||||
r"打开|开启|启动|关闭|关掉|关上|停止|暂停|取消|"
|
||||
r"调整|调节|调到|调成|调为|调至|设置|设为|设到|切换|"
|
||||
r"移动|移到|移至|前移|后移|上移|下移|升高|降低|抬起|放倒|"
|
||||
r"调直|立直|竖直|归零|复位|锁上|上锁|落锁|解锁|"
|
||||
r"零重力|半躺|最大|最小|最高|最低|最前|最后|到底|"
|
||||
r"(?:\d+(?:\.\d+)?|[零一二三四五六七八九十百两半]+)"
|
||||
r"(?:%|%|度|℃|档|级|秒|毫秒)"
|
||||
)
|
||||
UPDATE_MODE_PROFILE_PREFIX = re.compile(
|
||||
r"^(?:请(?:帮我)?|帮我)?(?:修改一下|修改|调整|更改|更新|改一下)"
|
||||
@@ -430,9 +463,6 @@ class SeatDemoService:
|
||||
return normalized
|
||||
|
||||
def runtime_info(self) -> dict[str, Any]:
|
||||
catalog = yaml.safe_load(
|
||||
(self.domain_root / "config" / "capabilities.yaml").read_text(encoding="utf-8")
|
||||
)
|
||||
domain_runtime_info = [
|
||||
runtime.runtime_info() for runtime in self.domain_runtimes.values()
|
||||
]
|
||||
@@ -756,6 +786,30 @@ class SeatDemoService:
|
||||
"request_id"
|
||||
) != clean_request_id:
|
||||
raise ValueError("控制指令流来源结果不完整,请重新生成。")
|
||||
if source_result.get("status") != "succeeded":
|
||||
raise ValueError("控制指令流没有完整通过校验,请重新生成。")
|
||||
source_vehicle = source_result.get("vehicle")
|
||||
source_vehicle_id = (
|
||||
str(source_vehicle.get("id") or "").strip()
|
||||
if isinstance(source_vehicle, dict)
|
||||
else ""
|
||||
)
|
||||
if source_vehicle_id and source_vehicle_id != resolved_vehicle_id:
|
||||
raise ValueError("控制指令流不属于当前车型,请重新生成。")
|
||||
source_draft = source_result.get("mode_profile_draft")
|
||||
if isinstance(source_draft, dict):
|
||||
draft_request_id = str(source_draft.get("source_request_id") or "").strip()
|
||||
draft_vehicle_id = str(source_draft.get("vehicle_id") or "").strip()
|
||||
draft_instruction = str(source_draft.get("instruction") or "").strip()
|
||||
draft_name = str(source_draft.get("name") or "").strip()
|
||||
if draft_request_id != clean_request_id:
|
||||
raise ValueError("功能模式草稿与控制指令流不匹配,请重新生成。")
|
||||
if draft_vehicle_id and draft_vehicle_id != resolved_vehicle_id:
|
||||
raise ValueError("功能模式草稿不属于当前车型,请重新生成。")
|
||||
if draft_instruction and draft_instruction != clean_instruction:
|
||||
raise ValueError("自然语言指令已修改,请重新生成模式预览。")
|
||||
if draft_name and draft_name != clean_name:
|
||||
raise ValueError("功能模式名称已修改,请重新生成模式预览。")
|
||||
recorded_instruction = str(source_result.get("utterance", "")).strip()
|
||||
if not recorded_instruction:
|
||||
source_spans = {
|
||||
@@ -1353,6 +1407,7 @@ class SeatDemoService:
|
||||
reasoning_effort: str | None = None,
|
||||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||||
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||||
assistant_plan: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Validate and execute deterministic controls before hosted parsing."""
|
||||
|
||||
@@ -1390,12 +1445,15 @@ class SeatDemoService:
|
||||
reasoning_effort=reasoning_effort,
|
||||
control_transport=control_transport,
|
||||
_on_commands_validated=on_commands_validated,
|
||||
_assistant_plan=assistant_plan,
|
||||
)
|
||||
result.update(
|
||||
{
|
||||
"phase": "control_plane_complete",
|
||||
"preview_request_id": result["request_id"],
|
||||
"unresolved_clauses": [],
|
||||
"unresolved_clauses": list(
|
||||
result.get("unresolved_clauses") or []
|
||||
),
|
||||
"supplement_required": False,
|
||||
}
|
||||
)
|
||||
@@ -1444,6 +1502,7 @@ class SeatDemoService:
|
||||
_local_preview=True,
|
||||
_allow_empty_control_result=True,
|
||||
_on_commands_validated=publish_local_commands,
|
||||
_assistant_plan=assistant_plan,
|
||||
)
|
||||
elapsed_ms = round((time.perf_counter() - started) * 1000, 3)
|
||||
supplement_required = bool(
|
||||
@@ -1889,6 +1948,7 @@ class SeatDemoService:
|
||||
_direct_can_batch: tuple[list[str], dict[str, Any]] | None = None,
|
||||
_direct_mode_profile_id: str | None = None,
|
||||
_speech_understanding: dict[str, Any] | None = None,
|
||||
_assistant_plan: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
control_transport = self._validate_control_transport(control_transport)
|
||||
# Creating a reusable scene is a configuration workflow. Synthesize
|
||||
@@ -1916,6 +1976,7 @@ class SeatDemoService:
|
||||
_direct_can_batch,
|
||||
_direct_mode_profile_id,
|
||||
_speech_understanding,
|
||||
assistant_plan=_assistant_plan,
|
||||
)
|
||||
if control_transport == WIRELESS_MQTT_TRANSPORT:
|
||||
# The head unit owns both physical CAN channels in wireless mode.
|
||||
@@ -1939,6 +2000,7 @@ class SeatDemoService:
|
||||
_direct_can_batch,
|
||||
_direct_mode_profile_id,
|
||||
_speech_understanding,
|
||||
assistant_plan=_assistant_plan,
|
||||
)
|
||||
if hardware_access_bypass or self._driver_only_vehicle_execution_active(
|
||||
control_transport
|
||||
@@ -1962,6 +2024,7 @@ class SeatDemoService:
|
||||
_direct_can_batch,
|
||||
_direct_mode_profile_id,
|
||||
_speech_understanding,
|
||||
assistant_plan=_assistant_plan,
|
||||
)
|
||||
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
|
||||
with self.hardware_execution.reserve():
|
||||
@@ -1984,6 +2047,7 @@ class SeatDemoService:
|
||||
_direct_can_batch,
|
||||
_direct_mode_profile_id,
|
||||
_speech_understanding,
|
||||
assistant_plan=_assistant_plan,
|
||||
)
|
||||
|
||||
def _process_once(
|
||||
@@ -2005,6 +2069,7 @@ class SeatDemoService:
|
||||
direct_can_batch: tuple[list[str], dict[str, Any]] | None = None,
|
||||
direct_mode_profile_id: str | None = None,
|
||||
speech_understanding: dict[str, Any] | None = None,
|
||||
assistant_plan: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
control_transport = self._validate_control_transport(control_transport)
|
||||
utterance = utterance.strip()
|
||||
@@ -2073,6 +2138,7 @@ class SeatDemoService:
|
||||
simulation_fault,
|
||||
mode_create_name,
|
||||
control_transport,
|
||||
assistant_plan=assistant_plan,
|
||||
)
|
||||
|
||||
request_id = f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
|
||||
@@ -2258,18 +2324,44 @@ class SeatDemoService:
|
||||
reasoning_effort=reasoning_effort,
|
||||
local_preview=local_preview,
|
||||
model_parse_source=model_parse_source,
|
||||
assistant_plan=assistant_plan,
|
||||
)
|
||||
dynamic_mode_stop = self._is_dynamic_mode_stop_request(utterance)
|
||||
if dynamic_mode_stop:
|
||||
parsed = self._apply_dynamic_mode_stop_request(parsed, utterance)
|
||||
# Reuse the same conservative clause segmentation for the final
|
||||
# native pass. Without this normalization a Chinese enumeration
|
||||
# comma between independent controls can leak an early target
|
||||
# (for example ``副驾``) into a later HVAC clause and can also
|
||||
# hide otherwise valid window/temperature actions.
|
||||
segmented_utterance = ",".join(
|
||||
self._split_intent_clauses(utterance)
|
||||
)
|
||||
relative_resolution = (
|
||||
self.native_can_relative_resolver.resolve(utterance, context)
|
||||
if self.native_can_relative_resolver.matches(utterance)
|
||||
self.native_can_relative_resolver.resolve(
|
||||
segmented_utterance,
|
||||
context,
|
||||
)
|
||||
if self.native_can_relative_resolver.matches(
|
||||
segmented_utterance
|
||||
)
|
||||
else None
|
||||
)
|
||||
native_merge_utterance = segmented_utterance
|
||||
if any(
|
||||
request.get("capability") in {SAVE_PRESET_CAPABILITY, APPLY_PRESET_CAPABILITY}
|
||||
for request in parsed.get("model_requests", [])
|
||||
):
|
||||
# These registered capabilities currently have one executable
|
||||
# target (the passenger-seat posture store). Keep that
|
||||
# adapter-resolved target from being discarded by the native
|
||||
# parser's general unscoped-seat => driver default. The
|
||||
# original utterance is still used by the language safety
|
||||
# guards below.
|
||||
native_merge_utterance = f"副驾座椅,{utterance}"
|
||||
parsed = merge_native_can_intent(
|
||||
parsed,
|
||||
utterance,
|
||||
native_merge_utterance,
|
||||
relative_actions=(
|
||||
relative_resolution.actions if relative_resolution else None
|
||||
),
|
||||
@@ -2299,8 +2391,25 @@ class SeatDemoService:
|
||||
"saved_mode_command_flow",
|
||||
)
|
||||
if local_preview:
|
||||
parsed["ambiguities"] = []
|
||||
parsed["unresolved_spans"] = []
|
||||
# A preview may discard ordinary model-supplement questions, but
|
||||
# target and safety conflicts are execution gates. Keeping these
|
||||
# makes the assistant path no less strict than normal process().
|
||||
parsed["ambiguities"] = [
|
||||
item
|
||||
for item in parsed.get("ambiguities", [])
|
||||
if str(item.get("field", ""))
|
||||
in {"target", "target_scope", "mode_profile", "risk", "safety", "utterance"}
|
||||
or str(item.get("reason", "")).startswith(
|
||||
("mode_target_", "safety_", "risk_", "mixed_")
|
||||
)
|
||||
]
|
||||
parsed["unresolved_spans"] = [
|
||||
item
|
||||
for item in parsed.get("unresolved_spans", [])
|
||||
if str(item.get("reason", "")).startswith(
|
||||
("mode_target_", "safety_", "risk_", "mixed_")
|
||||
)
|
||||
]
|
||||
parsed["model_requests"] = []
|
||||
if excluded_control_signatures:
|
||||
parsed = self._filter_excluded_controls(
|
||||
@@ -2332,9 +2441,18 @@ class SeatDemoService:
|
||||
]
|
||||
has_control = any(
|
||||
parsed.get(key)
|
||||
for key in ("actions", "domain_actions", "native_can_actions")
|
||||
for key in (
|
||||
"actions",
|
||||
"domain_actions",
|
||||
"native_can_actions",
|
||||
"preset_actions",
|
||||
)
|
||||
)
|
||||
if allow_empty_control_result and not has_control:
|
||||
if (
|
||||
allow_empty_control_result
|
||||
and not has_control
|
||||
and not parsed.get("ambiguities")
|
||||
):
|
||||
result["provider_used"] = provider_used
|
||||
result["decision_summary"] = [
|
||||
*parsed.get("decision_summary", []),
|
||||
@@ -3367,6 +3485,47 @@ class SeatDemoService:
|
||||
]
|
||||
return rejected
|
||||
|
||||
target_conflict = self._explicit_mode_target_conflict(
|
||||
matched_modes[0],
|
||||
utterance,
|
||||
)
|
||||
if target_conflict is not None:
|
||||
rejected = deepcopy(parsed)
|
||||
for key in (
|
||||
"actions",
|
||||
"domain_actions",
|
||||
"native_can_actions",
|
||||
"preset_actions",
|
||||
"preset_events",
|
||||
"deferred_requests",
|
||||
"model_requests",
|
||||
):
|
||||
rejected[key] = []
|
||||
requested_scope, supported_scopes = target_conflict
|
||||
rejected["ambiguities"] = [
|
||||
*rejected.get("ambiguities", []),
|
||||
{
|
||||
"field": "mode_profile",
|
||||
"question": (
|
||||
f"该模式的座椅目标为 {', '.join(supported_scopes)},"
|
||||
f"与用户指定的 {requested_scope} 不一致,请重新确认。"
|
||||
),
|
||||
"reason": "mode_target_scope_mismatch",
|
||||
},
|
||||
]
|
||||
rejected["unresolved_spans"] = [
|
||||
*rejected.get("unresolved_spans", []),
|
||||
{
|
||||
"text": utterance,
|
||||
"reason": "mode_target_scope_mismatch",
|
||||
},
|
||||
]
|
||||
rejected["decision_summary"] = [
|
||||
*rejected.get("decision_summary", []),
|
||||
"用户显式座位与功能模式登记目标不一致,已停止所有模式动作。",
|
||||
]
|
||||
return rejected
|
||||
|
||||
parsed = self._apply_implicit_mode_target_policy(
|
||||
parsed,
|
||||
matched_modes,
|
||||
@@ -3449,6 +3608,46 @@ class SeatDemoService:
|
||||
)
|
||||
return merged
|
||||
|
||||
@staticmethod
|
||||
def _explicit_mode_target_conflict(
|
||||
profile: ModeProfile,
|
||||
utterance: str,
|
||||
) -> tuple[str, tuple[str, ...]] | None:
|
||||
"""Reject a named mode when its persisted seat scope contradicts text."""
|
||||
|
||||
aliases = (profile.id, profile.name, *profile.aliases)
|
||||
mode_clause = next(
|
||||
(
|
||||
clause
|
||||
for clause in re.split(r"[,,;;。!?!?\n]+", utterance)
|
||||
if any(
|
||||
re.sub(r"\s+", "", alias.casefold())
|
||||
in re.sub(r"\s+", "", clause.casefold())
|
||||
for alias in aliases
|
||||
if alias
|
||||
)
|
||||
),
|
||||
utterance,
|
||||
)
|
||||
requested = resolve_explicit_seat_scopes(
|
||||
mode_clause,
|
||||
assume_seat_context=True,
|
||||
)
|
||||
if requested is None:
|
||||
return None
|
||||
|
||||
normalized_scopes = {
|
||||
"driver" if operation.target_scope == "front_left" else
|
||||
"passenger" if operation.target_scope == "front_right" else
|
||||
"rear" if operation.target_scope in {"rear", "rear_left", "rear_right"} else
|
||||
operation.target_scope
|
||||
for operation in profile.operations
|
||||
if operation.domain == "seat"
|
||||
}
|
||||
if requested == normalized_scopes:
|
||||
return None
|
||||
return ",".join(sorted(requested)), tuple(sorted(normalized_scopes))
|
||||
|
||||
@staticmethod
|
||||
def _apply_implicit_mode_target_policy(
|
||||
parsed: dict[str, Any],
|
||||
@@ -4071,6 +4270,114 @@ class SeatDemoService:
|
||||
except ValueError:
|
||||
return str(path)
|
||||
|
||||
@staticmethod
|
||||
def _mode_feedback_text(value: object, *, limit: int = 180) -> str:
|
||||
text = re.sub(r"[\x00-\x1f\x7f]+", " ", str(value or ""))
|
||||
return re.sub(r"\s+", " ", text).strip(" ,,。;;::")[:limit]
|
||||
|
||||
@classmethod
|
||||
def _mode_failure_issue(
|
||||
cls,
|
||||
clause: str,
|
||||
parsed: dict[str, Any],
|
||||
) -> dict[str, str]:
|
||||
display_clause = cls._mode_feedback_text(
|
||||
re.sub(r"^(?:副驾座椅|主驾座椅)[,,]", "", clause),
|
||||
limit=120,
|
||||
)
|
||||
ambiguities = [
|
||||
item
|
||||
for item in parsed.get("ambiguities", [])
|
||||
if isinstance(item, dict) and str(item.get("field") or "") != "capability"
|
||||
]
|
||||
ambiguity = ambiguities[0] if ambiguities else None
|
||||
field = str(ambiguity.get("field") or "") if ambiguity else ""
|
||||
question = cls._mode_feedback_text(
|
||||
ambiguity.get("question") if ambiguity else "",
|
||||
limit=220,
|
||||
)
|
||||
# Parser questions may contain both the diagnosis and a concrete
|
||||
# correction. Split those parts so every public failure has the same
|
||||
# reason/next-step contract.
|
||||
reason_from_question = question
|
||||
next_from_question = ""
|
||||
for separator in (";请", "。请"):
|
||||
if separator in question:
|
||||
reason_from_question, suffix = question.split(separator, 1)
|
||||
next_from_question = f"请{suffix}"
|
||||
break
|
||||
|
||||
if reason_from_question and not re.search(
|
||||
r"(?:CAN|CH[01]|JSON|schema|0x[0-9a-f]+|内部路径)",
|
||||
reason_from_question,
|
||||
re.IGNORECASE,
|
||||
):
|
||||
reason = f"设置“{display_clause}”未通过:{reason_from_question}"
|
||||
else:
|
||||
reason = (
|
||||
f"设置“{display_clause}”未被识别为当前支持的座舱控制动作"
|
||||
)
|
||||
|
||||
if next_from_question:
|
||||
next_step = next_from_question
|
||||
elif "temperature" in field or "温度" in display_clause:
|
||||
next_step = (
|
||||
"请只保留一个18至32℃的整数温度,例如“空调调整到24度”,然后重试"
|
||||
)
|
||||
elif field == "target" or "target_scope" in field:
|
||||
next_step = (
|
||||
"请改用当前支持的副驾座椅目标,并明确部件与位置,例如"
|
||||
"“副驾腿托调整到80%”"
|
||||
)
|
||||
elif "position" in field or re.search(
|
||||
r"座椅|靠背|椅背|坐垫|坐盆|腿托|脚托|滑轨",
|
||||
display_clause,
|
||||
):
|
||||
next_step = (
|
||||
"请按“副驾 + 座椅部件 + 方向或0到100%位置”重述,例如"
|
||||
"“副驾腿托向上调整到最大位置”或“副驾腿托调整到80%”"
|
||||
)
|
||||
else:
|
||||
next_step = (
|
||||
"请按“设备 + 动作 + 明确目标”重述,例如“关闭所有车窗”、"
|
||||
"“副驾靠背调整到80%”或“空调调整到24度”"
|
||||
)
|
||||
return {
|
||||
"clause": display_clause,
|
||||
"reason": reason,
|
||||
"next_step": cls._mode_feedback_text(next_step, limit=260),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _mode_creation_failure_from_clauses(
|
||||
cls,
|
||||
clause_results: list[tuple[str, dict[str, Any], str]],
|
||||
unresolved_clauses: list[str],
|
||||
) -> dict[str, Any]:
|
||||
unresolved = set(unresolved_clauses)
|
||||
issues = [
|
||||
cls._mode_failure_issue(clause, parsed)
|
||||
for clause, parsed, status in clause_results
|
||||
if status != "complete" and clause in unresolved
|
||||
][:3]
|
||||
if not issues:
|
||||
issues = [
|
||||
cls._mode_failure_issue(clause, {})
|
||||
for clause in unresolved_clauses[:3]
|
||||
]
|
||||
reason = ";".join(dict.fromkeys(item["reason"] for item in issues))
|
||||
next_step = ";".join(
|
||||
dict.fromkeys(item["next_step"] for item in issues)
|
||||
)
|
||||
return {
|
||||
"code": "MODE_PROFILE_DRAFT_INCOMPLETE",
|
||||
"reason": reason or "部分座舱设置没有转换为可保存的控制动作",
|
||||
"next_step": next_step
|
||||
or "请将未识别设置改写为明确的设备、动作和目标值后重试",
|
||||
"unresolved_clauses": [item["clause"] for item in issues],
|
||||
"issues": issues,
|
||||
}
|
||||
|
||||
def _process_mode_profile_create(
|
||||
self,
|
||||
utterance: str,
|
||||
@@ -4082,17 +4389,23 @@ class SeatDemoService:
|
||||
simulation_fault: str,
|
||||
mode_name: str,
|
||||
control_transport: str,
|
||||
assistant_plan: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build, verify, and persist a new named mode in one user turn.
|
||||
"""Build and verify a named-mode draft for explicit user confirmation.
|
||||
|
||||
A request such as ``构建一个K歌模式`` used to be interpreted as an
|
||||
ordinary temporary scene. Although that scene could execute
|
||||
successfully, it never reached the mode registry. Run the generated
|
||||
scene first so only validated successful actions are persisted, then
|
||||
use the existing control-plane save path to create the YAML profile.
|
||||
ordinary temporary scene. Generate the scene through the normal
|
||||
dry-run path, but leave persistence to the existing mode-card API so
|
||||
the assistant can show the exact actions before the user saves them.
|
||||
"""
|
||||
|
||||
source_utterance = (
|
||||
create_match = CREATE_MODE_PROFILE_NAME.fullmatch(utterance)
|
||||
requested_instruction = (
|
||||
str(create_match.group("instruction") or "").strip(" ,,::。.!!")
|
||||
if create_match is not None
|
||||
else ""
|
||||
)
|
||||
source_utterance = requested_instruction or (
|
||||
f"进入{mode_name},请根据该场景名称使用当前可控原子能力构建并执行场景"
|
||||
)
|
||||
source_result = self._process_once(
|
||||
@@ -4104,8 +4417,67 @@ class SeatDemoService:
|
||||
vehicle_profile.id,
|
||||
simulation_fault,
|
||||
control_transport=control_transport,
|
||||
assistant_plan=assistant_plan,
|
||||
)
|
||||
if source_result.get("status") not in {"succeeded", "partially_succeeded"}:
|
||||
draft_context = self._context_for_vehicle(
|
||||
self.conversations.snapshot(conversation_id),
|
||||
vehicle_profile,
|
||||
)
|
||||
draft_context["mode_profiles"] = self.mode_profiles.public_modes(
|
||||
vehicle_profile.id
|
||||
)
|
||||
local_clause_results = self._local_clause_results(
|
||||
source_utterance,
|
||||
draft_context,
|
||||
)
|
||||
unresolved_draft_clauses = [
|
||||
clause
|
||||
for clause, _parsed, status in local_clause_results
|
||||
if status != "complete"
|
||||
and not self.mode_profiles.match_text(
|
||||
clause,
|
||||
vehicle_id=vehicle_profile.id,
|
||||
)
|
||||
]
|
||||
if provider == "local" and assistant_plan is None and unresolved_draft_clauses:
|
||||
source_result["status"] = "needs_confirmation"
|
||||
source_result["unresolved_clauses"] = unresolved_draft_clauses
|
||||
source_result["mode_creation_failure"] = (
|
||||
self._mode_creation_failure_from_clauses(
|
||||
local_clause_results,
|
||||
unresolved_draft_clauses,
|
||||
)
|
||||
)
|
||||
source_result["supplement_required"] = False
|
||||
source_result["validation"] = [
|
||||
*list(source_result.get("validation", [])),
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "MODE_PROFILE_DRAFT_INCOMPLETE",
|
||||
"path": "mode_profile_draft.instruction",
|
||||
"message": (
|
||||
"以下模式设置尚未完整解析:"
|
||||
+ ";".join(unresolved_draft_clauses)
|
||||
),
|
||||
},
|
||||
]
|
||||
source_result["decision_summary"] = [
|
||||
f"未能完整构建“{mode_name}”,因此没有生成可保存草稿。",
|
||||
*list(source_result.get("decision_summary", [])),
|
||||
"请补充为当前支持的明确设备动作后重试。",
|
||||
]
|
||||
self._persist(source_result)
|
||||
return source_result
|
||||
if source_result.get("status") != "succeeded":
|
||||
source_result["mode_creation_failure"] = {
|
||||
"code": "MODE_PROFILE_ACTION_VALIDATION_FAILED",
|
||||
"reason": "场景动作未通过车辆能力或安全校验",
|
||||
"next_step": (
|
||||
"请检查每项设备目标是否在当前支持范围内;仍失败时可分条输入后重试"
|
||||
),
|
||||
"unresolved_clauses": [],
|
||||
"issues": [],
|
||||
}
|
||||
source_result["decision_summary"] = [
|
||||
f"未能完成“{mode_name}”的场景动作验证,因此没有创建长期模式。",
|
||||
*list(source_result.get("decision_summary", [])),
|
||||
@@ -4113,40 +4485,75 @@ class SeatDemoService:
|
||||
self._persist(source_result)
|
||||
return source_result
|
||||
|
||||
saved_result = self._process_once(
|
||||
f"这个模式不错,固化为{mode_name}",
|
||||
"local",
|
||||
conversation_id,
|
||||
"dry_run",
|
||||
None,
|
||||
vehicle_profile.id,
|
||||
"normal",
|
||||
control_transport=control_transport,
|
||||
intent_ir = source_result.get("intent_ir")
|
||||
intent_spec = intent_ir.get("spec") if isinstance(intent_ir, dict) else None
|
||||
raw_intent_actions = (
|
||||
intent_spec.get("actions", []) if isinstance(intent_spec, dict) else []
|
||||
)
|
||||
if saved_result.get("status") != "succeeded" or not saved_result.get(
|
||||
"mode_profile"
|
||||
):
|
||||
return saved_result
|
||||
reusable_actions = [
|
||||
item for item in raw_intent_actions if isinstance(item, dict)
|
||||
] if isinstance(raw_intent_actions, list) else []
|
||||
if not reusable_actions:
|
||||
source_result["status"] = "needs_confirmation"
|
||||
source_result["mode_creation_failure"] = {
|
||||
"code": "MODE_PROFILE_DRAFT_EMPTY",
|
||||
"reason": "没有识别到可保存的座舱控制动作",
|
||||
"next_step": (
|
||||
"请按“设备 + 动作 + 明确目标”补充,例如“关闭所有车窗,空调调整到24度”"
|
||||
),
|
||||
"unresolved_clauses": [],
|
||||
"issues": [],
|
||||
}
|
||||
source_result["validation"] = [
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "MODE_PROFILE_DRAFT_EMPTY",
|
||||
"path": "intent_ir.spec.actions",
|
||||
"message": "没有生成可保存的功能模式动作,请补充更明确的座舱设置。",
|
||||
}
|
||||
]
|
||||
source_result["decision_summary"] = [
|
||||
f"未能为“{mode_name}”生成可保存的场景动作。",
|
||||
*list(source_result.get("decision_summary", [])),
|
||||
]
|
||||
self._persist(source_result)
|
||||
return source_result
|
||||
|
||||
saved_result["mode_creation"] = {
|
||||
domains = list(
|
||||
dict.fromkeys(
|
||||
str(item.get("domain") or "")
|
||||
for item in reusable_actions
|
||||
if str(item.get("domain") or "")
|
||||
)
|
||||
)
|
||||
source_request_id = str(source_result.get("request_id") or "")
|
||||
source_result["mode_profile_draft"] = {
|
||||
"name": mode_name,
|
||||
"instruction": source_utterance,
|
||||
"source_request_id": source_request_id,
|
||||
"vehicle_id": vehicle_profile.id,
|
||||
"domains": domains,
|
||||
"operation_count": len(reusable_actions),
|
||||
}
|
||||
source_result["mode_creation"] = {
|
||||
"requested_utterance": utterance,
|
||||
"source_utterance": source_utterance,
|
||||
"source_request_id": source_result.get("request_id"),
|
||||
"source_request_id": source_request_id,
|
||||
"source_status": source_result.get("status"),
|
||||
"source_provider": source_result.get("provider_used"),
|
||||
"operation_count": saved_result["mode_profile"].get(
|
||||
"operation_count", 0
|
||||
),
|
||||
"operation_count": len(reusable_actions),
|
||||
}
|
||||
source_summary = list(source_result.get("decision_summary", []))
|
||||
saved_result["decision_summary"] = [
|
||||
source_result["orchestration_route"] = "mode_profile_create_preview"
|
||||
source_result["decision_summary"] = [
|
||||
f"已构建并验证“{mode_name}”场景。",
|
||||
*source_summary,
|
||||
*list(saved_result.get("decision_summary", [])),
|
||||
f"“{mode_name}”已加入功能模式,可直接点击执行。",
|
||||
*list(source_result.get("decision_summary", [])),
|
||||
"请确认动作后保存为功能模式,或先进行一次模拟试运行。",
|
||||
]
|
||||
self._persist(saved_result)
|
||||
return saved_result
|
||||
# Overwrite the persisted source result with the draft metadata. The
|
||||
# mode-card endpoint later re-reads this server-owned artifact and
|
||||
# refuses any client-side instruction or request-id substitution.
|
||||
self._persist(source_result)
|
||||
return source_result
|
||||
|
||||
def _process_mode_profile_save(
|
||||
self,
|
||||
@@ -4415,6 +4822,21 @@ class SeatDemoService:
|
||||
for action in candidate_native_actions
|
||||
}
|
||||
virtual_actions: list[dict[str, Any]] = []
|
||||
demoted_signatures: set[tuple[str, str, str, str]] = set()
|
||||
for action in candidate_native_actions:
|
||||
if str(action.get("capability")) not in UNVERIFIED_NATIVE_CAPABILITIES:
|
||||
continue
|
||||
demoted = dict(action)
|
||||
demoted["execution_backend"] = "configured_domain"
|
||||
virtual_actions.append(demoted)
|
||||
demoted_signatures.add(
|
||||
(
|
||||
str(action.get("domain")),
|
||||
str(action.get("capability")),
|
||||
str(action.get("target", action.get("target_scope", "vehicle"))),
|
||||
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
|
||||
)
|
||||
)
|
||||
for action in result.get("domain_actions", []):
|
||||
signature = (
|
||||
str(action.get("domain")),
|
||||
@@ -4458,7 +4880,16 @@ class SeatDemoService:
|
||||
for item in result.get("deferred_requests", [])
|
||||
if not (
|
||||
item.get("execution_backend") == "native_can"
|
||||
and self.native_can_runtime.supports(item)
|
||||
and (
|
||||
self.native_can_runtime.supports(item)
|
||||
or (
|
||||
str(item.get("domain")),
|
||||
str(item.get("capability")),
|
||||
str(item.get("target", item.get("target_scope", "vehicle"))),
|
||||
json.dumps(item.get("parameters", {}), sort_keys=True, ensure_ascii=False),
|
||||
)
|
||||
in demoted_signatures
|
||||
)
|
||||
)
|
||||
]
|
||||
for item in deferred_requests:
|
||||
@@ -5480,13 +5911,62 @@ class SeatDemoService:
|
||||
|
||||
@staticmethod
|
||||
def _split_intent_clauses(utterance: str) -> list[str]:
|
||||
fragments = [
|
||||
def looks_like_control(value: str) -> bool:
|
||||
return bool(
|
||||
CONTROL_CLAUSE_SUBJECT_PATTERN.search(value)
|
||||
and CONTROL_CLAUSE_PREDICATE_PATTERN.search(value)
|
||||
)
|
||||
|
||||
def split_independent_controls(
|
||||
value: str,
|
||||
separator_pattern: str,
|
||||
) -> list[str]:
|
||||
tokens = re.split(f"({separator_pattern})", value)
|
||||
current = tokens[0].strip()
|
||||
separated: list[str] = []
|
||||
for index in range(1, len(tokens), 2):
|
||||
separator = tokens[index]
|
||||
next_item = tokens[index + 1].strip()
|
||||
if looks_like_control(current) and looks_like_control(next_item):
|
||||
if current:
|
||||
separated.append(current)
|
||||
current = next_item
|
||||
else:
|
||||
current = f"{current}{separator}{next_item}".strip()
|
||||
if current:
|
||||
separated.append(current)
|
||||
return separated
|
||||
|
||||
coarse_fragments = [
|
||||
item.strip()
|
||||
for item in re.split(r"[,,。;;::!!??]+", utterance)
|
||||
for item in re.split(r"[,,。;;::!!??\r\n]+", utterance)
|
||||
if item.strip()
|
||||
]
|
||||
fragments: list[str] = []
|
||||
for coarse_fragment in coarse_fragments:
|
||||
list_fragments = split_independent_controls(coarse_fragment, r"、")
|
||||
for list_fragment in list_fragments:
|
||||
fragments.extend(
|
||||
split_independent_controls(
|
||||
list_fragment,
|
||||
r"然后|随后|接着|最后|同时|并且|以及|再|并",
|
||||
)
|
||||
)
|
||||
|
||||
clauses: list[str] = []
|
||||
for fragment in fragments:
|
||||
fragment = re.sub(
|
||||
r"^(?:然后|随后|接着|最后|同时|并且|以及|并)",
|
||||
"",
|
||||
fragment,
|
||||
).strip()
|
||||
fragment = re.sub(
|
||||
r"^再(?=(?:打开|开启|启动|关闭|关掉|关上|停止|暂停|取消))",
|
||||
"",
|
||||
fragment,
|
||||
).strip()
|
||||
if not fragment:
|
||||
continue
|
||||
# A massage mode/strength phrase may omit both "主驾" and "按摩"
|
||||
# after the first clause. Keep the whole request atomic so the ON
|
||||
# fallback does not emit relax before an explicit later mode.
|
||||
@@ -5755,6 +6235,7 @@ class SeatDemoService:
|
||||
reasoning_effort: str | None = None,
|
||||
local_preview: bool = False,
|
||||
model_parse_source: str = "cloud_model",
|
||||
assistant_plan: dict[str, Any] | None = None,
|
||||
) -> tuple[dict[str, Any], str, str | None]:
|
||||
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
||||
clause_results = self._local_clause_results(utterance, context)
|
||||
@@ -5774,6 +6255,50 @@ class SeatDemoService:
|
||||
for key in ("actions", "domain_actions", "native_can_actions")
|
||||
)
|
||||
safe_fallback = trusted_local if trusted_controls else all_local
|
||||
|
||||
# A Codex-authored plan is an untrusted normalization hint for only
|
||||
# the clauses the deterministic parser could not finish. The guard
|
||||
# binds every proposed capability back to that server-owned text and
|
||||
# prevents it from replacing a locally resolved capability/target.
|
||||
# ModelPlanAdapter then applies the same target, parameter, range and
|
||||
# runtime-availability checks as the established hosted-model path.
|
||||
if assistant_plan is not None and fallback_clauses:
|
||||
fallback_utterance = ",".join(fallback_clauses)
|
||||
guarded = guard_assistant_plan(
|
||||
assistant_plan,
|
||||
fallback_text=fallback_utterance,
|
||||
capability_context=self.capability_context,
|
||||
executable_capabilities={
|
||||
str(capability.get("name") or "")
|
||||
for domain in self.capability_context.get("domains", [])
|
||||
for capability in domain.get("capabilities", [])
|
||||
if domain.get("availability") == "executable"
|
||||
or str(capability.get("name") or "")
|
||||
in self.native_can_runtime.supported_capabilities
|
||||
},
|
||||
fallback_intents=[
|
||||
parsed
|
||||
for _, parsed, status in clause_results
|
||||
if status != "complete"
|
||||
],
|
||||
trusted_local=trusted_local,
|
||||
)
|
||||
model = self.model_plan_adapter.adapt(
|
||||
guarded.plan,
|
||||
context,
|
||||
vehicle_profile,
|
||||
utterance=fallback_utterance,
|
||||
)
|
||||
model = self._mark_control_parse_source(
|
||||
model,
|
||||
model_parse_source,
|
||||
overwrite=True,
|
||||
)
|
||||
return (
|
||||
self._merge_local_and_model_intent(trusted_local, model),
|
||||
"local_rules+codex_app_server",
|
||||
"本地规则结果优先;Codex 结构化计划仅补足未解析子句,并已重新通过能力注册表校验。",
|
||||
)
|
||||
if provider == "local" or not fallback_clauses:
|
||||
return (
|
||||
(
|
||||
|
||||
@@ -62,21 +62,56 @@ _ACTION_SYNONYM_GROUPS = (
|
||||
("白色", "白灯", "白光", "white"),
|
||||
)
|
||||
|
||||
_RESERVED_VEHICLE_DEVICE_TERMS = {
|
||||
"空调",
|
||||
"座椅",
|
||||
"主驾座椅",
|
||||
"副驾座椅",
|
||||
"车窗",
|
||||
"车门",
|
||||
"后门",
|
||||
"遮阳帘",
|
||||
"氛围灯",
|
||||
"阅读灯",
|
||||
"室内灯",
|
||||
"出风口",
|
||||
"风口",
|
||||
}
|
||||
_RESERVED_VEHICLE_DEVICE_TERMS = frozenset(
|
||||
{
|
||||
"ac",
|
||||
"airconditioner",
|
||||
"ambientlight",
|
||||
"door",
|
||||
"interiorlight",
|
||||
"light",
|
||||
"readinglight",
|
||||
"seat",
|
||||
"shade",
|
||||
"window",
|
||||
"空调",
|
||||
"空调压缩机",
|
||||
"压缩机",
|
||||
"空调出风口",
|
||||
"座椅",
|
||||
"主驾座椅",
|
||||
"副驾座椅",
|
||||
"驾驶员座椅",
|
||||
"乘客座椅",
|
||||
"全车座椅",
|
||||
"车窗",
|
||||
"前排车窗",
|
||||
"后排车窗",
|
||||
"左前车窗",
|
||||
"右前车窗",
|
||||
"左后车窗",
|
||||
"右后车窗",
|
||||
"车门",
|
||||
"门锁",
|
||||
"后门",
|
||||
"后排车门",
|
||||
"遮阳帘",
|
||||
"天幕帘",
|
||||
"智能天幕",
|
||||
"天幕",
|
||||
"氛围灯",
|
||||
"阅读灯",
|
||||
"室内灯",
|
||||
"车内灯",
|
||||
"车内灯光",
|
||||
"灯光",
|
||||
"出风口",
|
||||
"风口",
|
||||
}
|
||||
)
|
||||
_VEHICLE_PHRASE_PRIORITY_TERMS = frozenset(
|
||||
term for term in _RESERVED_VEHICLE_DEVICE_TERMS if not term.isascii()
|
||||
)
|
||||
|
||||
|
||||
class GatewayError(RuntimeError):
|
||||
@@ -186,7 +221,9 @@ class InfraredSlotCatalog:
|
||||
"slot": int(row["slot"]),
|
||||
"device_id": str(row["device_id"] or ""),
|
||||
"name": str(row["name"]),
|
||||
"object_names": cls._stored_names(row["object_names_json"]),
|
||||
"object_names": _safe_peripheral_identity_names(
|
||||
cls._stored_names(row["object_names_json"])
|
||||
),
|
||||
"action_names": cls._stored_names(row["action_names_json"]),
|
||||
"source": str(row["source"]),
|
||||
"learned_at": str(row["learned_at"]),
|
||||
@@ -263,8 +300,10 @@ class InfraredSlotCatalog:
|
||||
r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}", device_id
|
||||
):
|
||||
continue
|
||||
object_names = self._stored_names(
|
||||
json.dumps(capability.get("object_names") or [])
|
||||
object_names = _safe_peripheral_identity_names(
|
||||
self._stored_names(
|
||||
json.dumps(capability.get("object_names") or [])
|
||||
)
|
||||
)
|
||||
action_names = self._stored_names(
|
||||
json.dumps(capability.get("action_names") or [])
|
||||
@@ -324,7 +363,20 @@ class InfraredSlotCatalog:
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(record, dict):
|
||||
capabilities.append(record)
|
||||
device_name = str(record.get("device_name") or "").strip()
|
||||
if not _peripheral_identity_name_is_safe(
|
||||
device_name,
|
||||
strip_display_index=True,
|
||||
):
|
||||
continue
|
||||
sanitized = dict(record)
|
||||
sanitized["device_aliases"] = _safe_peripheral_identity_names(
|
||||
record.get("device_aliases")
|
||||
)
|
||||
sanitized["object_names"] = _safe_peripheral_identity_names(
|
||||
record.get("object_names")
|
||||
)
|
||||
capabilities.append(sanitized)
|
||||
return capabilities
|
||||
|
||||
def sync_capabilities(
|
||||
@@ -355,9 +407,12 @@ class InfraredSlotCatalog:
|
||||
).fetchone()
|
||||
if slot_row is not None:
|
||||
stored_device_id = str(slot_row["device_id"] or "").strip()
|
||||
stored_objects = self._stored_names(
|
||||
raw_stored_objects = self._stored_names(
|
||||
slot_row["object_names_json"]
|
||||
)
|
||||
stored_objects = _safe_peripheral_identity_names(
|
||||
raw_stored_objects
|
||||
)
|
||||
stored_actions = self._stored_names(
|
||||
slot_row["action_names_json"]
|
||||
)
|
||||
@@ -370,6 +425,7 @@ class InfraredSlotCatalog:
|
||||
if (
|
||||
not stored_device_id
|
||||
or not stored_objects
|
||||
or stored_objects != raw_stored_objects
|
||||
or not stored_actions
|
||||
):
|
||||
connection.execute(
|
||||
@@ -464,6 +520,10 @@ class InfraredSlotCatalog:
|
||||
normalized_objects = _normalized_aliases(
|
||||
object_names, field="控制对象名称"
|
||||
)
|
||||
_validate_peripheral_identity_names(
|
||||
normalized_objects,
|
||||
field="控制对象名称",
|
||||
)
|
||||
normalized_actions = _normalized_aliases(
|
||||
action_names, field="执行动作"
|
||||
)
|
||||
@@ -769,6 +829,138 @@ def _normalized_aliases(
|
||||
return aliases[:24]
|
||||
|
||||
|
||||
def _identity_name_variants(value: Any, *, strip_display_index: bool = False) -> set[str]:
|
||||
raw_name = str(value or "").strip()
|
||||
variants = {_compact_match_text(raw_name)}
|
||||
if strip_display_index:
|
||||
variants.add(
|
||||
_compact_match_text(
|
||||
re.sub(r"^\s*#?\d+\s*[·.、:_-]*\s*", "", raw_name)
|
||||
)
|
||||
)
|
||||
variants.discard("")
|
||||
return variants
|
||||
|
||||
|
||||
def _peripheral_identity_name_is_safe(
|
||||
value: Any,
|
||||
*,
|
||||
strip_display_index: bool = False,
|
||||
) -> bool:
|
||||
"""Reject identities that can match a vehicle actuator without extra text."""
|
||||
|
||||
variants = _identity_name_variants(
|
||||
value,
|
||||
strip_display_index=strip_display_index,
|
||||
)
|
||||
if not variants:
|
||||
return False
|
||||
for identity in variants:
|
||||
contains_cjk = bool(re.search(r"[\u4e00-\u9fff]", identity))
|
||||
minimum_length = 2 if contains_cjk else 3
|
||||
if len(identity) < minimum_length or identity.isdigit():
|
||||
return False
|
||||
if any(identity in reserved for reserved in _RESERVED_VEHICLE_DEVICE_TERMS):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _validate_peripheral_identity_names(
|
||||
values: list[str],
|
||||
*,
|
||||
field: str,
|
||||
strip_display_index: bool = False,
|
||||
) -> None:
|
||||
for value in values:
|
||||
if not _peripheral_identity_name_is_safe(
|
||||
value,
|
||||
strip_display_index=strip_display_index,
|
||||
):
|
||||
raise GatewayError(
|
||||
f"{field}“{value}”过短或与车端设备保留名称冲突,"
|
||||
"请使用可唯一识别外设的完整名称。"
|
||||
)
|
||||
|
||||
|
||||
def _safe_peripheral_identity_names(value: Any) -> list[str]:
|
||||
"""Sanitize legacy persisted aliases instead of returning them to a router."""
|
||||
|
||||
if isinstance(value, str):
|
||||
value = re.split(r"[,,、;;|]", value)
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
names: list[str] = []
|
||||
for raw_name in value:
|
||||
name = str(raw_name or "").strip()
|
||||
if (
|
||||
name
|
||||
and name not in names
|
||||
and _peripheral_identity_name_is_safe(name)
|
||||
):
|
||||
names.append(name)
|
||||
return names[:24]
|
||||
|
||||
|
||||
def _capability_identity_variants(capability: dict[str, Any]) -> set[str]:
|
||||
variants: set[str] = set()
|
||||
raw_name = str(capability.get("device_name") or "")
|
||||
for candidate in _identity_name_variants(raw_name, strip_display_index=True):
|
||||
if _peripheral_identity_name_is_safe(candidate):
|
||||
variants.add(candidate)
|
||||
for field in ("device_aliases", "object_names"):
|
||||
values = capability.get(field, [])
|
||||
if isinstance(values, str):
|
||||
values = re.split(r"[,,、;;|]", values)
|
||||
if not isinstance(values, list):
|
||||
continue
|
||||
for value in values:
|
||||
if _peripheral_identity_name_is_safe(value):
|
||||
variants.update(_identity_name_variants(value))
|
||||
return variants
|
||||
|
||||
|
||||
def _has_unshadowed_vehicle_device_phrase(
|
||||
utterance: str,
|
||||
candidates: list[dict[str, Any]],
|
||||
) -> bool:
|
||||
"""Return true when a native vehicle phrase is not inside an explicit IR name."""
|
||||
|
||||
spans: list[tuple[int, int, str]] = []
|
||||
for capability in candidates:
|
||||
for identity in _capability_identity_variants(capability):
|
||||
start = utterance.find(identity)
|
||||
while start >= 0:
|
||||
spans.append((start, start + len(identity), identity))
|
||||
start = utterance.find(identity, start + 1)
|
||||
|
||||
for vehicle_term in _VEHICLE_PHRASE_PRIORITY_TERMS:
|
||||
start = utterance.find(vehicle_term)
|
||||
while start >= 0:
|
||||
end = start + len(vehicle_term)
|
||||
covered_by_explicit_peripheral = any(
|
||||
identity != vehicle_term
|
||||
and len(identity) > len(vehicle_term)
|
||||
and identity_start <= start
|
||||
and identity_end >= end
|
||||
for identity_start, identity_end, identity in spans
|
||||
)
|
||||
if not covered_by_explicit_peripheral:
|
||||
return True
|
||||
start = utterance.find(vehicle_term, start + 1)
|
||||
return False
|
||||
|
||||
|
||||
def _peripheral_execution_identity_authorized(
|
||||
utterance: str,
|
||||
capability: dict[str, Any],
|
||||
) -> bool:
|
||||
# Saved modes execute an immutable, previously validated command flow and
|
||||
# are rebound to the same live NFC/device/slot tuple before reaching here.
|
||||
if capability.get("parse_source") == "saved_mode_command_flow":
|
||||
return True
|
||||
return bool(_device_match_score(_compact_match_text(utterance), capability))
|
||||
|
||||
|
||||
def _normalize_peripheral_capability(value: Any) -> dict[str, Any]:
|
||||
"""Validate one browser-synchronized learned infrared capability."""
|
||||
|
||||
@@ -820,6 +1012,19 @@ def _normalize_peripheral_capability(value: Any) -> dict[str, Any]:
|
||||
action_names = _normalized_aliases(
|
||||
[action, *action_aliases], field="执行动作"
|
||||
)
|
||||
_validate_peripheral_identity_names(
|
||||
[device_name],
|
||||
field="设备名称",
|
||||
strip_display_index=True,
|
||||
)
|
||||
_validate_peripheral_identity_names(
|
||||
device_aliases,
|
||||
field="设备别名",
|
||||
)
|
||||
_validate_peripheral_identity_names(
|
||||
object_names,
|
||||
field="控制对象名称",
|
||||
)
|
||||
|
||||
return {
|
||||
"id": capability_id,
|
||||
@@ -843,26 +1048,7 @@ def _normalize_peripheral_capability(value: Any) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _device_match_score(utterance: str, capability: dict[str, Any]) -> int:
|
||||
raw_name = str(capability.get("device_name") or "")
|
||||
raw_aliases = capability.get("device_aliases", [])
|
||||
if isinstance(raw_aliases, str):
|
||||
raw_aliases = re.split(r"[,,、;;|]", raw_aliases)
|
||||
if not isinstance(raw_aliases, list):
|
||||
raw_aliases = []
|
||||
object_names = capability.get("object_names", [])
|
||||
if isinstance(object_names, str):
|
||||
object_names = re.split(r"[,,、;;|]", object_names)
|
||||
if not isinstance(object_names, list):
|
||||
object_names = []
|
||||
name_variants = {
|
||||
_compact_match_text(raw_name),
|
||||
_compact_match_text(
|
||||
re.sub(r"^\s*#?\d+\s*[·.、:_-]*\s*", "", raw_name)
|
||||
),
|
||||
*(_compact_match_text(alias) for alias in raw_aliases),
|
||||
*(_compact_match_text(name) for name in object_names),
|
||||
}
|
||||
name_variants.difference_update(_RESERVED_VEHICLE_DEVICE_TERMS)
|
||||
name_variants = _capability_identity_variants(capability)
|
||||
return max(
|
||||
(len(name) for name in name_variants if name and name in utterance),
|
||||
default=0,
|
||||
@@ -917,7 +1103,27 @@ def _deterministic_peripheral_steps(
|
||||
]
|
||||
if not compact_utterance or len(learned) < 2:
|
||||
return []
|
||||
if len({str(item.get("device_id") or "") for item in learned}) != 1:
|
||||
explicit = [
|
||||
item for item in learned if _device_match_score(compact_utterance, item)
|
||||
]
|
||||
if not explicit or _has_unshadowed_vehicle_device_phrase(
|
||||
compact_utterance,
|
||||
explicit,
|
||||
):
|
||||
return []
|
||||
explicit_device_ids = {
|
||||
str(item.get("device_id") or "") for item in explicit
|
||||
}
|
||||
if len(explicit_device_ids) != 1:
|
||||
return []
|
||||
selected_device_id = next(iter(explicit_device_ids))
|
||||
learned = [
|
||||
item
|
||||
for item in learned
|
||||
if str(item.get("device_id") or "") == selected_device_id
|
||||
and _capability_identity_variants(item)
|
||||
]
|
||||
if len(learned) < 2:
|
||||
return []
|
||||
|
||||
events: list[tuple[int, int, dict[str, Any]]] = []
|
||||
@@ -1084,6 +1290,11 @@ def _resolve_bound_peripheral_command(
|
||||
|
||||
if not matches:
|
||||
return None
|
||||
if _has_unshadowed_vehicle_device_phrase(
|
||||
compact_utterance,
|
||||
[item[2] for item in matches],
|
||||
):
|
||||
return None
|
||||
matches.sort(key=lambda item: (item[0], item[1]), reverse=True)
|
||||
best_score = matches[0][:2]
|
||||
best_matches = [item for item in matches if item[:2] == best_score]
|
||||
@@ -1182,6 +1393,14 @@ def partition_active_peripheral_intent(
|
||||
for item in explicit_bound_candidates
|
||||
if item.get("enabled", True) is not False
|
||||
]
|
||||
if explicit_bound_candidates and _has_unshadowed_vehicle_device_phrase(
|
||||
compact_clause,
|
||||
explicit_bound_candidates,
|
||||
):
|
||||
residual.append(clause)
|
||||
contextual_device_id = None
|
||||
contextual_device_name = ""
|
||||
continue
|
||||
clause_candidates = explicit_candidates
|
||||
bound_clause_candidates = explicit_bound_candidates
|
||||
resolution_text = clause
|
||||
@@ -1216,7 +1435,7 @@ def partition_active_peripheral_intent(
|
||||
else:
|
||||
mentions_vehicle_device = any(
|
||||
_compact_match_text(term) in compact_clause
|
||||
for term in _RESERVED_VEHICLE_DEVICE_TERMS
|
||||
for term in _VEHICLE_PHRASE_PRIORITY_TERMS
|
||||
)
|
||||
requires_ordered_plan = bool(
|
||||
re.search(
|
||||
@@ -1357,6 +1576,8 @@ def active_peripheral_plan_catalog(
|
||||
item["slot"] = validate_slot(item.get("slot"))
|
||||
item["card_uid"] = active_uid
|
||||
active.append(item)
|
||||
if _has_unshadowed_vehicle_device_phrase(compact_utterance, active):
|
||||
return []
|
||||
return active
|
||||
|
||||
|
||||
@@ -1805,6 +2026,7 @@ class TcpGateway(BaseGateway):
|
||||
"gateway_acknowledged": False,
|
||||
"physical_state_confirmed": False,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
class MqttGateway(BaseGateway):
|
||||
@@ -2168,6 +2390,10 @@ class SelfExtensionService:
|
||||
) -> dict[str, Any]:
|
||||
"""Execute a live-NFC peripheral match through the main request API."""
|
||||
|
||||
if not _peripheral_execution_identity_authorized(utterance, capability):
|
||||
raise ExtensionIntegrationError(
|
||||
"外设名称未在原始指令中被明确提及,已拒绝红外执行。"
|
||||
)
|
||||
slot = validate_slot(capability.get("slot"))
|
||||
request_id = f"req-extension-{uuid.uuid4().hex}"
|
||||
conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
|
||||
@@ -2374,6 +2600,10 @@ class SelfExtensionService:
|
||||
raise ExtensionIntegrationError(
|
||||
"GPT 计划引用了不在当前 NFC 目录中的能力。"
|
||||
)
|
||||
if not _peripheral_execution_identity_authorized(utterance, capability):
|
||||
raise ExtensionIntegrationError(
|
||||
"GPT 计划引用的外设未在原始指令中被明确提及。"
|
||||
)
|
||||
slot = validate_slot(capability.get("slot"))
|
||||
operation_ref = f"{capability_id}-{index + 1}"
|
||||
compiled_peripheral_steps[index] = (
|
||||
@@ -2701,6 +2931,13 @@ class SelfExtensionService:
|
||||
peripherals = [
|
||||
item for item in peripherals[:MAX_CATALOG_ITEMS] if isinstance(item, dict)
|
||||
]
|
||||
compact_utterance = _compact_match_text(utterance)
|
||||
if _has_unshadowed_vehicle_device_phrase(compact_utterance, peripherals):
|
||||
peripherals = [
|
||||
item
|
||||
for item in peripherals
|
||||
if _device_match_score(compact_utterance, item)
|
||||
]
|
||||
deterministic_steps = _deterministic_peripheral_steps(utterance, peripherals)
|
||||
if deterministic_steps and not vehicle_devices:
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
@@ -39,6 +41,96 @@ DRIVER_SEAT_THERMAL_MIDDLE = re.compile(
|
||||
r"(?:中等(?:档|级)?|中档|中级|中间档|适中)"
|
||||
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)"
|
||||
)
|
||||
META_NON_EXECUTION = re.compile(
|
||||
r"复述|引用|翻译|改写|总结|解释|分析|造句|"
|
||||
r"故事|剧本|小说|作文|角色|主角|假设|假如|如果|"
|
||||
r"示例|举例|这句话|以下内容"
|
||||
)
|
||||
_EXECUTION_LIST_FIELDS = (
|
||||
"actions",
|
||||
"domain_actions",
|
||||
"native_can_actions",
|
||||
"deferred_requests",
|
||||
"model_requests",
|
||||
"preset_actions",
|
||||
"preset_events",
|
||||
)
|
||||
_SEAT_CONTEXT = re.compile(
|
||||
r"座椅|座位|靠背|椅背|坐盆|坐垫|腿托|脚托|"
|
||||
r"座椅通风|座椅加热|按摩|座椅记忆|零重力|木马|摇篮"
|
||||
)
|
||||
_SEAT_DRIVER_SCOPE = re.compile(
|
||||
r"\bdriver\b|\bfront[-_ ]?left\b|主驾|主驾驶|驾驶位|驾驶员|"
|
||||
r"驾驶席|驾驶座(?:椅)?|司机位|司机座(?:椅)?|左前"
|
||||
)
|
||||
_SEAT_PASSENGER_SCOPE = re.compile(
|
||||
r"\bpassenger\b|\bfront[-_ ]?right\b|副驾|副驾驶|乘客|乘员|右前"
|
||||
)
|
||||
_SEAT_UNSUPPORTED_SCOPE = re.compile(
|
||||
r"前排|后排|后座|二排|第二排|中排|第三排|三排|"
|
||||
r"左后|右后|前舱|后舱|左侧|右侧|左边|右边|"
|
||||
r"所有座椅|全部座椅|全车座椅"
|
||||
)
|
||||
|
||||
|
||||
def resolve_explicit_seat_scopes(
|
||||
utterance: str,
|
||||
*,
|
||||
assume_seat_context: bool = False,
|
||||
) -> frozenset[str] | None:
|
||||
"""Resolve scopes only from clauses that actually describe a seat action."""
|
||||
|
||||
scopes: set[str] = set()
|
||||
relevant_clause_seen = False
|
||||
clauses = [item for item in CLAUSE_SEPARATOR.split(utterance) if item.strip()]
|
||||
for clause in clauses or [utterance]:
|
||||
if not assume_seat_context and not _SEAT_CONTEXT.search(clause):
|
||||
continue
|
||||
relevant_clause_seen = True
|
||||
if _SEAT_UNSUPPORTED_SCOPE.search(clause):
|
||||
scopes.add("unsupported")
|
||||
continue
|
||||
if _SEAT_DRIVER_SCOPE.search(clause):
|
||||
scopes.add("driver")
|
||||
if _SEAT_PASSENGER_SCOPE.search(clause):
|
||||
scopes.add("passenger")
|
||||
if not relevant_clause_seen or not scopes:
|
||||
return None
|
||||
return frozenset(scopes)
|
||||
|
||||
|
||||
def _filter_explicit_seat_scope(
|
||||
result: dict[str, Any],
|
||||
requested_scopes: frozenset[str],
|
||||
) -> bool:
|
||||
"""Remove seat operations outside the user's explicit scope."""
|
||||
|
||||
allowed: set[str] = set()
|
||||
if "driver" in requested_scopes:
|
||||
allowed.update(("driver", "front_left"))
|
||||
if "passenger" in requested_scopes:
|
||||
allowed.update(("passenger", "front_right"))
|
||||
if "unsupported" in requested_scopes:
|
||||
allowed.clear()
|
||||
removed = False
|
||||
for key in _EXECUTION_LIST_FIELDS:
|
||||
kept: list[Any] = []
|
||||
for item in result.get(key, []):
|
||||
if not isinstance(item, dict):
|
||||
kept.append(item)
|
||||
continue
|
||||
capability = str(item.get("capability", ""))
|
||||
is_seat = item.get("domain") == "seat" or capability.startswith("seat.")
|
||||
if not is_seat:
|
||||
kept.append(item)
|
||||
continue
|
||||
target = str(item.get("target", item.get("target_scope", "")))
|
||||
if target in allowed:
|
||||
kept.append(item)
|
||||
else:
|
||||
removed = True
|
||||
result[key] = kept
|
||||
return removed
|
||||
|
||||
|
||||
def _span(utterance: str, match: re.Match[str]) -> dict[str, Any]:
|
||||
@@ -145,6 +237,125 @@ def _remove_capability(parsed: dict[str, Any], capability: str) -> None:
|
||||
]
|
||||
|
||||
|
||||
def _parameter_mapping(item: Mapping[str, Any]) -> dict[str, Any]:
|
||||
parameters: object = item.get("parameters", {})
|
||||
if isinstance(parameters, list):
|
||||
return {
|
||||
str(parameter.get("name")): parameter.get("value")
|
||||
for parameter in parameters
|
||||
if isinstance(parameter, dict) and parameter.get("name")
|
||||
}
|
||||
if isinstance(parameters, Mapping):
|
||||
normalized = {str(name): value for name, value in parameters.items()}
|
||||
if normalized:
|
||||
return normalized
|
||||
|
||||
# The passenger-seat domain predates the capability-shaped actions used by
|
||||
# the other domains. Normalize its legacy action fields here so the shared
|
||||
# conflict gate runs before FunctionConfig compilation and execution.
|
||||
legacy: dict[str, Any] = {}
|
||||
for source, target in (
|
||||
("state", "state"),
|
||||
("requested_level", "level"),
|
||||
("position_pct", "position_pct"),
|
||||
("angle_deg", "angle_deg"),
|
||||
):
|
||||
if item.get(source) is not None:
|
||||
legacy[target] = item.get(source)
|
||||
return legacy
|
||||
|
||||
|
||||
def _control_capability(item: Mapping[str, Any]) -> str:
|
||||
capability = str(item.get("capability", ""))
|
||||
if capability:
|
||||
return capability
|
||||
function = str(item.get("function", ""))
|
||||
if function in {"ventilation", "heating"}:
|
||||
return f"seat.set_{function}"
|
||||
if item.get("action_type") == "position":
|
||||
return {
|
||||
"longitudinal": "seat.set_longitudinal",
|
||||
"cushion_tilt": "seat.set_cushion_tilt",
|
||||
"backrest_recline": "seat.set_backrest_recline",
|
||||
"legrest_height": "seat.set_legrest_height",
|
||||
}.get(str(item.get("axis", "")), "")
|
||||
return ""
|
||||
|
||||
|
||||
def _control_conflict_key(
|
||||
item: Mapping[str, Any],
|
||||
parameters: Mapping[str, Any],
|
||||
) -> tuple[str, str, str, str]:
|
||||
target: object = item.get("target", item.get("target_scope", ""))
|
||||
if isinstance(target, Mapping):
|
||||
target = target.get("zone", target.get("device", ""))
|
||||
capability = _control_capability(item)
|
||||
domain = str(item.get("domain", "")) or (
|
||||
capability.split(".", 1)[0] if "." in capability else ""
|
||||
)
|
||||
selector = ""
|
||||
if capability == "seat.set_driver_axis_native":
|
||||
selector = f"axis:{parameters.get('axis', '')}"
|
||||
elif capability == "seat.set_driver_massage_native":
|
||||
selector = f"action:{parameters.get('action', '')}"
|
||||
return domain, capability, str(target), selector
|
||||
|
||||
|
||||
def _conflicting_control_keys(
|
||||
result: dict[str, Any],
|
||||
) -> set[tuple[str, str, str, str]]:
|
||||
"""Find contradictory values, while allowing complementary subcommands.
|
||||
|
||||
Some capabilities intentionally emit more than one operation for the same
|
||||
target: massage mode plus strength, or ambient colour plus brightness.
|
||||
Those parameter sets are complementary. A conflict exists only when the
|
||||
same setting parameter receives multiple values. ``action`` and ``axis``
|
||||
select a subcommand and therefore are not themselves setting values.
|
||||
"""
|
||||
|
||||
values: dict[tuple[str, str, str, str], dict[str, set[str]]] = {}
|
||||
# ``model_requests`` is audit metadata and can still contain a built-in
|
||||
# mode's superseded defaults. The executable action lists below have
|
||||
# already applied explicit-user-value precedence, so conflict decisions
|
||||
# must be based on those lists only.
|
||||
for field in ("actions", "domain_actions", "native_can_actions"):
|
||||
for item in result.get(field, []):
|
||||
if not isinstance(item, dict) or not _control_capability(item):
|
||||
continue
|
||||
parameters = _parameter_mapping(item)
|
||||
if not parameters:
|
||||
continue
|
||||
key = _control_conflict_key(item, parameters)
|
||||
settings = values.setdefault(key, {})
|
||||
for name, value in parameters.items():
|
||||
if str(name) in {"action", "axis"}:
|
||||
continue
|
||||
settings.setdefault(str(name), set()).add(
|
||||
json.dumps(value, ensure_ascii=False, sort_keys=True, default=str)
|
||||
)
|
||||
return {
|
||||
key
|
||||
for key, settings in values.items()
|
||||
if any(len(candidates) > 1 for candidates in settings.values())
|
||||
}
|
||||
|
||||
|
||||
def _remove_conflicting_controls(
|
||||
result: dict[str, Any],
|
||||
conflicts: set[tuple[str, str, str, str]],
|
||||
) -> None:
|
||||
for field in _EXECUTION_LIST_FIELDS:
|
||||
kept: list[Any] = []
|
||||
for item in result.get(field, []):
|
||||
if not isinstance(item, dict) or not _control_capability(item):
|
||||
kept.append(item)
|
||||
continue
|
||||
key = _control_conflict_key(item, _parameter_mapping(item))
|
||||
if key not in conflicts:
|
||||
kept.append(item)
|
||||
result[field] = kept
|
||||
|
||||
|
||||
def apply_language_safety_guards(parsed: dict[str, Any], utterance: str) -> dict[str, Any]:
|
||||
"""Preserve negation and unresolved control language without producing CAN."""
|
||||
|
||||
@@ -152,6 +363,60 @@ def apply_language_safety_guards(parsed: dict[str, Any], utterance: str) -> dict
|
||||
constraints = list(result.get("semantic_constraints", []))
|
||||
unresolved = list(result.get("unresolved_spans", []))
|
||||
|
||||
meta_match = META_NON_EXECUTION.search(utterance)
|
||||
if meta_match is not None:
|
||||
for key in _EXECUTION_LIST_FIELDS:
|
||||
result[key] = []
|
||||
constraints.append(
|
||||
{
|
||||
"type": "prohibit_meta_language_execution",
|
||||
"source_span": _span(utterance, meta_match),
|
||||
}
|
||||
)
|
||||
unresolved.append(
|
||||
{
|
||||
"text": utterance,
|
||||
"reason": "meta_language_is_not_vehicle_authorization",
|
||||
}
|
||||
)
|
||||
result["ambiguities"] = []
|
||||
result["decision_summary"] = [
|
||||
*result.get("decision_summary", []),
|
||||
"检测到复述、翻译或创作类元语言;未将其中的控制词当作车辆授权。",
|
||||
]
|
||||
result["confidence"] = min(float(result.get("confidence", 0)), 0.8)
|
||||
|
||||
explicit_seat_scopes = resolve_explicit_seat_scopes(utterance)
|
||||
if explicit_seat_scopes is not None:
|
||||
removed = _filter_explicit_seat_scope(result, explicit_seat_scopes)
|
||||
if removed:
|
||||
constraints.append(
|
||||
{
|
||||
"type": "restrict_explicit_seat_scope",
|
||||
"domain": "seat",
|
||||
"target": ",".join(sorted(explicit_seat_scopes)),
|
||||
}
|
||||
)
|
||||
result["decision_summary"] = [
|
||||
*result.get("decision_summary", []),
|
||||
"已移除与用户显式座位不一致的座椅动作,未扩大控制范围。",
|
||||
]
|
||||
if "unsupported" in explicit_seat_scopes:
|
||||
unresolved.append(
|
||||
{
|
||||
"text": utterance,
|
||||
"reason": "explicit_seat_scope_is_not_safely_callable",
|
||||
}
|
||||
)
|
||||
result["ambiguities"] = [
|
||||
*result.get("ambiguities", []),
|
||||
{
|
||||
"field": "target_scope",
|
||||
"question": "当前请求的座位范围不能在不扩大目标的前提下执行。",
|
||||
"reason": "explicit_seat_scope_is_not_safely_callable",
|
||||
},
|
||||
]
|
||||
|
||||
for match in PRESERVE_SEAT.finditer(utterance):
|
||||
target = "driver" if match.group("target") == "主驾" else "passenger"
|
||||
constraints.append(
|
||||
@@ -167,10 +432,7 @@ def apply_language_safety_guards(parsed: dict[str, Any], utterance: str) -> dict
|
||||
negated = list(NEGATED_OPERATION.finditer(utterance))
|
||||
if negated:
|
||||
constraints.extend(_constraint(utterance, item) for item in negated)
|
||||
for key in (
|
||||
"actions", "domain_actions", "native_can_actions", "deferred_requests",
|
||||
"model_requests", "preset_actions", "preset_events",
|
||||
):
|
||||
for key in _EXECUTION_LIST_FIELDS:
|
||||
result[key] = []
|
||||
result["ambiguities"] = []
|
||||
if len(CONTROL_VERB.findall(utterance)) > len(negated):
|
||||
@@ -297,6 +559,38 @@ def apply_language_safety_guards(parsed: dict[str, Any], utterance: str) -> dict
|
||||
}
|
||||
)
|
||||
|
||||
conflicts = _conflicting_control_keys(result)
|
||||
if conflicts:
|
||||
_remove_conflicting_controls(result, conflicts)
|
||||
for domain, capability, target, selector in sorted(conflicts):
|
||||
constraint = {
|
||||
"type": "reject_conflicting_absolute_values",
|
||||
"domain": domain,
|
||||
"capability": capability,
|
||||
"target": target,
|
||||
}
|
||||
if selector:
|
||||
constraint["selector"] = selector
|
||||
constraints.append(constraint)
|
||||
unresolved.append(
|
||||
{
|
||||
"text": utterance,
|
||||
"reason": "conflicting_values_for_same_capability_target",
|
||||
}
|
||||
)
|
||||
result["ambiguities"] = [
|
||||
*result.get("ambiguities", []),
|
||||
{
|
||||
"field": "parameters",
|
||||
"question": "同一设备目标包含互相冲突的设定值,已停止相关操作,请明确唯一值。",
|
||||
"reason": "conflicting_values_for_same_capability_target",
|
||||
},
|
||||
]
|
||||
result["decision_summary"] = [
|
||||
*result.get("decision_summary", []),
|
||||
"检测到同一能力与目标的冲突值,相关动作均未下发。",
|
||||
]
|
||||
|
||||
result["semantic_constraints"] = constraints
|
||||
result["unresolved_spans"] = unresolved
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user