优化后端和前端
This commit is contained in:
@@ -567,14 +567,19 @@ class ConversationStateStore:
|
||||
device = state["domain_devices"][domain]
|
||||
|
||||
if domain == "climate":
|
||||
# The trusted CAN command vocabulary uses the concise token
|
||||
# ``rear`` while the conversation state stores that same HVAC
|
||||
# zone as ``rear_row``. Keep the public command stable and
|
||||
# normalize only at the state boundary.
|
||||
zone_target = "rear_row" if target == "rear" else target
|
||||
if capability == "climate.set_power":
|
||||
device["zones"][target]["power"] = parameters["state"]
|
||||
device["zones"][zone_target]["power"] = parameters["state"]
|
||||
if target == "vehicle":
|
||||
device["power"] = parameters["state"]
|
||||
for zone in device["zones"].values():
|
||||
zone["power"] = parameters["state"]
|
||||
elif capability == "climate.set_temperature":
|
||||
device["zones"][target]["temperature_c"] = parameters[
|
||||
device["zones"][zone_target]["temperature_c"] = parameters[
|
||||
"temperature_c"
|
||||
]
|
||||
if target == "vehicle":
|
||||
@@ -589,7 +594,7 @@ class ConversationStateStore:
|
||||
elif capability == "climate.set_front_defrost":
|
||||
device["front_defrost"] = parameters["state"]
|
||||
elif capability == "climate.set_outlet_mode":
|
||||
device["zones"][target]["outlet_mode"] = parameters["mode"]
|
||||
device["zones"][zone_target]["outlet_mode"] = parameters["mode"]
|
||||
elif capability == "climate.set_control_mode":
|
||||
device["control_mode"] = parameters["mode"]
|
||||
device["feedback_source"] = feedback_source
|
||||
|
||||
@@ -78,6 +78,9 @@ class ConfiguredDomainRuntime:
|
||||
"zone": action["target"],
|
||||
},
|
||||
"parameters": dict(action["parameters"]),
|
||||
"parse_source": str(
|
||||
action.get("parse_source", "local_rule")
|
||||
),
|
||||
"timeout_ms": 8000 if self.domain == "roof" else 4000,
|
||||
"failure_policy": "abort",
|
||||
}
|
||||
|
||||
@@ -25,6 +25,16 @@ WINDOW_TARGETS = {
|
||||
"后左": "rear_left",
|
||||
"右后": "rear_right",
|
||||
"后右": "rear_right",
|
||||
# Natural cabin language names the front windows by occupant more often
|
||||
# than by left/right. These aliases belong only to the window domain:
|
||||
# driver = left-front, passenger = right-front.
|
||||
"主驾驶": "front_left",
|
||||
"驾驶位": "front_left",
|
||||
"司机位": "front_left",
|
||||
"主驾": "front_left",
|
||||
"副驾驶": "front_right",
|
||||
"乘客位": "front_right",
|
||||
"副驾": "front_right",
|
||||
}
|
||||
WINDOW_GROUP_MEMBERS = {
|
||||
"all": ["front_left", "front_right", "rear_left", "rear_right"],
|
||||
@@ -49,6 +59,7 @@ LIGHT_TARGETS = {
|
||||
"左后": "rear_left",
|
||||
"右后": "rear_right",
|
||||
}
|
||||
CLAUSE_SEPARATOR = re.compile(r"[,,;;。!?!?\n]+")
|
||||
|
||||
# Defaults are intentionally scoped to the driver console and verified CH1
|
||||
# body-CAN vocabulary. A missing target must never become the legacy
|
||||
@@ -83,7 +94,8 @@ CH1_DEFAULT_TARGETS = {
|
||||
|
||||
_EXPLICIT_TARGET_PATTERNS = {
|
||||
"window": re.compile(
|
||||
r"(?:全部|所有|四个|全车|前排|后排|左前|前左|右前|前右|左后|后左|右后|后右)"
|
||||
r"(?:全部|所有|四个|全车|前排|后排|左前|前左|右前|前右|左后|后左|右后|后右|"
|
||||
r"主驾驶|驾驶位|司机位|主驾|副驾驶|乘客位|副驾)"
|
||||
r"(?:的)?(?:车窗|窗)|window\s+(?:all|front|rear|front-left|front-right|rear-left|rear-right)",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
@@ -113,6 +125,88 @@ _EXPLICIT_TARGET_PATTERNS = {
|
||||
}
|
||||
|
||||
|
||||
def _scoped_text(
|
||||
compact: str,
|
||||
subject_pattern: str,
|
||||
continuation_pattern: str | None = None,
|
||||
) -> str:
|
||||
"""Keep action words inside clauses that name the controlled device."""
|
||||
|
||||
clauses = [clause for clause in CLAUSE_SEPARATOR.split(compact) if clause]
|
||||
subject_indexes = {
|
||||
index
|
||||
for index, clause in enumerate(clauses)
|
||||
if re.search(subject_pattern, clause, re.IGNORECASE)
|
||||
}
|
||||
selected_indexes = set(subject_indexes)
|
||||
if continuation_pattern:
|
||||
selected_indexes.update(
|
||||
index + 1
|
||||
for index in subject_indexes
|
||||
if index + 1 < len(clauses)
|
||||
and re.search(
|
||||
continuation_pattern,
|
||||
clauses[index + 1],
|
||||
re.IGNORECASE,
|
||||
)
|
||||
)
|
||||
return ",".join(
|
||||
clause
|
||||
for index, clause in enumerate(clauses)
|
||||
if index in selected_indexes
|
||||
)
|
||||
|
||||
|
||||
def _window_targets(text: str) -> tuple[list[str], bool]:
|
||||
"""Resolve target aliases only when they are attached to a window noun."""
|
||||
|
||||
explicit: list[str] = []
|
||||
broad: str | None = None
|
||||
target_labels = "|".join(
|
||||
re.escape(label)
|
||||
for label in sorted(WINDOW_TARGETS, key=len, reverse=True)
|
||||
)
|
||||
for label, target in WINDOW_TARGETS.items():
|
||||
attached = bool(
|
||||
re.search(
|
||||
rf"{re.escape(label)}"
|
||||
rf"(?:(?:和|与|及|、|以及)(?:{target_labels}))*"
|
||||
rf"(?:的)?(?:两个|两侧)?(?:车窗|窗)",
|
||||
text,
|
||||
)
|
||||
)
|
||||
if not attached:
|
||||
continue
|
||||
if target in WINDOW_GROUP_MEMBERS:
|
||||
broad = broad or target
|
||||
elif target not in explicit:
|
||||
explicit.append(target)
|
||||
|
||||
exclusions = {
|
||||
target
|
||||
for label, target in WINDOW_TARGETS.items()
|
||||
if target not in WINDOW_GROUP_MEMBERS
|
||||
and re.search(
|
||||
rf"{re.escape(label)}(?:车窗)?(?:除外|除去|不包括|不含)",
|
||||
text,
|
||||
)
|
||||
}
|
||||
if broad and exclusions:
|
||||
return (
|
||||
[
|
||||
target
|
||||
for target in WINDOW_GROUP_MEMBERS[broad]
|
||||
if target not in exclusions
|
||||
],
|
||||
True,
|
||||
)
|
||||
if explicit:
|
||||
return ([target for target in explicit if target not in exclusions], True)
|
||||
if broad:
|
||||
return ([broad], True)
|
||||
return (["front_left"], False)
|
||||
|
||||
|
||||
def _capability_domain(capability: str) -> str:
|
||||
return "interior_light" if capability.startswith("interior_light.") else capability.split(".", 1)[0]
|
||||
|
||||
@@ -156,6 +250,7 @@ def _action(
|
||||
"operation": "set",
|
||||
"reason": reason,
|
||||
"execution_backend": "native_can",
|
||||
"parse_source": "local_rule",
|
||||
}
|
||||
|
||||
|
||||
@@ -284,6 +379,8 @@ def _parse_exact_command(text: str) -> list[dict[str, Any]]:
|
||||
}
|
||||
if direction not in valid_directions[axis]:
|
||||
return []
|
||||
if direction == "stop" and duration_token is not None:
|
||||
return []
|
||||
duration = (
|
||||
500
|
||||
if direction == "stop"
|
||||
@@ -381,17 +478,23 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
|
||||
|
||||
actions: list[dict[str, Any]] = []
|
||||
memory = re.search(
|
||||
r"(?:记忆(?:位置|位)?|记忆座椅(?:位置)?|座椅记忆(?:位置)?|位置记忆)"
|
||||
r"(?:"
|
||||
r"(?:记忆(?:位置|位)?|预设(?:位置)?|记忆座椅(?:位置)?|"
|
||||
r"座椅记忆(?:位置)?|位置记忆)"
|
||||
r"(?:切换到|切换为|调整到|调到|唤醒|调用|恢复到|恢复为|设为|设置为)?"
|
||||
r"([1-3])(?:号|档|位)?",
|
||||
r"(?P<after>[1-3一二三])(?:号|档|位)?"
|
||||
r"|(?P<before>[1-3一二三])号?(?:记忆|预设)(?:位置|位)?"
|
||||
r")",
|
||||
compact,
|
||||
)
|
||||
if memory is not None:
|
||||
slot_token = memory.group("after") or memory.group("before")
|
||||
slot = {"一": 1, "二": 2, "三": 3}.get(slot_token, slot_token)
|
||||
actions.append(_action(
|
||||
"seat",
|
||||
"seat.recall_driver_memory_native",
|
||||
"seat.set_driver_position_native",
|
||||
"driver",
|
||||
{"slot": int(memory[1])},
|
||||
{"slot": int(slot)},
|
||||
(
|
||||
"用户明确指定了 CH1 主驾座椅记忆位置。"
|
||||
if explicit_driver
|
||||
@@ -449,8 +552,10 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
|
||||
strength = (
|
||||
1
|
||||
if re.search(r"弱|低|1档", compact)
|
||||
else 2
|
||||
if re.search(r"中等|中档|2档", compact)
|
||||
else 3
|
||||
if re.search(r"强|高|3档", compact)
|
||||
if re.search(r"强(?!度)|高|3档", compact)
|
||||
else 2
|
||||
)
|
||||
massage = {"action": "strength", "strength": strength}
|
||||
@@ -549,103 +654,149 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
|
||||
|
||||
def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
compact = re.sub(r"\s+", "", text)
|
||||
actions: list[dict[str, Any]] = _parse_chinese_driver_seat_commands(compact)
|
||||
seat_text = _scoped_text(
|
||||
compact,
|
||||
r"座椅|座位|靠背|椅背|坐盆|坐垫|腿托|脚托|按摩|记忆|预设|"
|
||||
r"通风|加热|前移|后移|向前移动|向后移动",
|
||||
)
|
||||
actions: list[dict[str, Any]] = _parse_chinese_driver_seat_commands(
|
||||
seat_text
|
||||
)
|
||||
|
||||
climate_text = _scoped_text(
|
||||
compact,
|
||||
r"空调|温度|风量|风速|内循环|外循环|循环模式|吹面|吹脸|吹脚|除霜|(?:AC|A/C)",
|
||||
)
|
||||
|
||||
if (
|
||||
"空调" in compact
|
||||
"空调" in climate_text
|
||||
or re.search(r"(?:AC|A/C)", text, re.IGNORECASE)
|
||||
or re.search(r"温度|风量|风速|内循环|外循环|循环模式", compact)
|
||||
or re.search(
|
||||
r"温度|风量|风速|内循环|外循环|循环模式|吹面|吹脸|吹脚|除霜",
|
||||
climate_text,
|
||||
)
|
||||
):
|
||||
if re.search(r"(?:打开|开启|启动)空调", compact):
|
||||
if re.search(r"(?:打开|开启|启动)空调", climate_text):
|
||||
actions.append(_action("climate", "climate.set_power", "vehicle", {"state": "on"}, "用户要求打开空调。"))
|
||||
elif re.search(r"(?:关闭|关掉|关上)空调", compact):
|
||||
elif re.search(r"(?:关闭|关掉|关上)空调", climate_text):
|
||||
actions.append(_action("climate", "climate.set_power", "vehicle", {"state": "off"}, "用户要求关闭空调。"))
|
||||
|
||||
temperature = re.search(
|
||||
r"(?:(主驾|副驾|后排)(?:空调)?(?:温度)?|空调(?:温度)?|温度)(?:调到|设为|设置为|到)?(1[89]|2\d|3[0-2])(?:℃|度)?",
|
||||
compact,
|
||||
r"(?:空调(?:温度)?|温度)(?:都)?"
|
||||
r"(?:调整到|调整为|调到|调为|设为|设置为|调至|到|为)?"
|
||||
r"(1[89]|2\d|3[0-2])(?:摄氏)?(?:℃|度)?(?![\d.])",
|
||||
climate_text,
|
||||
)
|
||||
if temperature:
|
||||
target = {"主驾": "driver", "副驾": "passenger", "后排": "rear"}.get(temperature[1], "driver")
|
||||
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 temperature[1]
|
||||
if explicit_temperature_target
|
||||
else "用户未指定空调温区,按驾驶台默认选择主驾温区。"
|
||||
)
|
||||
actions.append(_action("climate", "climate.set_temperature", target, {"temperature_c": int(temperature[2])}, reason))
|
||||
actions.extend(
|
||||
_action(
|
||||
"climate",
|
||||
"climate.set_temperature",
|
||||
target,
|
||||
{"temperature_c": int(temperature[1])},
|
||||
reason,
|
||||
)
|
||||
for target in temperature_targets
|
||||
)
|
||||
|
||||
fan = re.search(r"(?:风量|风速)(?:调到|设为|设置为|到)?([1-7])(?:档)?", compact)
|
||||
fan = re.search(
|
||||
r"(?:风量|风速)(?:调整到|调整为|调到|调为|设为|设置为|到)?"
|
||||
r"([1-7])(?:档|级)?(?![\d.])",
|
||||
climate_text,
|
||||
)
|
||||
if fan:
|
||||
actions.append(_action("climate", "climate.set_fan_level", "vehicle", {"level": int(fan[1])}, "用户指定了空调风量。"))
|
||||
|
||||
if re.search(r"自动(?:控制|模式)", compact):
|
||||
if re.search(r"自动(?:控制|模式)", climate_text):
|
||||
actions.append(_action("climate", "climate.set_control_mode", "vehicle", {"mode": "auto"}, "用户指定空调自动控制。"))
|
||||
elif re.search(r"手动(?:控制|模式)", compact):
|
||||
elif re.search(r"手动(?:控制|模式)", climate_text):
|
||||
actions.append(_action("climate", "climate.set_control_mode", "vehicle", {"mode": "manual"}, "用户指定空调手动控制。"))
|
||||
|
||||
if re.search(r"内循环", compact):
|
||||
if re.search(r"内循环", climate_text):
|
||||
actions.append(_action("climate", "climate.set_cycle_mode", "vehicle", {"mode": "internal"}, "用户指定空调内循环。"))
|
||||
elif re.search(r"外循环", compact):
|
||||
elif re.search(r"外循环", climate_text):
|
||||
actions.append(_action("climate", "climate.set_cycle_mode", "vehicle", {"mode": "external"}, "用户指定空调外循环。"))
|
||||
|
||||
outlet_modes = [
|
||||
("吹面吹脚除霜", "face_feet_defrost"),
|
||||
("吹面除霜", "face_defrost"),
|
||||
("吹脚除霜", "feet_defrost"),
|
||||
("吹面吹脚", "face_feet"),
|
||||
("除霜", "defrost"),
|
||||
("吹脚", "feet"),
|
||||
("吹面", "face"),
|
||||
]
|
||||
for label, mode in outlet_modes:
|
||||
if label in compact:
|
||||
actions.append(_action("climate", "climate.set_outlet_mode", "vehicle", {"mode": mode}, "用户指定了空调出风模式。"))
|
||||
break
|
||||
|
||||
if "车窗" in compact:
|
||||
explicit_targets = []
|
||||
for label, target_name in WINDOW_TARGETS.items():
|
||||
if target_name in WINDOW_GROUP_MEMBERS or label not in compact:
|
||||
continue
|
||||
if target_name not in explicit_targets:
|
||||
explicit_targets.append(target_name)
|
||||
broad_target = next(
|
||||
(
|
||||
target_name
|
||||
for label, target_name in WINDOW_TARGETS.items()
|
||||
if target_name in WINDOW_GROUP_MEMBERS and label in compact
|
||||
),
|
||||
None,
|
||||
has_face = bool(re.search(r"吹面|吹脸", climate_text))
|
||||
has_feet = "吹脚" in climate_text
|
||||
has_defrost = "除霜" in climate_text
|
||||
outlet_mode = (
|
||||
"face_feet_defrost"
|
||||
if has_face and has_feet and has_defrost
|
||||
else "face_feet"
|
||||
if has_face and has_feet
|
||||
else "face_defrost"
|
||||
if has_face and has_defrost
|
||||
else "feet_defrost"
|
||||
if has_feet and has_defrost
|
||||
else "defrost"
|
||||
if has_defrost
|
||||
else "face"
|
||||
if has_face
|
||||
else "feet"
|
||||
if has_feet
|
||||
else None
|
||||
)
|
||||
exclusions = {
|
||||
target_name
|
||||
for label, target_name in WINDOW_TARGETS.items()
|
||||
if target_name not in WINDOW_GROUP_MEMBERS
|
||||
and re.search(
|
||||
rf"{re.escape(label)}(?:车窗)?(?:除外|除去|不包括|不含)",
|
||||
compact,
|
||||
if outlet_mode:
|
||||
actions.append(
|
||||
_action(
|
||||
"climate",
|
||||
"climate.set_outlet_mode",
|
||||
"vehicle",
|
||||
{"mode": outlet_mode},
|
||||
"用户指定了空调出风模式。",
|
||||
)
|
||||
)
|
||||
}
|
||||
if broad_target and exclusions:
|
||||
targets = [
|
||||
target_name
|
||||
for target_name in WINDOW_GROUP_MEMBERS[broad_target]
|
||||
if target_name not in exclusions
|
||||
]
|
||||
elif explicit_targets:
|
||||
targets = [target_name for target_name in explicit_targets if target_name not in exclusions]
|
||||
else:
|
||||
targets = [broad_target or "front_left"]
|
||||
|
||||
window_text = _scoped_text(
|
||||
compact,
|
||||
r"车窗",
|
||||
r"^(?:主驾|主驾驶|驾驶位|司机位|副驾|副驾驶|乘客位|"
|
||||
r"左前|前左|右前|前右|左后|后左|右后|后右)(?:车窗)?(?:除外|除去|不包括|不含)$",
|
||||
)
|
||||
if window_text:
|
||||
targets, explicit_window_target = _window_targets(window_text)
|
||||
position = re.search(
|
||||
r"(?:位置|开度|调到|设为|设置为|开启到|打开到)(\d{1,3})%",
|
||||
compact,
|
||||
r"(?:位置|开度|调整到|调到|设为|设置为|开启到|打开到|开到)(\d{1,3})%",
|
||||
window_text,
|
||||
)
|
||||
half_position = bool(
|
||||
re.search(
|
||||
r"(?:开启|打开|开到|开度)?(?:一半|半开)",
|
||||
window_text,
|
||||
)
|
||||
)
|
||||
half_position = bool(re.search(r"(?:开启|打开|开到|开度)?(?:一半|半开)", compact))
|
||||
position_pct = int(position[1]) if position else 50 if half_position else None
|
||||
if position_pct is not None and all(target not in WINDOW_GROUP_MEMBERS for target in targets) and position_pct <= 100:
|
||||
if (
|
||||
position_pct is not None
|
||||
and all(target not in WINDOW_GROUP_MEMBERS for target in targets)
|
||||
and position_pct <= 100
|
||||
):
|
||||
reason = (
|
||||
"用户指定了单个车窗的绝对开度。"
|
||||
if explicit_targets or broad_target
|
||||
if explicit_window_target
|
||||
else "用户未指定车窗目标,按驾驶台默认选择主驾左前窗。"
|
||||
)
|
||||
actions.extend(
|
||||
@@ -658,15 +809,15 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
pass
|
||||
else:
|
||||
motion = (
|
||||
"stop" if re.search(r"停止|停下|暂停", compact)
|
||||
else "close" if re.search(r"关闭|关上|升起|关窗", compact)
|
||||
else "open" if re.search(r"打开|开启|降下|开窗", compact)
|
||||
"stop" if re.search(r"停止|停下|暂停", window_text)
|
||||
else "close" if re.search(r"关闭|关上|升起|关窗", window_text)
|
||||
else "open" if re.search(r"打开|开启|降下|开窗", window_text)
|
||||
else None
|
||||
)
|
||||
if motion:
|
||||
reason = (
|
||||
"用户指定了车窗目标和动作。"
|
||||
if explicit_targets or broad_target
|
||||
if explicit_window_target
|
||||
else "用户未指定车窗目标,按驾驶台默认选择主驾左前窗。"
|
||||
)
|
||||
actions.extend(
|
||||
@@ -674,17 +825,18 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
for target in targets
|
||||
)
|
||||
|
||||
door_text = _scoped_text(compact, r"门锁|车门|后门|锁车|解锁车辆")
|
||||
rear_lock_request = bool(
|
||||
re.search(
|
||||
r"(?:后门|后排车门|后排门锁|二排车门|二排门锁)", compact
|
||||
r"(?:后门|后排车门|后排门锁|二排车门|二排门锁)", door_text
|
||||
)
|
||||
)
|
||||
if rear_lock_request:
|
||||
state = (
|
||||
"unlock"
|
||||
if re.search(r"解锁|开锁", compact)
|
||||
if re.search(r"解锁|开锁", door_text)
|
||||
else "lock"
|
||||
if re.search(r"上锁|落锁|锁上", compact)
|
||||
if re.search(r"上锁|落锁|锁上", door_text)
|
||||
else None
|
||||
)
|
||||
if state:
|
||||
@@ -695,59 +847,123 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
||||
r"整车(?:门锁|车门)|全车(?:门锁|车门)|(?:所有|全部)(?:车门|门锁)|"
|
||||
r"(?:车门|门锁)(?:上锁|落锁|锁上|解锁|开锁)|"
|
||||
r"(?:锁车|整车解锁|全车解锁|解锁车辆)",
|
||||
compact,
|
||||
door_text,
|
||||
)
|
||||
)
|
||||
if vehicle_lock_request:
|
||||
state = (
|
||||
"unlock"
|
||||
if re.search(r"解锁|开锁", compact)
|
||||
if re.search(r"解锁|开锁", door_text)
|
||||
else "lock"
|
||||
if re.search(r"上锁|落锁|锁上|锁车", compact)
|
||||
if re.search(r"上锁|落锁|锁上|锁车", door_text)
|
||||
else None
|
||||
)
|
||||
if state:
|
||||
actions.append(_action("door", "door.set_vehicle_lock", "vehicle", {"state": state}, "用户指定了整车门锁状态。"))
|
||||
|
||||
if re.search(r"遮阳帘|天幕帘", compact):
|
||||
position = re.search(r"(?:位置|开度|调到|设为)(\d{1,3})%", compact)
|
||||
shade_text = _scoped_text(compact, r"遮阳帘|天幕帘")
|
||||
if shade_text:
|
||||
position = re.search(
|
||||
r"(?:位置|开度|调整到|调到|设为|设置为|开到|打开(?:到)?|开启(?:到)?)(\d{1,3})%",
|
||||
shade_text,
|
||||
)
|
||||
if position and int(position[1]) <= 100:
|
||||
actions.append(_action("roof", "roof.set_shade_position", "vehicle", {"position_pct": int(position[1])}, "用户指定了遮阳帘开度。"))
|
||||
else:
|
||||
motion = (
|
||||
"stop" if re.search(r"停止|停下|暂停", compact)
|
||||
else "half" if re.search(r"半开|一半", compact)
|
||||
else "close" if re.search(r"关闭|关上", compact)
|
||||
else "open" if re.search(r"打开|开启", compact)
|
||||
"stop" if re.search(r"停止|停下|暂停", shade_text)
|
||||
else "half" if re.search(r"半开|一半", shade_text)
|
||||
else "close" if re.search(r"关闭|关上", shade_text)
|
||||
else "open" if re.search(r"打开|开启", shade_text)
|
||||
else None
|
||||
)
|
||||
if motion:
|
||||
actions.append(_action("roof", "roof.set_shade_motion", "vehicle", {"motion": motion}, "用户指定了遮阳帘动作。"))
|
||||
|
||||
indoor_text = _scoped_text(compact, r"室内灯|车内灯")
|
||||
for label, capability in (("室内灯", "interior_light.set_indoor_power"), ("车内灯", "interior_light.set_indoor_power")):
|
||||
if label in compact:
|
||||
state = "off" if re.search(r"关闭|关掉|关上", compact) else "on" if re.search(r"打开|开启", compact) else None
|
||||
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:
|
||||
actions.append(_action("interior_light", capability, "vehicle", {"state": state}, "用户指定了车内灯状态。"))
|
||||
break
|
||||
|
||||
if "阅读灯" in compact:
|
||||
target = next((value for label, value in LIGHT_TARGETS.items() if label in compact), "all")
|
||||
state = "off" if re.search(r"关闭|关掉|关上", compact) else "on" if re.search(r"打开|开启", compact) else None
|
||||
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")
|
||||
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}, "用户指定了阅读灯区域和状态。"))
|
||||
|
||||
if "氛围灯" in compact:
|
||||
color_match = re.search(r"颜色(?:索引|编号)?(\d{1,3})", compact)
|
||||
brightness_match = re.search(r"亮度(?:档|级)?([1-5])", compact)
|
||||
ambient_text = _scoped_text(
|
||||
compact,
|
||||
r"氛围灯",
|
||||
r"^(?:亮度|强度)(?:调整|调节|调|设置|设)?",
|
||||
)
|
||||
if ambient_text:
|
||||
state = (
|
||||
"off"
|
||||
if re.search(r"关闭|关掉|熄灭", ambient_text)
|
||||
else "on"
|
||||
if re.search(r"打开|开启|点亮", ambient_text)
|
||||
else None
|
||||
)
|
||||
if state:
|
||||
actions.append(
|
||||
_action(
|
||||
"ambient_light",
|
||||
"ambient_light.set_power",
|
||||
"vehicle",
|
||||
{"state": state},
|
||||
"用户指定了氛围灯总开关状态。",
|
||||
)
|
||||
)
|
||||
color_match = re.search(
|
||||
r"(?:颜色编号|颜色索引|颜色|色号)"
|
||||
r"(?:调整到|调整为|调到|调为|设置为|设为|为)?"
|
||||
r"(\d{1,3})(?![\d.])",
|
||||
ambient_text,
|
||||
)
|
||||
brightness_match = re.search(
|
||||
r"(?:亮度|强度)(?:调整到|调整为|调到|调为|设置为|设为|为)?"
|
||||
r"([1-5])(?:档|级)?(?![\d.])",
|
||||
ambient_text,
|
||||
)
|
||||
if color_match and 1 <= int(color_match[1]) <= 127:
|
||||
actions.append(_action("ambient_light", "ambient_light.set_color_index", "vehicle", {"color_index": int(color_match[1])}, "用户指定了已验证的氛围灯颜色索引。"))
|
||||
if brightness_match:
|
||||
actions.append(_action("ambient_light", "ambient_light.set_brightness_level", "vehicle", {"brightness_level": int(brightness_match[1])}, "用户指定了已验证的氛围灯亮度档位。"))
|
||||
|
||||
outlet_target = next((value for label, value in OUTLET_TARGETS.items() if f"{label}风口" in compact), None)
|
||||
outlet_position = re.search(r"(?:左右|水平)(\d{1,3})%?.*?(?:上下|垂直)(\d{1,3})%?", compact)
|
||||
if "风口" in compact and outlet_position and all(int(value) <= 100 for value in outlet_position.groups()):
|
||||
outlet_text = _scoped_text(compact, r"风口")
|
||||
outlet_target = next((value for label, value in OUTLET_TARGETS.items() if f"{label}风口" in outlet_text), None)
|
||||
calibrated_position = re.search(
|
||||
r"(?:标定坐标|标定位置).*?(?:左右|水平)"
|
||||
r"(-?\d{1,3})(?![\d.]).*?(?:上下|垂直)(-?\d{1,3})(?![\d.])",
|
||||
outlet_text,
|
||||
)
|
||||
outlet_position = re.search(
|
||||
r"(?:左右|水平)(\d{1,3})%?(?![\d.]).*?"
|
||||
r"(?:上下|垂直)(\d{1,3})%?(?![\d.])",
|
||||
outlet_text,
|
||||
)
|
||||
if (
|
||||
outlet_target
|
||||
and calibrated_position
|
||||
and all(-128 <= int(value) <= 126 for value in calibrated_position.groups())
|
||||
):
|
||||
actions.append(
|
||||
_action(
|
||||
"outlet",
|
||||
"outlet.set_calibrated_position",
|
||||
outlet_target,
|
||||
{
|
||||
"horizontal_value": int(calibrated_position[1]),
|
||||
"vertical_value": int(calibrated_position[2]),
|
||||
},
|
||||
"用户指定了已标定风口的双轴原始坐标。",
|
||||
)
|
||||
)
|
||||
elif outlet_text and outlet_position and all(int(value) <= 100 for value in outlet_position.groups()):
|
||||
target = outlet_target or "driver_left"
|
||||
reason = (
|
||||
"用户指定了已标定风口的双轴百分比位置。"
|
||||
|
||||
@@ -275,6 +275,7 @@ class NativeCanRelativeResolver:
|
||||
"target": target,
|
||||
"operation": "set",
|
||||
"parameters": parameters,
|
||||
"parse_source": "local_rule",
|
||||
"reason": f"用户要求{label}{verb}{delta};基于当前状态 {current} 计算绝对目标 {target_value}{',已按允许范围收敛' if clamped else ''}。",
|
||||
"relative_resolution": {
|
||||
"direction": direction,
|
||||
|
||||
@@ -183,6 +183,9 @@ class NativeCanRuntime:
|
||||
"zone": target,
|
||||
},
|
||||
"parameters": dict(action.get("parameters", {})),
|
||||
"parse_source": str(
|
||||
action.get("parse_source", "local_rule")
|
||||
),
|
||||
"timeout_ms": (
|
||||
20_000
|
||||
if capability_name
|
||||
@@ -417,6 +420,8 @@ class NativeCanRuntime:
|
||||
return f"seat driver massage strength {strength}"
|
||||
return f"seat driver massage mode {parameters['mode']}"
|
||||
if capability == "seat.set_driver_axis_native":
|
||||
if parameters["direction"] == "stop":
|
||||
return f"seat driver {parameters['axis']} stop"
|
||||
return (
|
||||
f"seat driver {parameters['axis']} {parameters['direction']} "
|
||||
f"{parameters.get('duration_ms', 500)}"
|
||||
@@ -1129,15 +1134,30 @@ class NativeCanExecutor:
|
||||
"feedback ID and a target-value match contract"
|
||||
)
|
||||
special_handler = command["signals"].get("special_handler")
|
||||
if special_handler == "window_position":
|
||||
feedback_required = self.feedback_required
|
||||
if not feedback_required and special_handler in {
|
||||
None,
|
||||
"shade_preset",
|
||||
}:
|
||||
# Wireless MQTT uses the state topic as an asynchronous
|
||||
# source of truth. Fixed frames and self-terminating
|
||||
# shade presets must not hold the HTTP request open
|
||||
# while waiting for the same state stream synchronously.
|
||||
sent_frames = self._send_command(bus, command)
|
||||
feedback = None
|
||||
elif special_handler == "window_position":
|
||||
sent_frames, feedback = self._execute_window_position(bus, command)
|
||||
elif special_handler == "driver_seat_axis":
|
||||
sent_frames, feedback = self._execute_driver_seat_axis(
|
||||
bus, command
|
||||
bus,
|
||||
command,
|
||||
wait_for_feedback=feedback_required,
|
||||
)
|
||||
elif special_handler == "driver_seat_memory_recall":
|
||||
sent_frames, feedback = self._execute_driver_seat_memory(
|
||||
bus, command
|
||||
bus,
|
||||
command,
|
||||
wait_for_completion=feedback_required,
|
||||
)
|
||||
elif special_handler == "shade_preset":
|
||||
sent_frames, feedback = self._execute_shade_preset(
|
||||
@@ -1151,7 +1171,6 @@ class NativeCanExecutor:
|
||||
sent_frames = self._send_command(bus, command)
|
||||
feedback = self._wait_feedback(bus, command)
|
||||
feedback_match = self._feedback_matches(command, feedback)
|
||||
feedback_required = self.feedback_required
|
||||
verified = feedback is not None and feedback_match is True
|
||||
delivered = bool(sent_frames)
|
||||
succeeded = verified or (delivered and not feedback_required)
|
||||
@@ -1167,17 +1186,26 @@ class NativeCanExecutor:
|
||||
publication_start:
|
||||
]
|
||||
)
|
||||
delivery_status = "submitted"
|
||||
if not feedback_required and delivered:
|
||||
delivery_status = (
|
||||
"published"
|
||||
if mqtt_messages
|
||||
and all(
|
||||
message.get("delivery_status") == "published"
|
||||
for message in mqtt_messages
|
||||
)
|
||||
else "queued"
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"ref": operation["id"],
|
||||
"domain": function_config["spec"]["domain"],
|
||||
"status": "succeeded" if succeeded else "failed",
|
||||
"message": (
|
||||
"MQTT 白名单控制帧已发布,并收到匹配的车辆状态反馈。"
|
||||
if not feedback_required and feedback_match is True
|
||||
else "MQTT 白名单控制帧已发布;已收到状态帧,但目标值尚未确认。"
|
||||
if not feedback_required and feedback is not None
|
||||
else "MQTT 白名单控制帧已发布;车辆状态反馈尚未确认。"
|
||||
"MQTT 白名单控制帧已由 Broker 确认;车辆效果以状态话题回读为准。"
|
||||
if not feedback_required and delivery_status == "published"
|
||||
else "MQTT 白名单控制帧已进入客户端发布队列;车辆效果以状态话题回读为准。"
|
||||
if not feedback_required
|
||||
else "CAN 盒已提交车端控制帧并观察到匹配反馈。"
|
||||
if feedback_match is True
|
||||
@@ -1197,11 +1225,7 @@ class NativeCanExecutor:
|
||||
"feedback_match": feedback_match,
|
||||
"feedback_state": self._feedback_state(feedback),
|
||||
"feedback_required": feedback_required,
|
||||
"delivery_status": (
|
||||
"published"
|
||||
if not feedback_required and delivered
|
||||
else "submitted"
|
||||
),
|
||||
"delivery_status": delivery_status,
|
||||
"physical_effect_confirmed": verified,
|
||||
"control_gate": (
|
||||
"compiled_action_and_frame_whitelist"
|
||||
@@ -1260,7 +1284,11 @@ class NativeCanExecutor:
|
||||
return sent
|
||||
|
||||
def _execute_driver_seat_axis(
|
||||
self, bus: CanBus, command: dict[str, Any]
|
||||
self,
|
||||
bus: CanBus,
|
||||
command: dict[str, Any],
|
||||
*,
|
||||
wait_for_feedback: bool = True,
|
||||
) -> tuple[list[CanFrame], CanFrame | None]:
|
||||
"""Send bounded motion and attempt all three stop frames in a finally path."""
|
||||
|
||||
@@ -1313,10 +1341,14 @@ class NativeCanExecutor:
|
||||
f"{len(stop_errors)}/3 forced-stop frame(s) failed"
|
||||
)
|
||||
raise RuntimeError("; ".join(details))
|
||||
return sent, self._wait_feedback(bus, command)
|
||||
return sent, self._wait_feedback(bus, command) if wait_for_feedback else None
|
||||
|
||||
def _execute_driver_seat_memory(
|
||||
self, bus: CanBus, command: dict[str, Any]
|
||||
self,
|
||||
bus: CanBus,
|
||||
command: dict[str, Any],
|
||||
*,
|
||||
wait_for_completion: bool = True,
|
||||
) -> tuple[list[CanFrame], CanFrame | None]:
|
||||
"""Require fresh initialization, then replay the learned 16/15 sequence."""
|
||||
|
||||
@@ -1366,8 +1398,12 @@ class NativeCanExecutor:
|
||||
)
|
||||
if index + 1 < len(sequence):
|
||||
time.sleep(int(special["period_ms"]) / 1_000)
|
||||
feedback = self._wait_for_id(
|
||||
bus, 0x494, int(special["post_status_timeout_ms"])
|
||||
feedback = (
|
||||
self._wait_for_id(
|
||||
bus, 0x494, int(special["post_status_timeout_ms"])
|
||||
)
|
||||
if wait_for_completion
|
||||
else None
|
||||
)
|
||||
return sent, feedback
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import re
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from copy import deepcopy
|
||||
from datetime import UTC, datetime
|
||||
@@ -177,7 +178,7 @@ UPDATE_MODE_PROFILE_PREFIX = re.compile(
|
||||
UPDATE_MODE_PROFILE_BA = re.compile(
|
||||
r"^(?:请(?:帮我)?|帮我)?(?:把|将)"
|
||||
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
|
||||
r"(?:的)?(?P<change>.+)$"
|
||||
r"的(?P<change>.+)$"
|
||||
)
|
||||
RESTORE_MODE_PROFILE_PREFIX = re.compile(
|
||||
r"^(?:请(?:帮我)?|帮我)?(?:恢复|还原|重置|初始化)"
|
||||
@@ -1066,6 +1067,340 @@ class SeatDemoService:
|
||||
return MqttOtaSeatExecutor(executor, resolved_bus_factory)
|
||||
return executor
|
||||
|
||||
def preview_native_can(
|
||||
self,
|
||||
utterance: str,
|
||||
provider: str = "auto",
|
||||
conversation_id: str | None = None,
|
||||
execution_mode: str = "dry_run",
|
||||
approval_token: str | None = None,
|
||||
vehicle_id: str | None = None,
|
||||
simulation_fault: str = "normal",
|
||||
*,
|
||||
hardware_client_id: str | None = None,
|
||||
hardware_access_bypass: bool = True,
|
||||
reasoning_effort: str | None = None,
|
||||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||||
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Validate and execute deterministic controls before hosted parsing."""
|
||||
|
||||
utterance = utterance.strip()
|
||||
if not utterance:
|
||||
raise ValueError("utterance must not be empty")
|
||||
if provider not in {"auto", "openai", "local"}:
|
||||
raise ValueError("provider must be auto, openai, or local")
|
||||
conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
|
||||
if not CONVERSATION_ID_PATTERN.fullmatch(conversation_id):
|
||||
raise ValueError(
|
||||
"conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens"
|
||||
)
|
||||
vehicle_profile = self.vehicle_profiles.get(vehicle_id)
|
||||
control_plane_request = any(
|
||||
matcher(utterance) is not None
|
||||
for matcher in (
|
||||
self._parse_mode_profile_save_name,
|
||||
self._parse_mode_profile_create_name,
|
||||
self._parse_mode_profile_restore_name,
|
||||
self._parse_mode_profile_update,
|
||||
)
|
||||
)
|
||||
if control_plane_request:
|
||||
result = self.process(
|
||||
utterance,
|
||||
provider=provider,
|
||||
conversation_id=conversation_id,
|
||||
execution_mode=execution_mode,
|
||||
approval_token=approval_token,
|
||||
vehicle_id=vehicle_profile.id,
|
||||
simulation_fault=simulation_fault,
|
||||
hardware_client_id=hardware_client_id,
|
||||
hardware_access_bypass=hardware_access_bypass,
|
||||
reasoning_effort=reasoning_effort,
|
||||
control_transport=control_transport,
|
||||
_on_commands_validated=on_commands_validated,
|
||||
)
|
||||
result.update(
|
||||
{
|
||||
"phase": "control_plane_complete",
|
||||
"preview_request_id": result["request_id"],
|
||||
"unresolved_clauses": [],
|
||||
"supplement_required": False,
|
||||
}
|
||||
)
|
||||
self._persist(result)
|
||||
return result
|
||||
context = self._context_for_vehicle(
|
||||
self.conversations.snapshot(conversation_id),
|
||||
vehicle_profile,
|
||||
)
|
||||
clause_results = self._local_clause_results(utterance, context)
|
||||
unresolved_clauses = [
|
||||
clause
|
||||
for clause, _, status in clause_results
|
||||
if status != "complete"
|
||||
]
|
||||
started = time.perf_counter()
|
||||
|
||||
def publish_local_commands(validated: dict[str, Any]) -> None:
|
||||
if on_commands_validated is None:
|
||||
return
|
||||
event = deepcopy(validated)
|
||||
event.update(
|
||||
{
|
||||
"phase": "deterministic_commands_validated",
|
||||
"provider_requested": provider,
|
||||
"unresolved_clauses": unresolved_clauses,
|
||||
"supplement_required": bool(
|
||||
provider != "local" and unresolved_clauses
|
||||
),
|
||||
}
|
||||
)
|
||||
on_commands_validated(event)
|
||||
|
||||
result = self.process(
|
||||
utterance,
|
||||
provider="local",
|
||||
conversation_id=conversation_id,
|
||||
execution_mode=execution_mode,
|
||||
approval_token=approval_token,
|
||||
vehicle_id=vehicle_profile.id,
|
||||
simulation_fault=simulation_fault,
|
||||
hardware_client_id=hardware_client_id,
|
||||
hardware_access_bypass=hardware_access_bypass,
|
||||
reasoning_effort=reasoning_effort,
|
||||
control_transport=control_transport,
|
||||
_local_preview=True,
|
||||
_allow_empty_control_result=True,
|
||||
_on_commands_validated=publish_local_commands,
|
||||
)
|
||||
elapsed_ms = round((time.perf_counter() - started) * 1000, 3)
|
||||
supplement_required = bool(
|
||||
provider != "local" and unresolved_clauses
|
||||
)
|
||||
result.update(
|
||||
{
|
||||
"phase": "deterministic_preview",
|
||||
"provider_requested": provider,
|
||||
"preview_request_id": result["request_id"],
|
||||
"preview_elapsed_ms": elapsed_ms,
|
||||
"unresolved_clauses": unresolved_clauses,
|
||||
"supplement_required": supplement_required,
|
||||
}
|
||||
)
|
||||
result["decision_summary"] = [
|
||||
*result.get("decision_summary", []),
|
||||
(
|
||||
"本地规则阶段已完成白名单校验并先行处理 "
|
||||
f"{sum(len((config.get('spec') or {}).get('operations', [])) for config in result.get('function_configs', {}).values())} 条 CAN 控制指令。"
|
||||
),
|
||||
]
|
||||
self._persist(result)
|
||||
manifest = {
|
||||
"version": 1,
|
||||
"preview_request_id": result["request_id"],
|
||||
"utterance": utterance,
|
||||
"provider": provider,
|
||||
"conversation_id": conversation_id,
|
||||
"execution_mode": execution_mode,
|
||||
"vehicle_id": vehicle_profile.id,
|
||||
"simulation_fault": simulation_fault,
|
||||
"hardware_client_id": hardware_client_id,
|
||||
"reasoning_effort": reasoning_effort,
|
||||
"control_transport": control_transport,
|
||||
"unresolved_clauses": unresolved_clauses,
|
||||
"preview_control_signatures": result.get(
|
||||
"parsed_control_signatures", []
|
||||
),
|
||||
}
|
||||
self._atomic_write(
|
||||
self.artifact_root / result["request_id"] / "preview.json",
|
||||
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
|
||||
)
|
||||
return result
|
||||
|
||||
def supplement_native_can(
|
||||
self,
|
||||
preview_request_id: str,
|
||||
*,
|
||||
provider: str | None = None,
|
||||
approval_token: str | None = None,
|
||||
hardware_client_id: str | None = None,
|
||||
hardware_access_bypass: bool = True,
|
||||
reasoning_effort: str | None = None,
|
||||
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Parse only unresolved clauses and merge them with the local result."""
|
||||
|
||||
if not MODE_EDITOR_REQUEST_ID_PATTERN.fullmatch(preview_request_id):
|
||||
raise ValueError("invalid preview request id")
|
||||
preview_dir = self.artifact_root / preview_request_id
|
||||
try:
|
||||
manifest = json.loads(
|
||||
(preview_dir / "preview.json").read_text(encoding="utf-8")
|
||||
)
|
||||
preview = json.loads(
|
||||
(preview_dir / "result.json").read_text(encoding="utf-8")
|
||||
)
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise ValueError("native CAN preview not found") from exc
|
||||
if (
|
||||
not isinstance(manifest, dict)
|
||||
or manifest.get("version") != 1
|
||||
or manifest.get("preview_request_id") != preview_request_id
|
||||
or not isinstance(preview, dict)
|
||||
or preview.get("request_id") != preview_request_id
|
||||
):
|
||||
raise ValueError("invalid native CAN preview manifest")
|
||||
selected_provider = str(manifest.get("provider") or "auto")
|
||||
if provider is not None and provider != selected_provider:
|
||||
raise ValueError("supplement provider does not match preview")
|
||||
selected_effort = manifest.get("reasoning_effort")
|
||||
if reasoning_effort is not None and reasoning_effort != selected_effort:
|
||||
raise ValueError("supplement reasoning effort does not match preview")
|
||||
expected_client_id = manifest.get("hardware_client_id")
|
||||
if hardware_client_id != expected_client_id:
|
||||
raise ValueError("supplement hardware client does not match preview")
|
||||
unresolved_clauses = [
|
||||
str(item).strip()
|
||||
for item in manifest.get("unresolved_clauses", [])
|
||||
if str(item).strip()
|
||||
]
|
||||
if not unresolved_clauses:
|
||||
raise ValueError("native CAN preview has no unresolved clauses")
|
||||
excluded_signatures = {
|
||||
tuple(item)
|
||||
for item in manifest.get("preview_control_signatures", [])
|
||||
if isinstance(item, list)
|
||||
}
|
||||
|
||||
def publish_cloud_commands(validated: dict[str, Any]) -> None:
|
||||
if on_commands_validated is None:
|
||||
return
|
||||
event = self._merge_native_can_phases(preview, validated)
|
||||
event.update(
|
||||
{
|
||||
"phase": "supplement_commands_validated",
|
||||
"preview_request_id": preview_request_id,
|
||||
"supplement_of": preview_request_id,
|
||||
"supplement_required": False,
|
||||
}
|
||||
)
|
||||
on_commands_validated(event)
|
||||
|
||||
supplement = self.process(
|
||||
",".join(unresolved_clauses),
|
||||
provider=selected_provider,
|
||||
conversation_id=str(manifest["conversation_id"]),
|
||||
execution_mode=str(manifest.get("execution_mode") or "dry_run"),
|
||||
approval_token=approval_token,
|
||||
vehicle_id=str(manifest["vehicle_id"]),
|
||||
simulation_fault=str(manifest.get("simulation_fault") or "normal"),
|
||||
hardware_client_id=hardware_client_id,
|
||||
hardware_access_bypass=hardware_access_bypass,
|
||||
reasoning_effort=(
|
||||
str(selected_effort) if selected_effort is not None else None
|
||||
),
|
||||
control_transport=str(
|
||||
manifest.get("control_transport") or WIRED_CAN_TRANSPORT
|
||||
),
|
||||
_excluded_control_signatures=excluded_signatures,
|
||||
_model_parse_source="cloud_supplement",
|
||||
_allow_empty_control_result=True,
|
||||
_on_commands_validated=publish_cloud_commands,
|
||||
)
|
||||
merged = self._merge_native_can_phases(preview, supplement)
|
||||
merged.update(
|
||||
{
|
||||
"phase": "two_stage_complete",
|
||||
"utterance": str(manifest["utterance"]),
|
||||
"provider_requested": selected_provider,
|
||||
"preview_request_id": preview_request_id,
|
||||
"supplement_of": preview_request_id,
|
||||
"supplement_request_id": supplement["request_id"],
|
||||
"supplement_required": False,
|
||||
"unresolved_clauses": unresolved_clauses,
|
||||
}
|
||||
)
|
||||
self._persist(merged)
|
||||
return merged
|
||||
|
||||
@staticmethod
|
||||
def _merge_native_can_phases(
|
||||
preview: dict[str, Any],
|
||||
supplement: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Create one durable result without collapsing same-domain phases."""
|
||||
|
||||
merged = deepcopy(supplement)
|
||||
|
||||
def phase_documents(
|
||||
local_documents: dict[str, Any] | None,
|
||||
cloud_documents: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
**{
|
||||
f"local_{key}": deepcopy(value)
|
||||
for key, value in (local_documents or {}).items()
|
||||
},
|
||||
**{
|
||||
f"cloud_{key}": deepcopy(value)
|
||||
for key, value in (cloud_documents or {}).items()
|
||||
},
|
||||
}
|
||||
|
||||
configs = phase_documents(
|
||||
preview.get("function_configs"),
|
||||
supplement.get("function_configs"),
|
||||
)
|
||||
plans = phase_documents(
|
||||
preview.get("command_plans"),
|
||||
supplement.get("command_plans"),
|
||||
)
|
||||
merged["function_configs"] = configs
|
||||
merged["command_plans"] = plans
|
||||
merged["function_config"] = next(iter(configs.values()), None)
|
||||
merged["command_plan"] = next(iter(plans.values()), None)
|
||||
merged["decision_summary"] = [
|
||||
*preview.get("decision_summary", []),
|
||||
*supplement.get("decision_summary", []),
|
||||
]
|
||||
merged["trace_events"] = [
|
||||
*preview.get("trace_events", []),
|
||||
*supplement.get("trace_events", []),
|
||||
]
|
||||
merged["validation"] = [
|
||||
*preview.get("validation", []),
|
||||
*supplement.get("validation", []),
|
||||
]
|
||||
merged["parsed_control_signatures"] = [
|
||||
*preview.get("parsed_control_signatures", []),
|
||||
*supplement.get("parsed_control_signatures", []),
|
||||
]
|
||||
merged["phase_execution_reports"] = {
|
||||
"local_rule": deepcopy(preview.get("execution_report")),
|
||||
"cloud_supplement": deepcopy(supplement.get("execution_report")),
|
||||
}
|
||||
if supplement.get("execution_report") is None:
|
||||
merged["execution_report"] = deepcopy(preview.get("execution_report"))
|
||||
successful = {"succeeded", "partially_succeeded", "no_action"}
|
||||
preview_ok = preview.get("status") in successful
|
||||
supplement_ok = supplement.get("status") in successful
|
||||
has_any_commands = bool(configs and plans)
|
||||
if preview_ok and supplement_ok:
|
||||
merged["status"] = "succeeded" if has_any_commands else "no_action"
|
||||
elif preview_ok and preview.get("function_configs"):
|
||||
merged["status"] = "partially_succeeded"
|
||||
merged["local_preview"] = {
|
||||
"request_id": preview.get("request_id"),
|
||||
"elapsed_ms": preview.get("preview_elapsed_ms"),
|
||||
"command_count": sum(
|
||||
len((config.get("spec") or {}).get("operations", []))
|
||||
for config in preview.get("function_configs", {}).values()
|
||||
),
|
||||
}
|
||||
return merged
|
||||
|
||||
def process(
|
||||
self,
|
||||
utterance: str,
|
||||
@@ -1080,6 +1415,11 @@ class SeatDemoService:
|
||||
hardware_access_bypass: bool = True,
|
||||
reasoning_effort: str | None = None,
|
||||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||||
_local_preview: bool = False,
|
||||
_excluded_control_signatures: set[tuple[Any, ...]] | None = None,
|
||||
_model_parse_source: str = "cloud_model",
|
||||
_allow_empty_control_result: bool = False,
|
||||
_on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
control_transport = self._validate_control_transport(control_transport)
|
||||
# Creating a reusable scene is a configuration workflow. Synthesize
|
||||
@@ -1099,6 +1439,11 @@ class SeatDemoService:
|
||||
simulation_fault,
|
||||
reasoning_effort,
|
||||
control_transport,
|
||||
_local_preview,
|
||||
_excluded_control_signatures,
|
||||
_model_parse_source,
|
||||
_allow_empty_control_result,
|
||||
_on_commands_validated,
|
||||
)
|
||||
if hardware_access_bypass or self._driver_only_vehicle_execution_active(
|
||||
control_transport
|
||||
@@ -1114,6 +1459,11 @@ class SeatDemoService:
|
||||
simulation_fault,
|
||||
reasoning_effort,
|
||||
control_transport,
|
||||
_local_preview,
|
||||
_excluded_control_signatures,
|
||||
_model_parse_source,
|
||||
_allow_empty_control_result,
|
||||
_on_commands_validated,
|
||||
)
|
||||
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
|
||||
with self.hardware_execution.reserve():
|
||||
@@ -1128,6 +1478,11 @@ class SeatDemoService:
|
||||
simulation_fault,
|
||||
reasoning_effort,
|
||||
control_transport,
|
||||
_local_preview,
|
||||
_excluded_control_signatures,
|
||||
_model_parse_source,
|
||||
_allow_empty_control_result,
|
||||
_on_commands_validated,
|
||||
)
|
||||
|
||||
def _process_once(
|
||||
@@ -1141,6 +1496,11 @@ class SeatDemoService:
|
||||
simulation_fault: str = "normal",
|
||||
reasoning_effort: str | None = None,
|
||||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||||
local_preview: bool = False,
|
||||
excluded_control_signatures: set[tuple[Any, ...]] | None = None,
|
||||
model_parse_source: str = "cloud_model",
|
||||
allow_empty_control_result: bool = False,
|
||||
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
control_transport = self._validate_control_transport(control_transport)
|
||||
utterance = utterance.strip()
|
||||
@@ -1298,6 +1658,8 @@ class SeatDemoService:
|
||||
context,
|
||||
vehicle_profile,
|
||||
reasoning_effort=reasoning_effort,
|
||||
local_preview=local_preview,
|
||||
model_parse_source=model_parse_source,
|
||||
)
|
||||
if self._is_dynamic_mode_stop_request(utterance):
|
||||
parsed = self._apply_dynamic_mode_stop_request(parsed, utterance)
|
||||
@@ -1305,6 +1667,7 @@ class SeatDemoService:
|
||||
parsed = self._apply_mode_profiles(
|
||||
parsed, matched_modes, context, vehicle_profile
|
||||
)
|
||||
parsed = self._mark_control_parse_source(parsed, "local_rule")
|
||||
relative_resolution = (
|
||||
self.native_can_relative_resolver.resolve(utterance, context)
|
||||
if self.native_can_relative_resolver.matches(utterance)
|
||||
@@ -1323,6 +1686,15 @@ class SeatDemoService:
|
||||
relative_resolution.claimed_controls if relative_resolution else None
|
||||
),
|
||||
)
|
||||
if local_preview:
|
||||
parsed["ambiguities"] = []
|
||||
parsed["unresolved_spans"] = []
|
||||
parsed["model_requests"] = []
|
||||
if excluded_control_signatures:
|
||||
parsed = self._filter_excluded_controls(
|
||||
parsed,
|
||||
excluded_control_signatures,
|
||||
)
|
||||
parsed = apply_language_safety_guards(parsed, utterance)
|
||||
parsed = self._partition_native_actions(
|
||||
parsed,
|
||||
@@ -1335,6 +1707,47 @@ class SeatDemoService:
|
||||
)
|
||||
),
|
||||
)
|
||||
result["parsed_control_signatures"] = [
|
||||
list(self._control_signature(action))
|
||||
for action in [
|
||||
*parsed.get("actions", []),
|
||||
*parsed.get("domain_actions", []),
|
||||
*parsed.get("native_can_actions", []),
|
||||
]
|
||||
]
|
||||
has_control = any(
|
||||
parsed.get(key)
|
||||
for key in ("actions", "domain_actions", "native_can_actions")
|
||||
)
|
||||
if allow_empty_control_result and not has_control:
|
||||
result["provider_used"] = provider_used
|
||||
result["decision_summary"] = [
|
||||
*parsed.get("decision_summary", []),
|
||||
"当前阶段没有新增的已验证 CAN 控制指令。",
|
||||
]
|
||||
result["deferred_requests"] = parsed.get("deferred_requests", [])
|
||||
result["model_requests"] = parsed.get("model_requests", [])
|
||||
result["intent_ir"] = build_intent_ir(
|
||||
parsed,
|
||||
utterance,
|
||||
request_id,
|
||||
provider_used,
|
||||
self.semantic_capability_metadata,
|
||||
)
|
||||
result["validation"] = self.contracts.validate(result["intent_ir"])
|
||||
result["status"] = "no_action"
|
||||
self._trace(
|
||||
traces,
|
||||
request_id,
|
||||
"intent_parse",
|
||||
"succeeded",
|
||||
"当前阶段未产生新的白名单控制动作。",
|
||||
started,
|
||||
{"provider": provider_used, "control_count": 0},
|
||||
domain="orchestration",
|
||||
)
|
||||
self._persist(result)
|
||||
return result
|
||||
if hardware_request and not control_plane_request:
|
||||
if (
|
||||
control_transport == WIRELESS_MQTT_TRANSPORT
|
||||
@@ -1505,6 +1918,7 @@ class SeatDemoService:
|
||||
simulation_fault=simulation_fault,
|
||||
vehicle_profile=vehicle_profile,
|
||||
control_transport=control_transport,
|
||||
on_commands_validated=on_commands_validated,
|
||||
)
|
||||
|
||||
intent_ir = build_intent_ir(
|
||||
@@ -1776,6 +2190,8 @@ class SeatDemoService:
|
||||
self._persist(result)
|
||||
return result
|
||||
|
||||
self._emit_validated_commands(result, on_commands_validated)
|
||||
|
||||
safety_started = time.perf_counter()
|
||||
self._trace(
|
||||
traces,
|
||||
@@ -3395,6 +3811,21 @@ class SeatDemoService:
|
||||
result["deferred_requests"] = deferred_requests
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _emit_validated_commands(
|
||||
result: dict[str, Any],
|
||||
callback: Callable[[dict[str, Any]], None] | None,
|
||||
) -> None:
|
||||
"""Expose fully compiled commands before physical execution begins."""
|
||||
|
||||
if callback is None:
|
||||
return
|
||||
event = deepcopy(result)
|
||||
event["status"] = "processing"
|
||||
event["phase"] = "commands_validated"
|
||||
event["execution_report"] = None
|
||||
callback(event)
|
||||
|
||||
def _process_orchestrated(
|
||||
self,
|
||||
utterance: str,
|
||||
@@ -3411,6 +3842,7 @@ class SeatDemoService:
|
||||
simulation_fault: str = "normal",
|
||||
vehicle_profile: VehicleProfile | None = None,
|
||||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||||
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Run isolated adapters under the selected physical-bus policy."""
|
||||
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
||||
@@ -3669,6 +4101,8 @@ class SeatDemoService:
|
||||
self._persist(result)
|
||||
return result
|
||||
|
||||
self._emit_validated_commands(result, on_commands_validated)
|
||||
|
||||
self._trace(
|
||||
traces,
|
||||
request_id,
|
||||
@@ -3842,7 +4276,7 @@ class SeatDemoService:
|
||||
"execute",
|
||||
"succeeded" if execution_succeeded and not report_issues else "failed",
|
||||
(
|
||||
"无线 MQTT 白名单控制帧已发布;车辆实际状态以状态话题回读为准。"
|
||||
"无线 MQTT 白名单控制帧已提交;车辆实际状态以状态话题回读为准。"
|
||||
if (
|
||||
hardware_request
|
||||
and control_transport == WIRELESS_MQTT_TRANSPORT
|
||||
@@ -4385,16 +4819,29 @@ class SeatDemoService:
|
||||
)
|
||||
return merged
|
||||
|
||||
def _interpret(
|
||||
@staticmethod
|
||||
def _mark_control_parse_source(
|
||||
parsed: dict[str, Any],
|
||||
source: str,
|
||||
*,
|
||||
overwrite: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Attach per-control provenance without changing semantic content."""
|
||||
|
||||
marked = deepcopy(parsed)
|
||||
for key in ("actions", "domain_actions", "native_can_actions"):
|
||||
for action in marked.get(key, []):
|
||||
if overwrite or not action.get("parse_source"):
|
||||
action["parse_source"] = source
|
||||
return marked
|
||||
|
||||
def _local_clause_results(
|
||||
self,
|
||||
utterance: str,
|
||||
provider: str,
|
||||
context: dict[str, Any],
|
||||
vehicle_profile: VehicleProfile | None = None,
|
||||
*,
|
||||
reasoning_effort: str | None = None,
|
||||
) -> tuple[dict[str, Any], str, str | None]:
|
||||
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
||||
) -> list[tuple[str, dict[str, Any], str]]:
|
||||
"""Parse each clause deterministically and preserve inherited seat scope."""
|
||||
|
||||
clause_results: list[tuple[str, dict[str, Any], str]] = []
|
||||
|
||||
def parse_clause(clause: str) -> dict[str, Any]:
|
||||
@@ -4413,7 +4860,7 @@ class SeatDemoService:
|
||||
relative.claimed_controls if relative else None
|
||||
),
|
||||
)
|
||||
return local
|
||||
return self._mark_control_parse_source(local, "local_rule")
|
||||
|
||||
def has_seat_domain(parsed: dict[str, Any]) -> bool:
|
||||
if parsed.get("actions") or "seat" in parsed.get("detected_domains", []):
|
||||
@@ -4441,12 +4888,6 @@ class SeatDemoService:
|
||||
if clause_has_seat and explicit_driver != explicit_passenger:
|
||||
inherited_seat_scope = "driver" if explicit_driver else "passenger"
|
||||
elif clause_has_seat and inherited_seat_scope:
|
||||
# Chinese compound controls commonly state the seat once and
|
||||
# then list axes or thermal functions after commas. Carry the
|
||||
# last explicit seat scope only into later seat clauses; this
|
||||
# prevents a passenger request from silently falling through
|
||||
# to the CH1 driver default while leaving climate/door/window
|
||||
# clauses untouched.
|
||||
scope_label = (
|
||||
"主驾座椅"
|
||||
if inherited_seat_scope == "driver"
|
||||
@@ -4457,10 +4898,59 @@ class SeatDemoService:
|
||||
clause_results.append(
|
||||
(clause, local, self._local_clause_status(local, clause))
|
||||
)
|
||||
return clause_results
|
||||
|
||||
@classmethod
|
||||
def _trusted_local_preview(
|
||||
cls,
|
||||
clause_results: list[tuple[str, dict[str, Any], str]],
|
||||
) -> dict[str, Any]:
|
||||
"""Keep deterministic controls while deferring incomplete text to GPT."""
|
||||
|
||||
trusted = cls._merge_parsed_results(
|
||||
[parsed for _, parsed, status in clause_results if status != "unparsed"]
|
||||
)
|
||||
# Ambiguities and unresolved spans belong to the supplement phase. A
|
||||
# deterministic action still goes through the normal schema, compiler,
|
||||
# whitelist and physical execution gates before it can be sent.
|
||||
trusted["ambiguities"] = []
|
||||
trusted["unresolved_spans"] = []
|
||||
trusted["model_requests"] = []
|
||||
return trusted
|
||||
|
||||
@classmethod
|
||||
def _filter_excluded_controls(
|
||||
cls,
|
||||
parsed: dict[str, Any],
|
||||
excluded: set[tuple[Any, ...]],
|
||||
) -> dict[str, Any]:
|
||||
filtered = deepcopy(parsed)
|
||||
for key in ("actions", "domain_actions", "native_can_actions"):
|
||||
filtered[key] = [
|
||||
action
|
||||
for action in filtered.get(key, [])
|
||||
if cls._control_signature(action) not in excluded
|
||||
]
|
||||
return filtered
|
||||
|
||||
def _interpret(
|
||||
self,
|
||||
utterance: str,
|
||||
provider: str,
|
||||
context: dict[str, Any],
|
||||
vehicle_profile: VehicleProfile | None = None,
|
||||
*,
|
||||
reasoning_effort: str | None = None,
|
||||
local_preview: bool = False,
|
||||
model_parse_source: str = "cloud_model",
|
||||
) -> tuple[dict[str, Any], str, str | None]:
|
||||
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
||||
clause_results = self._local_clause_results(utterance, context)
|
||||
|
||||
trusted_local = self._merge_parsed_results(
|
||||
[parsed for _, parsed, status in clause_results if status != "unparsed"]
|
||||
)
|
||||
preview_local = self._trusted_local_preview(clause_results)
|
||||
all_local = self._merge_parsed_results(
|
||||
[parsed for _, parsed, _ in clause_results]
|
||||
)
|
||||
@@ -4474,7 +4964,13 @@ class SeatDemoService:
|
||||
safe_fallback = trusted_local if trusted_controls else all_local
|
||||
if provider == "local" or not fallback_clauses:
|
||||
return (
|
||||
all_local if provider == "local" else trusted_local,
|
||||
(
|
||||
preview_local
|
||||
if provider == "local" and local_preview
|
||||
else all_local
|
||||
if provider == "local"
|
||||
else trusted_local
|
||||
),
|
||||
"local_rules",
|
||||
None,
|
||||
)
|
||||
@@ -4505,6 +5001,11 @@ class SeatDemoService:
|
||||
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),
|
||||
f"local_rules+openai:{self.openai.model}",
|
||||
|
||||
@@ -411,6 +411,10 @@ def mqtt_control_payload(
|
||||
"data": frame.data.hex(" ").upper(),
|
||||
"repeat": 1,
|
||||
"intervalMs": 0,
|
||||
# Vehicle state is consumed asynchronously from can/state by the
|
||||
# Cognitive Agent. Do not serialize Android's command worker on a
|
||||
# per-command response wait as well.
|
||||
"expectResponse": False,
|
||||
"responseTimeoutMs": bounded_timeout_ms,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
@@ -788,14 +792,19 @@ class MqttCanTransport:
|
||||
)
|
||||
if int(getattr(result, "rc", 0)) != 0:
|
||||
raise CanGatewayError(f"MQTT CAN publish failed with code {result.rc}")
|
||||
if hasattr(result, "wait_for_publish"):
|
||||
published = result.wait_for_publish(timeout=2.0)
|
||||
if published is False:
|
||||
raise CanGatewayError("MQTT CAN publish confirmation timed out")
|
||||
# Paho already accepted the payload into its network-loop queue when
|
||||
# publish() returns rc == MQTT_ERR_SUCCESS. Waiting here serializes
|
||||
# every repeated CAN frame and previously added up to two seconds per
|
||||
# frame. Observe an already-available acknowledgement without ever
|
||||
# blocking the control pipeline.
|
||||
is_published = getattr(result, "is_published", None)
|
||||
broker_confirmed = bool(is_published()) if callable(is_published) else False
|
||||
return {
|
||||
"topic": self.settings.control_topic,
|
||||
"qos": self.settings.qos,
|
||||
"retain": False,
|
||||
"delivery_status": "published" if broker_confirmed else "queued",
|
||||
"message_id": getattr(result, "mid", None),
|
||||
"payload": json.loads(raw_payload),
|
||||
}
|
||||
|
||||
@@ -876,7 +885,7 @@ class MqttCanBus:
|
||||
|
||||
@property
|
||||
def published_messages(self) -> tuple[dict[str, Any], ...]:
|
||||
"""Return this bus view's confirmed MQTT publications in send order."""
|
||||
"""Return this bus view's accepted MQTT publications in send order."""
|
||||
|
||||
return tuple(deepcopy(self._published_messages))
|
||||
|
||||
|
||||
+215
-26
@@ -362,7 +362,14 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
if path.startswith("/api/mode-profiles/"):
|
||||
self._handle_mode_profile_post(path)
|
||||
return
|
||||
if path != "/api/requests":
|
||||
supplement_match = re.fullmatch(
|
||||
r"/api/requests/(?P<request_id>req-[a-z0-9-]+)/supplement",
|
||||
path,
|
||||
)
|
||||
if (
|
||||
path not in {"/api/requests", "/api/requests/preview"}
|
||||
and not supplement_match
|
||||
):
|
||||
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
|
||||
return
|
||||
|
||||
@@ -397,40 +404,82 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
payload = self._read_json_body()
|
||||
utterance = str(payload.get("utterance", ""))
|
||||
source = str(payload.get("client_type", "head_unit"))
|
||||
self.vehicle_display_sync.update_task(
|
||||
status="running",
|
||||
utterance=utterance,
|
||||
source=source,
|
||||
if utterance:
|
||||
self.vehicle_display_sync.update_task(
|
||||
status="running",
|
||||
utterance=utterance,
|
||||
source=source,
|
||||
)
|
||||
hardware_client_id = (
|
||||
str(payload["hardware_client_id"])
|
||||
if payload.get("hardware_client_id")
|
||||
else None
|
||||
)
|
||||
result = self.service.process(
|
||||
utterance,
|
||||
str(payload.get("provider", "auto")),
|
||||
str(payload["conversation_id"]) if payload.get("conversation_id") else None,
|
||||
str(payload.get("execution_mode", "dry_run")),
|
||||
reasoning_effort = (
|
||||
str(payload["reasoning_effort"])
|
||||
if payload.get("reasoning_effort") is not None
|
||||
else None
|
||||
)
|
||||
common_arguments = {
|
||||
"utterance": utterance,
|
||||
"provider": str(payload.get("provider", "auto")),
|
||||
"conversation_id": (
|
||||
str(payload["conversation_id"])
|
||||
if payload.get("conversation_id")
|
||||
else None
|
||||
),
|
||||
"execution_mode": str(
|
||||
payload.get("execution_mode", "dry_run")
|
||||
),
|
||||
# Hardware authorization is a server deployment policy. The
|
||||
# browser no longer renders or transmits an approval secret.
|
||||
approval_token=os.getenv("COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"),
|
||||
vehicle_id=(
|
||||
# browser never receives or transmits this secret.
|
||||
"approval_token": os.getenv(
|
||||
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
|
||||
),
|
||||
"vehicle_id": (
|
||||
str(payload["vehicle_id"])
|
||||
if payload.get("vehicle_id")
|
||||
else None
|
||||
),
|
||||
simulation_fault=str(payload.get("simulation_fault", "normal")),
|
||||
hardware_client_id=(
|
||||
str(payload["hardware_client_id"])
|
||||
if payload.get("hardware_client_id")
|
||||
else None
|
||||
"simulation_fault": str(
|
||||
payload.get("simulation_fault", "normal")
|
||||
),
|
||||
hardware_access_bypass=self._is_unproxied_loopback_request(),
|
||||
reasoning_effort=(
|
||||
str(payload["reasoning_effort"])
|
||||
if payload.get("reasoning_effort") is not None
|
||||
else None
|
||||
),
|
||||
control_transport=str(
|
||||
"hardware_client_id": hardware_client_id,
|
||||
"hardware_access_bypass": self._is_unproxied_loopback_request(),
|
||||
"reasoning_effort": reasoning_effort,
|
||||
"control_transport": str(
|
||||
payload.get("control_transport", "wired_can")
|
||||
),
|
||||
)
|
||||
}
|
||||
if "application/x-ndjson" in self.headers.get("Accept", ""):
|
||||
self._stream_control_request(
|
||||
path,
|
||||
supplement_match,
|
||||
payload,
|
||||
common_arguments,
|
||||
source=source,
|
||||
)
|
||||
return
|
||||
if path == "/api/requests/preview":
|
||||
result = self.service.preview_native_can(**common_arguments)
|
||||
elif supplement_match:
|
||||
result = self.service.supplement_native_can(
|
||||
supplement_match.group("request_id"),
|
||||
provider=(
|
||||
str(payload["provider"])
|
||||
if payload.get("provider")
|
||||
else None
|
||||
),
|
||||
approval_token=os.getenv(
|
||||
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
|
||||
),
|
||||
hardware_client_id=hardware_client_id,
|
||||
hardware_access_bypass=self._is_unproxied_loopback_request(),
|
||||
reasoning_effort=reasoning_effort,
|
||||
)
|
||||
else:
|
||||
result = self.service.process(**common_arguments)
|
||||
utterance = str(result.get("utterance") or utterance)
|
||||
self.vehicle_display_sync.update_task(
|
||||
status=str(result.get("status", "succeeded")),
|
||||
utterance=utterance,
|
||||
@@ -489,6 +538,146 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
def _stream_control_request(
|
||||
self,
|
||||
path: str,
|
||||
supplement_match: re.Match[str] | None,
|
||||
payload: dict[str, object],
|
||||
common_arguments: dict[str, object],
|
||||
*,
|
||||
source: str,
|
||||
) -> None:
|
||||
"""Stream validated command plans before physical execution finishes."""
|
||||
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", "application/x-ndjson; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("X-Accel-Buffering", "no")
|
||||
self._send_security_headers()
|
||||
self.end_headers()
|
||||
stream_open = True
|
||||
|
||||
def emit(event: dict[str, object]) -> None:
|
||||
nonlocal stream_open
|
||||
if not stream_open:
|
||||
return
|
||||
content = (json.dumps(event, ensure_ascii=False) + "\n").encode(
|
||||
"utf-8"
|
||||
)
|
||||
try:
|
||||
self.wfile.write(content)
|
||||
self.wfile.flush()
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
# The submitted control request must continue even if the
|
||||
# browser closes while the physical execution is in progress.
|
||||
stream_open = False
|
||||
|
||||
def on_commands_validated(result: dict[str, object]) -> None:
|
||||
emit({"type": "validated_commands", "payload": result})
|
||||
|
||||
utterance = str(common_arguments.get("utterance") or "")
|
||||
try:
|
||||
if path == "/api/requests/preview":
|
||||
result = self.service.preview_native_can(
|
||||
**common_arguments,
|
||||
on_commands_validated=on_commands_validated,
|
||||
)
|
||||
elif supplement_match:
|
||||
result = self.service.supplement_native_can(
|
||||
supplement_match.group("request_id"),
|
||||
provider=(
|
||||
str(payload["provider"])
|
||||
if payload.get("provider")
|
||||
else None
|
||||
),
|
||||
approval_token=os.getenv(
|
||||
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
|
||||
),
|
||||
hardware_client_id=(
|
||||
str(payload["hardware_client_id"])
|
||||
if payload.get("hardware_client_id")
|
||||
else None
|
||||
),
|
||||
hardware_access_bypass=self._is_unproxied_loopback_request(),
|
||||
reasoning_effort=(
|
||||
str(payload["reasoning_effort"])
|
||||
if payload.get("reasoning_effort") is not None
|
||||
else None
|
||||
),
|
||||
on_commands_validated=on_commands_validated,
|
||||
)
|
||||
else:
|
||||
result = self.service.process(
|
||||
**common_arguments,
|
||||
_on_commands_validated=on_commands_validated,
|
||||
)
|
||||
utterance = str(result.get("utterance") or utterance)
|
||||
self.vehicle_display_sync.update_task(
|
||||
status=str(result.get("status", "succeeded")),
|
||||
utterance=utterance,
|
||||
source=source,
|
||||
result=result,
|
||||
)
|
||||
emit({"type": "result", "payload": result})
|
||||
except HardwareAccessRequiredError as exc:
|
||||
self.vehicle_display_sync.update_task(
|
||||
status="failed",
|
||||
utterance=utterance,
|
||||
source=source,
|
||||
error={"code": "hardware_access_required", "message": str(exc)},
|
||||
)
|
||||
emit({
|
||||
"type": "error",
|
||||
"error": "hardware_access_required",
|
||||
"message": str(exc),
|
||||
"access_state": exc.state,
|
||||
})
|
||||
except HardwareBusyError as exc:
|
||||
self.vehicle_display_sync.update_task(
|
||||
status="failed",
|
||||
utterance=utterance,
|
||||
source=source,
|
||||
error={"code": "hardware_busy", "message": str(exc)},
|
||||
)
|
||||
emit({
|
||||
"type": "error",
|
||||
"error": "hardware_busy",
|
||||
"message": str(exc),
|
||||
"retry_after_seconds": exc.retry_after_seconds,
|
||||
})
|
||||
except HardwareSafetyError as exc:
|
||||
self.vehicle_display_sync.update_task(
|
||||
status="failed",
|
||||
utterance=utterance,
|
||||
source=source,
|
||||
error={"code": "hardware_safety_gate", "message": str(exc)},
|
||||
)
|
||||
emit({
|
||||
"type": "error",
|
||||
"error": "hardware_safety_gate",
|
||||
"message": str(exc),
|
||||
})
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
emit({
|
||||
"type": "error",
|
||||
"error": "invalid_request",
|
||||
"message": str(exc),
|
||||
})
|
||||
except OpenAIIntentError as exc:
|
||||
self.log_error("OpenAI intent request failed: %s", exc)
|
||||
emit({
|
||||
"type": "error",
|
||||
"error": "openai_request_failed",
|
||||
"message": "GPT 意图解析失败,请检查 API Key、模型权限或网络状态。",
|
||||
})
|
||||
except Exception as exc: # noqa: BLE001 - stream a terminal event
|
||||
self.log_error("streaming request failed: %s", exc)
|
||||
emit({
|
||||
"type": "error",
|
||||
"error": "internal_error",
|
||||
"message": "请求处理失败,请查看服务端日志。",
|
||||
})
|
||||
|
||||
def do_DELETE(self) -> None: # noqa: N802 - stdlib handler API
|
||||
parsed_url = urlparse(self.path)
|
||||
path = unquote(parsed_url.path)
|
||||
|
||||
Reference in New Issue
Block a user