fix: compose named cockpit modes from capabilities
This commit is contained in:
+56
-7
@@ -8310,20 +8310,69 @@ window.CognitiveAgentConsole = {
|
|||||||
? draft.trim()
|
? draft.trim()
|
||||||
: String(draft?.instruction || "").trim();
|
: String(draft?.instruction || "").trim();
|
||||||
if (!instruction) throw new Error("功能模式草稿缺少可试运行的指令。");
|
if (!instruction) throw new Error("功能模式草稿缺少可试运行的指令。");
|
||||||
const { payload } = await requestNaturalLanguageControl(instruction, {
|
let payload;
|
||||||
executionMode: "dry_run",
|
if (draft && typeof draft === "object" && draft.source_request_id) {
|
||||||
clientType: "assistant_mode_trial",
|
const response = await apiRequest("/api/mode-profile-drafts/trial", {
|
||||||
provider: "local",
|
method: "POST",
|
||||||
previewOnly: false,
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
body: JSON.stringify({
|
||||||
|
name: draft.name,
|
||||||
|
instruction,
|
||||||
|
source_request_id: draft.source_request_id,
|
||||||
|
vehicle_id: draft.vehicle_id || state.vehicleId || null,
|
||||||
|
}),
|
||||||
|
timeoutMs: 30 * 1000,
|
||||||
|
retries: 0,
|
||||||
|
});
|
||||||
|
payload = await response.json();
|
||||||
|
if (response.status === 401) {
|
||||||
|
window.location.replace("/login");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(payload.message || payload.error || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
({ payload } = await requestNaturalLanguageControl(instruction, {
|
||||||
|
executionMode: "dry_run",
|
||||||
|
clientType: "assistant_mode_trial",
|
||||||
|
provider: "local",
|
||||||
|
previewOnly: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
if (!payload) throw new Error("模拟试运行没有返回结果。");
|
if (!payload) throw new Error("模拟试运行没有返回结果。");
|
||||||
const actualOperationCount = Object.values(payload.function_configs || {})
|
const actualOperationCount = Object.values(payload.function_configs || {})
|
||||||
.reduce((total, config) => total + (config?.spec?.operations?.length || 0), 0);
|
.reduce((total, config) => total + (config?.spec?.operations?.length || 0), 0);
|
||||||
const expectedOperationCount = Number(
|
const expectedOperationCount = Number(
|
||||||
typeof draft === "object" ? draft?.operation_count : 0,
|
typeof draft === "object" ? draft?.operation_count : 0,
|
||||||
);
|
);
|
||||||
|
const actionSignature = (action) => {
|
||||||
|
const target = action?.target && typeof action.target === "object"
|
||||||
|
? action.target.instance || action.target.zone || action.target.device || ""
|
||||||
|
: action?.target || "";
|
||||||
|
const parameters = action?.parameters && typeof action.parameters === "object"
|
||||||
|
? Object.fromEntries(Object.entries(action.parameters).sort(([left], [right]) => left.localeCompare(right)))
|
||||||
|
: {};
|
||||||
|
return JSON.stringify([
|
||||||
|
String(action?.domain || ""),
|
||||||
|
String(action?.capability || ""),
|
||||||
|
String(target),
|
||||||
|
parameters,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
const expectedSignatures = Array.isArray(draft?.actions)
|
||||||
|
? draft.actions.map(actionSignature).sort()
|
||||||
|
: [];
|
||||||
|
const actualActions = Array.isArray(payload?.intent_ir?.spec?.actions)
|
||||||
|
? payload.intent_ir.spec.actions
|
||||||
|
: [];
|
||||||
|
const actualSignatures = actualActions.map(actionSignature).sort();
|
||||||
|
const sameActions = expectedSignatures.length === 0
|
||||||
|
|| (expectedSignatures.length === actualSignatures.length
|
||||||
|
&& expectedSignatures.every((signature, index) => signature === actualSignatures[index]));
|
||||||
if (payload.status !== "succeeded"
|
if (payload.status !== "succeeded"
|
||||||
|| (expectedOperationCount > 0 && actualOperationCount < expectedOperationCount)) {
|
|| (expectedOperationCount > 0 && actualOperationCount < expectedOperationCount)
|
||||||
|
|| !sameActions) {
|
||||||
throw new Error("试运行未完整覆盖草稿动作,请重新生成模式预览。");
|
throw new Error("试运行未完整覆盖草稿动作,请重新生成模式预览。");
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
|
|||||||
@@ -271,7 +271,7 @@
|
|||||||
followLatest();
|
followLatest();
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendModeCreatedState(message, profile) {
|
function appendModeCreatedState(message, profile, { updated = false } = {}) {
|
||||||
if (!message?.body || !profile?.id) return;
|
if (!message?.body || !profile?.id) return;
|
||||||
const selector = `[data-assistant-mode-created="${CSS.escape(String(profile.id))}"]`;
|
const selector = `[data-assistant-mode-created="${CSS.escape(String(profile.id))}"]`;
|
||||||
if ($(selector, message.body)) return;
|
if ($(selector, message.body)) return;
|
||||||
@@ -286,7 +286,7 @@
|
|||||||
const copy = document.createElement("span");
|
const copy = document.createElement("span");
|
||||||
copy.className = "assistant-mode-created-copy";
|
copy.className = "assistant-mode-created-copy";
|
||||||
const label = document.createElement("small");
|
const label = document.createElement("small");
|
||||||
label.textContent = "已添加到「我的模式」";
|
label.textContent = updated ? "已更新「我的模式」" : "已添加到「我的模式」";
|
||||||
const name = document.createElement("strong");
|
const name = document.createElement("strong");
|
||||||
name.textContent = String(profile.name || "新功能模式");
|
name.textContent = String(profile.name || "新功能模式");
|
||||||
copy.append(label, name);
|
copy.append(label, name);
|
||||||
@@ -396,9 +396,16 @@
|
|||||||
icon = "panels-top-left";
|
icon = "panels-top-left";
|
||||||
order = instructionOrder("车窗");
|
order = instructionOrder("车窗");
|
||||||
} else if (domain === "seat") {
|
} else if (domain === "seat") {
|
||||||
label = ["passenger", "front_right"].includes(zone) ? "副驾座椅" : "主驾座椅";
|
const seatLabel = ["passenger", "front_right"].includes(zone) ? "副驾" : "主驾";
|
||||||
|
const seatCapabilityLabels = {
|
||||||
|
"seat.set_longitudinal_position": "座椅前后位置",
|
||||||
|
"seat.set_cushion_tilt": "坐垫角度",
|
||||||
|
"seat.set_backrest_recline": "靠背角度",
|
||||||
|
"seat.set_legrest_height": "腿托高度",
|
||||||
|
};
|
||||||
|
label = `${seatLabel}${seatCapabilityLabels[capability] || "座椅"}`;
|
||||||
value = params.position_pct != null
|
value = params.position_pct != null
|
||||||
? `${params.position_pct}%`
|
? `${params.position_pct}%${params.angle_deg != null ? `(${params.angle_deg}°)` : ""}`
|
||||||
: motionLabel(params.state || params.mode_id);
|
: motionLabel(params.state || params.mode_id);
|
||||||
icon = "armchair";
|
icon = "armchair";
|
||||||
order = instructionOrder("座椅", "副驾", "主驾");
|
order = instructionOrder("座椅", "副驾", "主驾");
|
||||||
@@ -408,8 +415,12 @@
|
|||||||
icon = "thermometer";
|
icon = "thermometer";
|
||||||
order = instructionOrder("空调", "温度");
|
order = instructionOrder("空调", "温度");
|
||||||
} else if (domain === "climate") {
|
} else if (domain === "climate") {
|
||||||
label = "空调";
|
const cycleLabels = { internal: "内循环", external: "外循环", auto: "自动循环" };
|
||||||
value = params.level != null ? `${params.level}档` : motionLabel(params.state);
|
label = capability === "climate.set_fan_level" ? "空调风量"
|
||||||
|
: capability === "climate.set_cycle_mode" ? "空调循环" : "空调";
|
||||||
|
value = params.level != null ? `${params.level}档`
|
||||||
|
: params.mode != null ? (cycleLabels[String(params.mode)] || String(params.mode))
|
||||||
|
: motionLabel(params.state);
|
||||||
icon = "fan";
|
icon = "fan";
|
||||||
order = instructionOrder("空调", "风量");
|
order = instructionOrder("空调", "风量");
|
||||||
} else if (capability === "interior_light.set_reading_power") {
|
} else if (capability === "interior_light.set_reading_power") {
|
||||||
@@ -433,9 +444,15 @@
|
|||||||
card.classList.add("is-busy");
|
card.classList.add("is-busy");
|
||||||
status.hidden = false;
|
status.hidden = false;
|
||||||
status.className = "assistant-mode-draft-status working";
|
status.className = "assistant-mode-draft-status working";
|
||||||
status.textContent = "正在保存到「我的模式」…";
|
const existingModeId = String(draft.existing_mode_id || "").trim();
|
||||||
|
status.textContent = existingModeId
|
||||||
|
? "正在将验证结果保存为模式更新…"
|
||||||
|
: "正在保存到「我的模式」…";
|
||||||
try {
|
try {
|
||||||
const response = await window.CognitiveAgentNetwork.request("/api/mode-profiles", {
|
const endpoint = existingModeId
|
||||||
|
? `/api/mode-profiles/${encodeURIComponent(existingModeId)}`
|
||||||
|
: "/api/mode-profiles";
|
||||||
|
const response = await window.CognitiveAgentNetwork.request(endpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -456,10 +473,10 @@
|
|||||||
card.classList.remove("is-busy");
|
card.classList.remove("is-busy");
|
||||||
card.classList.add("is-saved");
|
card.classList.add("is-saved");
|
||||||
status.className = "assistant-mode-draft-status success";
|
status.className = "assistant-mode-draft-status success";
|
||||||
status.textContent = "模式已保存";
|
status.textContent = existingModeId ? "模式已更新" : "模式已保存";
|
||||||
card.querySelector(".assistant-mode-draft-actions")?.setAttribute("hidden", "");
|
card.querySelector(".assistant-mode-draft-actions")?.setAttribute("hidden", "");
|
||||||
await window.CognitiveAgentConsole?.refreshModeProfiles?.(payload.mode_profile?.id);
|
await window.CognitiveAgentConsole?.refreshModeProfiles?.(payload.mode_profile?.id);
|
||||||
appendModeCreatedState(message, payload.mode_profile);
|
appendModeCreatedState(message, payload.mode_profile, { updated: Boolean(existingModeId) });
|
||||||
persistMessages();
|
persistMessages();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
card.classList.remove("is-busy");
|
card.classList.remove("is-busy");
|
||||||
@@ -469,14 +486,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tryModeProfileDraft(card, draft, buttons, status) {
|
async function tryModeProfileDraft(card, draft, actions, buttons, status) {
|
||||||
buttons.forEach((button) => { button.disabled = true; });
|
buttons.forEach((button) => { button.disabled = true; });
|
||||||
card.classList.add("is-busy");
|
card.classList.add("is-busy");
|
||||||
status.hidden = false;
|
status.hidden = false;
|
||||||
status.className = "assistant-mode-draft-status working";
|
status.className = "assistant-mode-draft-status working";
|
||||||
status.textContent = "正在进行模拟试运行…";
|
status.textContent = "正在进行模拟试运行…";
|
||||||
try {
|
try {
|
||||||
const result = await window.CognitiveAgentConsole?.tryModeDraft?.(draft);
|
const result = await window.CognitiveAgentConsole?.tryModeDraft?.({ ...draft, actions });
|
||||||
if (!result || result.status !== "succeeded") throw new Error("试运行未通过,请检查动作设置。");
|
if (!result || result.status !== "succeeded") throw new Error("试运行未通过,请检查动作设置。");
|
||||||
status.className = "assistant-mode-draft-status success";
|
status.className = "assistant-mode-draft-status success";
|
||||||
status.textContent = "试运行通过 · 尚未保存";
|
status.textContent = "试运行通过 · 尚未保存";
|
||||||
@@ -501,7 +518,9 @@
|
|||||||
const heading = document.createElement("header");
|
const heading = document.createElement("header");
|
||||||
const headingCopy = document.createElement("span");
|
const headingCopy = document.createElement("span");
|
||||||
const eyebrow = document.createElement("small");
|
const eyebrow = document.createElement("small");
|
||||||
eyebrow.textContent = "已为你生成功能模式";
|
eyebrow.textContent = draft.existing_mode_id
|
||||||
|
? "已载入现有功能模式"
|
||||||
|
: "已为你生成功能模式";
|
||||||
const title = document.createElement("strong");
|
const title = document.createElement("strong");
|
||||||
title.textContent = `「${draft.name}」`;
|
title.textContent = `「${draft.name}」`;
|
||||||
headingCopy.append(eyebrow, title);
|
headingCopy.append(eyebrow, title);
|
||||||
@@ -530,7 +549,9 @@
|
|||||||
const save = document.createElement("button");
|
const save = document.createElement("button");
|
||||||
save.type = "button";
|
save.type = "button";
|
||||||
save.className = "assistant-mode-draft-save";
|
save.className = "assistant-mode-draft-save";
|
||||||
save.innerHTML = '<i data-lucide="bookmark-plus"></i><span>保存为功能模式</span>';
|
save.innerHTML = draft.existing_mode_id
|
||||||
|
? '<i data-lucide="refresh-cw"></i><span>确认更新此模式</span>'
|
||||||
|
: '<i data-lucide="bookmark-plus"></i><span>保存为功能模式</span>';
|
||||||
const trial = document.createElement("button");
|
const trial = document.createElement("button");
|
||||||
trial.type = "button";
|
trial.type = "button";
|
||||||
trial.className = "assistant-mode-draft-trial";
|
trial.className = "assistant-mode-draft-trial";
|
||||||
@@ -545,7 +566,7 @@
|
|||||||
void saveModeProfileDraft(message, card, draft, buttons, status);
|
void saveModeProfileDraft(message, card, draft, buttons, status);
|
||||||
});
|
});
|
||||||
trial.addEventListener("click", () => {
|
trial.addEventListener("click", () => {
|
||||||
void tryModeProfileDraft(card, draft, buttons, status);
|
void tryModeProfileDraft(card, draft, actions, buttons, status);
|
||||||
});
|
});
|
||||||
|
|
||||||
card.append(heading, list, controls, status);
|
card.append(heading, list, controls, status);
|
||||||
|
|||||||
@@ -20,13 +20,13 @@
|
|||||||
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
|
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
|
||||||
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
|
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
|
||||||
<script src="/network.js?v=android-pad-2" defer></script>
|
<script src="/network.js?v=android-pad-2" defer></script>
|
||||||
<script src="/app.js?v=dual-seat-126" defer></script>
|
<script src="/app.js?v=dual-seat-127" defer></script>
|
||||||
<script src="/speech-input.js?v=sensevoice-12" defer></script>
|
<script src="/speech-input.js?v=sensevoice-12" defer></script>
|
||||||
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
|
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
|
||||||
<script src="/pwa.js?v=android-pad-3" defer></script>
|
<script src="/pwa.js?v=android-pad-3" defer></script>
|
||||||
<script src="/console-v2.js?v=delivery-state-62" defer></script>
|
<script src="/console-v2.js?v=delivery-state-62" defer></script>
|
||||||
<script src="/self-extension.js?v=gscu-tcp-43" defer></script>
|
<script src="/self-extension.js?v=gscu-tcp-43" defer></script>
|
||||||
<script src="/assistant-chat.js?v=codex-assistant-20" defer></script>
|
<script src="/assistant-chat.js?v=codex-assistant-22" defer></script>
|
||||||
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
|
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="console-v2">
|
<body class="console-v2">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_VERSION = "cognitive-agent-pad-v269";
|
const CACHE_VERSION = "cognitive-agent-pad-v271";
|
||||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||||
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||||
const APP_SHELL = [
|
const APP_SHELL = [
|
||||||
@@ -6,7 +6,7 @@ const APP_SHELL = [
|
|||||||
"/index.html",
|
"/index.html",
|
||||||
"/styles.css?v=dual-seat-23",
|
"/styles.css?v=dual-seat-23",
|
||||||
"/network.js?v=android-pad-2",
|
"/network.js?v=android-pad-2",
|
||||||
"/app.js?v=dual-seat-126",
|
"/app.js?v=dual-seat-127",
|
||||||
"/gesture-control.js?v=android-pad-9",
|
"/gesture-control.js?v=android-pad-9",
|
||||||
"/pwa.js?v=android-pad-3",
|
"/pwa.js?v=android-pad-3",
|
||||||
"/console-v2.css?v=delivery-state-65",
|
"/console-v2.css?v=delivery-state-65",
|
||||||
@@ -17,7 +17,7 @@ const APP_SHELL = [
|
|||||||
"/speech-input.js?v=sensevoice-12",
|
"/speech-input.js?v=sensevoice-12",
|
||||||
"/speech-audio-worklet.js?v=sensevoice-5",
|
"/speech-audio-worklet.js?v=sensevoice-5",
|
||||||
"/assistant-chat.css?v=codex-assistant-22",
|
"/assistant-chat.css?v=codex-assistant-22",
|
||||||
"/assistant-chat.js?v=codex-assistant-20",
|
"/assistant-chat.js?v=codex-assistant-22",
|
||||||
"/energy-monitor.css?v=energy-monitor-4",
|
"/energy-monitor.css?v=energy-monitor-4",
|
||||||
"/energy-monitor.js?v=energy-monitor-4",
|
"/energy-monitor.js?v=energy-monitor-4",
|
||||||
"/assets/pets/self-extension-cat.png?v=3",
|
"/assets/pets/self-extension-cat.png?v=3",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ RUN node --version \
|
|||||||
/app/artifacts/server \
|
/app/artifacts/server \
|
||||||
/app/artifacts/tmp \
|
/app/artifacts/tmp \
|
||||||
/app/artifacts/self-repair \
|
/app/artifacts/self-repair \
|
||||||
|
/app/artifacts/mode-profiles \
|
||||||
&& chown -R 10001:10001 /var/lib/codex /app/artifacts /home/cognitive-agent
|
&& chown -R 10001:10001 /var/lib/codex /app/artifacts /home/cognitive-agent
|
||||||
|
|
||||||
USER 10001:10001
|
USER 10001:10001
|
||||||
|
|||||||
+10
-6
@@ -16,11 +16,15 @@ The server layout is:
|
|||||||
├── artifacts/user-presets/
|
├── artifacts/user-presets/
|
||||||
├── artifacts/server/ # GSCU infrared catalog and server-side state
|
├── artifacts/server/ # GSCU infrared catalog and server-side state
|
||||||
├── artifacts/tmp/ # Runtime gateway and calibration state
|
├── artifacts/tmp/ # Runtime gateway and calibration state
|
||||||
└── artifacts/self-repair/
|
├── artifacts/self-repair/
|
||||||
|
└── artifacts/mode-profiles/ # User-created modes for all vehicles
|
||||||
```
|
```
|
||||||
|
|
||||||
Vehicle-specific user-created function modes are persisted from
|
User-created function modes are persisted from the ignored host path
|
||||||
`config/modes/vehicles/denza_n7/user/` into the otherwise read-only container.
|
`artifacts/mode-profiles/` into the service's separate writable user-mode
|
||||||
|
registry at `/app/artifacts/mode-profiles/`, including common and future
|
||||||
|
vehicle-specific modes.
|
||||||
|
Preset files under `/app/config/modes` remain read-only and are never modified.
|
||||||
|
|
||||||
Build and start the service:
|
Build and start the service:
|
||||||
|
|
||||||
@@ -32,21 +36,21 @@ mkdir -p \
|
|||||||
artifacts/server \
|
artifacts/server \
|
||||||
artifacts/tmp \
|
artifacts/tmp \
|
||||||
artifacts/self-repair \
|
artifacts/self-repair \
|
||||||
config/modes/vehicles/denza_n7/user
|
artifacts/mode-profiles
|
||||||
chown -R 10001:10001 \
|
chown -R 10001:10001 \
|
||||||
artifacts/runs \
|
artifacts/runs \
|
||||||
artifacts/user-presets \
|
artifacts/user-presets \
|
||||||
artifacts/server \
|
artifacts/server \
|
||||||
artifacts/tmp \
|
artifacts/tmp \
|
||||||
artifacts/self-repair \
|
artifacts/self-repair \
|
||||||
config/modes/vehicles/denza_n7/user
|
artifacts/mode-profiles
|
||||||
chmod 0700 \
|
chmod 0700 \
|
||||||
artifacts/runs \
|
artifacts/runs \
|
||||||
artifacts/user-presets \
|
artifacts/user-presets \
|
||||||
artifacts/server \
|
artifacts/server \
|
||||||
artifacts/tmp \
|
artifacts/tmp \
|
||||||
artifacts/self-repair \
|
artifacts/self-repair \
|
||||||
config/modes/vehicles/denza_n7/user
|
artifacts/mode-profiles
|
||||||
docker compose -f deploy/server/compose.yaml up -d --build
|
docker compose -f deploy/server/compose.yaml up -d --build
|
||||||
docker compose -f deploy/server/compose.yaml ps
|
docker compose -f deploy/server/compose.yaml ps
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ services:
|
|||||||
- ../../artifacts/tmp:/app/artifacts/tmp
|
- ../../artifacts/tmp:/app/artifacts/tmp
|
||||||
- ../../artifacts/self-repair:/app/artifacts/self-repair
|
- ../../artifacts/self-repair:/app/artifacts/self-repair
|
||||||
- ../../artifacts/models:/app/artifacts/models:ro
|
- ../../artifacts/models:/app/artifacts/models:ro
|
||||||
- ../../config/modes/vehicles/denza_n7/user:/app/config/modes/vehicles/denza_n7/user
|
- ../../artifacts/mode-profiles:/app/artifacts/mode-profiles
|
||||||
- codex_home:/var/lib/codex
|
- codex_home:/var/lib/codex
|
||||||
read_only: true
|
read_only: true
|
||||||
tmpfs:
|
tmpfs:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
- 只有 `n7_vehicle.execute_cockpit_request` 可以产生车辆、座椅、座舱模式或已授权外设的副作用。该工具不接受模型生成的控制文本或原始 CAN;它只执行服务端绑定到本回合令牌的原始需求或服务端规范化的舒适性动作。禁止用 shell、文件修改、原始 CAN 帧或其他工具绕过它。
|
- 只有 `n7_vehicle.execute_cockpit_request` 可以产生车辆、座椅、座舱模式或已授权外设的副作用。该工具不接受模型生成的控制文本或原始 CAN;它只执行服务端绑定到本回合令牌的原始需求或服务端规范化的舒适性动作。禁止用 shell、文件修改、原始 CAN 帧或其他工具绕过它。
|
||||||
- 对目录中的原子设备控制,调用工具时同时提交结构化 `plan`:只能逐字使用本回合能力目录中 `assistant_callable=true` 的 capability、target scope 和参数契约。对于目录中已有的座舱模式或当前 NFC 卡已授权的红外外设,省略 `plan`,由应用按动态注册表解析。结构化计划只是候选数据,应用会再次执行原始文本证据校验、Schema 校验和安全校验;本地确定性解析与计划重叠时,以本地结果为准。
|
- 对目录中的原子设备控制,调用工具时同时提交结构化 `plan`:只能逐字使用本回合能力目录中 `assistant_callable=true` 的 capability、target scope 和参数契约。对于目录中已有的座舱模式或当前 NFC 卡已授权的红外外设,省略 `plan`,由应用按动态注册表解析。结构化计划只是候选数据,应用会再次执行原始文本证据校验、Schema 校验和安全校验;本地确定性解析与计划重叠时,以本地结果为准。
|
||||||
|
- 用户仅给出一个尚未登记的功能模式名称时,应结合该场景的通常含义,从本回合能力目录中选择少量、明确且相互协调的 `assistant_callable=true` 原子能力,提交一个结构化 `plan`。每项动作必须是可重复的绝对 `set` 状态,不得使用相对增减、时长动作、动态/循环模式、保存或应用操作;同一能力与目标不得重复。若任何动作或参数有歧义,就不要提交残缺计划。不得引用目录外能力,不得生成原始 CAN,不得声称已经保存或真实执行;应用会把整组计划原子校验为 dry-run 动作预览,用户确认后才会保存。已有模式名称不要重新推测动作,应省略 `plan` 复用工程配置。
|
||||||
- 用户明确要求控制车辆,或直接表达当前座舱不适并期待处理时,必须调用该工具;不要只给建议,也不要要求用户先选择“问答/车控”模式。
|
- 用户明确要求控制车辆,或直接表达当前座舱不适并期待处理时,必须调用该工具;不要只给建议,也不要要求用户先选择“问答/车控”模式。
|
||||||
- “车上有点热”“有点热”“太热了”等当前不适,在没有否定、假设或纯知识提问时,直接调用一次工具;具体规范动作由服务端决定。
|
- “车上有点热”“有点热”“太热了”等当前不适,在没有否定、假设或纯知识提问时,直接调用一次工具;具体规范动作由服务端决定。
|
||||||
- “如果车上有点热应该怎么办”“为什么暴晒后车里会热”“车上有点热,但不要调整设备”等是假设、知识问题或否定请求,不得执行。
|
- “如果车上有点热应该怎么办”“为什么暴晒后车里会热”“车上有点热,但不要调整设备”等是假设、知识问题或否定请求,不得执行。
|
||||||
|
|||||||
@@ -850,7 +850,14 @@ class AssistantCapabilityCatalog:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
compact_modes = [
|
compact_modes = [
|
||||||
{"id": item.get("id"), "name": item.get("name"), "aliases": item.get("aliases")}
|
{
|
||||||
|
"id": item.get("id"),
|
||||||
|
"name": item.get("name"),
|
||||||
|
"aliases": item.get("aliases"),
|
||||||
|
"description": item.get("description"),
|
||||||
|
"domains": item.get("domains"),
|
||||||
|
"operation_ids": item.get("operation_ids"),
|
||||||
|
}
|
||||||
for item in catalog.get("modes", [])
|
for item in catalog.get("modes", [])
|
||||||
if isinstance(item, Mapping)
|
if isinstance(item, Mapping)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -30,6 +30,47 @@ MAX_PLAN_REQUESTS = 12
|
|||||||
MAX_PLAN_PARAMETERS = 12
|
MAX_PLAN_PARAMETERS = 12
|
||||||
|
|
||||||
_OPERATIONS = frozenset({"activate", "deactivate", "increase", "decrease", "set", "save", "apply"})
|
_OPERATIONS = frozenset({"activate", "deactivate", "increase", "decrease", "set", "save", "apply"})
|
||||||
|
_SCENE_COMPOSABLE_CAPABILITIES = frozenset(
|
||||||
|
{
|
||||||
|
"seat.set_longitudinal_position",
|
||||||
|
"seat.set_cushion_tilt",
|
||||||
|
"seat.set_backrest_recline",
|
||||||
|
"seat.set_legrest_height",
|
||||||
|
"seat.set_legrest_extension",
|
||||||
|
"seat.set_ventilation",
|
||||||
|
"seat.set_heating",
|
||||||
|
"climate.set_power",
|
||||||
|
"climate.set_temperature",
|
||||||
|
"climate.set_fan_level",
|
||||||
|
"climate.set_cycle_mode",
|
||||||
|
"climate.set_control_mode",
|
||||||
|
"climate.set_outlet_mode",
|
||||||
|
"climate.set_front_defrost",
|
||||||
|
"roof.set_shade_position",
|
||||||
|
"roof.set_transmittance",
|
||||||
|
"roof.set_automatic_dimming",
|
||||||
|
"window.set_position",
|
||||||
|
"interior_light.set_indoor_power",
|
||||||
|
"interior_light.set_reading_power",
|
||||||
|
"interior_light.set_wall_power",
|
||||||
|
"ambient_light.set_power",
|
||||||
|
"ambient_light.set_illumination",
|
||||||
|
"ambient_light.set_mode",
|
||||||
|
"ambient_light.set_color_index",
|
||||||
|
"ambient_light.set_brightness_level",
|
||||||
|
"outlet.set_switch_state",
|
||||||
|
"outlet.set_position_percent",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# Reusable scenes are replayed through the native compiler after the user saves
|
||||||
|
# them. Keep scene-only parameter bounds aligned with that compiler even when
|
||||||
|
# a broader virtual capability contract accepts additional values. In
|
||||||
|
# particular, fan level 0 is a virtual shorthand for "off", while the native
|
||||||
|
# operation is explicitly 1-7; changing it to HVAC power-off here would alter
|
||||||
|
# the model's proposed intent.
|
||||||
|
_SCENE_PARAMETER_BOUNDS: dict[tuple[str, str], tuple[float | None, float | None]] = {
|
||||||
|
("climate.set_fan_level", "level"): (1, 7),
|
||||||
|
}
|
||||||
_ROOT_FIELDS = frozenset(
|
_ROOT_FIELDS = frozenset(
|
||||||
{"detected_domains", "requests", "ambiguities", "decision_summary", "confidence"}
|
{"detected_domains", "requests", "ambiguities", "decision_summary", "confidence"}
|
||||||
)
|
)
|
||||||
@@ -404,6 +445,123 @@ def _registered_capabilities(
|
|||||||
return registered
|
return registered
|
||||||
|
|
||||||
|
|
||||||
|
def _registered_capability_specs(
|
||||||
|
capability_context: Mapping[str, Any],
|
||||||
|
executable_capabilities: AbstractSet[str],
|
||||||
|
) -> dict[str, tuple[str, Mapping[str, Any]]]:
|
||||||
|
registered: dict[str, tuple[str, Mapping[str, Any]]] = {}
|
||||||
|
domains = capability_context.get("domains", [])
|
||||||
|
if not isinstance(domains, Sequence) or isinstance(
|
||||||
|
domains,
|
||||||
|
(str, bytes, bytearray),
|
||||||
|
):
|
||||||
|
return registered
|
||||||
|
for raw_domain in domains:
|
||||||
|
if not isinstance(raw_domain, Mapping):
|
||||||
|
continue
|
||||||
|
domain = _safe_text(raw_domain.get("domain"), limit=64)
|
||||||
|
capabilities = raw_domain.get("capabilities", [])
|
||||||
|
if not isinstance(capabilities, Sequence) or isinstance(
|
||||||
|
capabilities,
|
||||||
|
(str, bytes, bytearray),
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
for raw_capability in capabilities:
|
||||||
|
if not isinstance(raw_capability, Mapping):
|
||||||
|
continue
|
||||||
|
capability = _safe_text(raw_capability.get("name"), limit=128)
|
||||||
|
if capability and domain and capability in executable_capabilities:
|
||||||
|
registered[capability] = (domain, raw_capability)
|
||||||
|
return registered
|
||||||
|
|
||||||
|
|
||||||
|
def _scene_contract_error(
|
||||||
|
capability: str,
|
||||||
|
target_scope: str,
|
||||||
|
parameters: Sequence[Mapping[str, Any]],
|
||||||
|
specification: Mapping[str, Any],
|
||||||
|
) -> str | None:
|
||||||
|
"""Validate a scene request before adapter/native routing can drop it."""
|
||||||
|
|
||||||
|
target_scopes = specification.get("target_scopes", [])
|
||||||
|
if (
|
||||||
|
not isinstance(target_scopes, Sequence)
|
||||||
|
or isinstance(target_scopes, (str, bytes, bytearray))
|
||||||
|
or target_scope not in {str(item) for item in target_scopes}
|
||||||
|
):
|
||||||
|
return f"能力 {capability} 不支持目标范围 {target_scope!r}"
|
||||||
|
|
||||||
|
raw_specs = specification.get("parameters", [])
|
||||||
|
if not isinstance(raw_specs, Sequence) or isinstance(
|
||||||
|
raw_specs,
|
||||||
|
(str, bytes, bytearray),
|
||||||
|
):
|
||||||
|
return f"能力 {capability} 的参数契约不可用"
|
||||||
|
specs = {
|
||||||
|
str(item.get("name")): item
|
||||||
|
for item in raw_specs
|
||||||
|
if isinstance(item, Mapping) and str(item.get("name") or "").strip()
|
||||||
|
}
|
||||||
|
values = {str(item["name"]): item.get("value") for item in parameters}
|
||||||
|
unknown = sorted(set(values) - set(specs))
|
||||||
|
if unknown:
|
||||||
|
return f"能力 {capability} 不支持参数 {', '.join(unknown)}"
|
||||||
|
relative = sorted(
|
||||||
|
name
|
||||||
|
for name in values
|
||||||
|
if name in {"distance_mm", "angle_delta_deg"} or "delta" in name
|
||||||
|
)
|
||||||
|
if relative:
|
||||||
|
return f"功能模式不接受相对参数 {', '.join(relative)}"
|
||||||
|
if capability.startswith("seat.set_") and len(
|
||||||
|
{
|
||||||
|
name
|
||||||
|
for name in ("position_pct", "angle_deg")
|
||||||
|
if values.get(name) is not None
|
||||||
|
}
|
||||||
|
) > 1:
|
||||||
|
return "座椅绝对位置只能使用 position_pct 或 angle_deg 中的一种表示"
|
||||||
|
|
||||||
|
for name, spec in specs.items():
|
||||||
|
value = values.get(name)
|
||||||
|
if spec.get("required") and value is None:
|
||||||
|
return f"能力 {capability} 缺少必填参数 {name}"
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
expected_type = str(spec.get("type") or "")
|
||||||
|
numeric = isinstance(value, (int, float)) and not isinstance(value, bool)
|
||||||
|
type_valid = {
|
||||||
|
"number": numeric,
|
||||||
|
"integer": numeric and float(value).is_integer(),
|
||||||
|
"string": isinstance(value, str),
|
||||||
|
"boolean": isinstance(value, bool),
|
||||||
|
}.get(expected_type, False)
|
||||||
|
if not type_valid:
|
||||||
|
return f"参数 {name} 的类型必须是 {expected_type}"
|
||||||
|
minimum = spec.get("minimum")
|
||||||
|
maximum = spec.get("maximum")
|
||||||
|
scene_bounds = _SCENE_PARAMETER_BOUNDS.get((capability, name))
|
||||||
|
if scene_bounds is not None:
|
||||||
|
minimum, maximum = scene_bounds
|
||||||
|
if numeric and (
|
||||||
|
(minimum is not None and float(value) < float(minimum))
|
||||||
|
or (maximum is not None and float(value) > float(maximum))
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
f"参数 {name} 超出能力 {capability} 的允许范围 "
|
||||||
|
f"{minimum if minimum is not None else '-'}-"
|
||||||
|
f"{maximum if maximum is not None else '+'}"
|
||||||
|
)
|
||||||
|
allowed = spec.get("allowed_values")
|
||||||
|
if (
|
||||||
|
isinstance(allowed, Sequence)
|
||||||
|
and not isinstance(allowed, (str, bytes, bytearray))
|
||||||
|
and value not in allowed
|
||||||
|
):
|
||||||
|
return f"参数 {name} 不在能力 {capability} 的允许值内"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _relevant_text(capability: str, text: str) -> str:
|
def _relevant_text(capability: str, text: str) -> str:
|
||||||
"""Narrow semantic checks to clauses mentioning this capability's subject."""
|
"""Narrow semantic checks to clauses mentioning this capability's subject."""
|
||||||
|
|
||||||
@@ -920,12 +1078,16 @@ def guard_assistant_plan(
|
|||||||
executable_capabilities: AbstractSet[str],
|
executable_capabilities: AbstractSet[str],
|
||||||
fallback_intents: Sequence[Mapping[str, Any]] = (),
|
fallback_intents: Sequence[Mapping[str, Any]] = (),
|
||||||
trusted_local: Mapping[str, Any] | None = None,
|
trusted_local: Mapping[str, Any] | None = None,
|
||||||
|
allow_scene_composition: bool = False,
|
||||||
) -> GuardedAssistantPlan:
|
) -> GuardedAssistantPlan:
|
||||||
"""Authorize a Codex plan against the server-owned unresolved text.
|
"""Authorize a Codex plan against the server-owned unresolved text.
|
||||||
|
|
||||||
Rejected capability requests are omitted and represented as adapter-style
|
Rejected capability requests are omitted and represented as adapter-style
|
||||||
ambiguities. Structurally malformed plans raise ``ValueError`` so callers
|
ambiguities. Structurally malformed plans raise ``ValueError`` so callers
|
||||||
can fail the tool call without invoking any executor.
|
can fail the tool call without invoking any executor. ``allow_scene_composition``
|
||||||
|
is reserved for a name-only mode-creation preview: it relaxes textual value
|
||||||
|
evidence, but never registry membership, schema/range validation, dry-run,
|
||||||
|
or the user's explicit save confirmation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not isinstance(raw_plan, Mapping) or set(raw_plan) != _ROOT_FIELDS:
|
if not isinstance(raw_plan, Mapping) or set(raw_plan) != _ROOT_FIELDS:
|
||||||
@@ -948,6 +1110,10 @@ def guard_assistant_plan(
|
|||||||
capability_context,
|
capability_context,
|
||||||
executable_capabilities,
|
executable_capabilities,
|
||||||
)
|
)
|
||||||
|
registered_specs = _registered_capability_specs(
|
||||||
|
capability_context,
|
||||||
|
executable_capabilities,
|
||||||
|
)
|
||||||
parser_claims: set[str] = set()
|
parser_claims: set[str] = set()
|
||||||
for intent in fallback_intents:
|
for intent in fallback_intents:
|
||||||
if isinstance(intent, Mapping):
|
if isinstance(intent, Mapping):
|
||||||
@@ -957,6 +1123,21 @@ def guard_assistant_plan(
|
|||||||
|
|
||||||
accepted: list[dict[str, Any]] = []
|
accepted: list[dict[str, Any]] = []
|
||||||
rejected: list[dict[str, str]] = []
|
rejected: list[dict[str, str]] = []
|
||||||
|
raw_ambiguities = raw_plan.get("ambiguities", [])
|
||||||
|
if allow_scene_composition:
|
||||||
|
if not isinstance(raw_ambiguities, Sequence) or isinstance(
|
||||||
|
raw_ambiguities,
|
||||||
|
(str, bytes, bytearray),
|
||||||
|
):
|
||||||
|
raise ValueError("assistant scene plan ambiguities must be an array")
|
||||||
|
if raw_ambiguities:
|
||||||
|
rejected.append(
|
||||||
|
{
|
||||||
|
"field": "ambiguities",
|
||||||
|
"question": "场景候选计划仍有未解决歧义,已拒绝生成部分模式。",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
scene_keys: set[tuple[str, str, str]] = set()
|
||||||
for index, raw_request in enumerate(requests):
|
for index, raw_request in enumerate(requests):
|
||||||
if not isinstance(raw_request, Mapping) or set(raw_request) != _REQUEST_FIELDS:
|
if not isinstance(raw_request, Mapping) or set(raw_request) != _REQUEST_FIELDS:
|
||||||
raise ValueError(f"assistant plan request {index} is invalid")
|
raise ValueError(f"assistant plan request {index} is invalid")
|
||||||
@@ -974,11 +1155,30 @@ def guard_assistant_plan(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
if allow_scene_composition and capability not in _SCENE_COMPOSABLE_CAPABILITIES:
|
||||||
|
rejected.append(
|
||||||
|
{
|
||||||
|
"field": f"requests[{index}].capability",
|
||||||
|
"question": "该能力不是可重复功能模式允许组合的绝对状态能力。",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if allow_scene_composition and operation != "set":
|
||||||
|
rejected.append(
|
||||||
|
{
|
||||||
|
"field": f"requests[{index}].operation",
|
||||||
|
"question": "功能模式只接受可重复的绝对 set 操作。",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
request_text = _relevant_text(capability, text)
|
request_text = _relevant_text(capability, text)
|
||||||
if non_execution or not _has_explicit_evidence(
|
if non_execution or (
|
||||||
capability,
|
not allow_scene_composition
|
||||||
request_text,
|
and not _has_explicit_evidence(
|
||||||
parser_claims,
|
capability,
|
||||||
|
request_text,
|
||||||
|
parser_claims,
|
||||||
|
)
|
||||||
):
|
):
|
||||||
rejected.append(
|
rejected.append(
|
||||||
{
|
{
|
||||||
@@ -987,7 +1187,10 @@ def guard_assistant_plan(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if not _target_is_evidenced(capability, target_scope, request_text):
|
if (
|
||||||
|
not allow_scene_composition
|
||||||
|
and not _target_is_evidenced(capability, target_scope, request_text)
|
||||||
|
):
|
||||||
rejected.append(
|
rejected.append(
|
||||||
{
|
{
|
||||||
"field": f"requests[{index}].target_scope",
|
"field": f"requests[{index}].target_scope",
|
||||||
@@ -1009,6 +1212,37 @@ def guard_assistant_plan(
|
|||||||
names = [str(item["name"]) for item in normalized_parameters]
|
names = [str(item["name"]) for item in normalized_parameters]
|
||||||
if len(names) != len(set(names)):
|
if len(names) != len(set(names)):
|
||||||
raise ValueError(f"assistant plan request {index} repeats a parameter")
|
raise ValueError(f"assistant plan request {index} repeats a parameter")
|
||||||
|
if allow_scene_composition:
|
||||||
|
registered_spec = registered_specs.get(capability)
|
||||||
|
contract_error = (
|
||||||
|
_scene_contract_error(
|
||||||
|
capability,
|
||||||
|
target_scope,
|
||||||
|
normalized_parameters,
|
||||||
|
registered_spec[1],
|
||||||
|
)
|
||||||
|
if registered_spec is not None
|
||||||
|
else "能力契约不可用"
|
||||||
|
)
|
||||||
|
if contract_error is not None:
|
||||||
|
rejected.append(
|
||||||
|
{
|
||||||
|
"field": f"requests[{index}]",
|
||||||
|
"question": f"{contract_error},已拒绝生成该功能模式。",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
scene_key = (domain, capability, target_scope)
|
||||||
|
if allow_scene_composition and scene_key in scene_keys:
|
||||||
|
rejected.append(
|
||||||
|
{
|
||||||
|
"field": f"requests[{index}]",
|
||||||
|
"question": "功能模式包含重复的能力和目标,已拒绝生成部分模式。",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
scene_keys.add(scene_key)
|
||||||
|
|
||||||
local_key = (domain, capability, target_scope)
|
local_key = (domain, capability, target_scope)
|
||||||
if local_key in local_keys or (
|
if local_key in local_keys or (
|
||||||
@@ -1025,12 +1259,24 @@ def guard_assistant_plan(
|
|||||||
or not 0 <= duration <= 600_000
|
or not 0 <= duration <= 600_000
|
||||||
):
|
):
|
||||||
raise ValueError(f"assistant plan request {index} has invalid duration_ms")
|
raise ValueError(f"assistant plan request {index} has invalid duration_ms")
|
||||||
semantic_error = _semantic_parameter_error(
|
if allow_scene_composition and duration is not None:
|
||||||
capability,
|
rejected.append(
|
||||||
operation,
|
{
|
||||||
normalized_parameters,
|
"field": f"requests[{index}].duration_ms",
|
||||||
duration,
|
"question": "功能模式只接受无时长的稳定目标状态。",
|
||||||
request_text,
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
semantic_error = (
|
||||||
|
None
|
||||||
|
if allow_scene_composition
|
||||||
|
else _semantic_parameter_error(
|
||||||
|
capability,
|
||||||
|
operation,
|
||||||
|
normalized_parameters,
|
||||||
|
duration,
|
||||||
|
request_text,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if semantic_error is not None:
|
if semantic_error is not None:
|
||||||
rejected.append(
|
rejected.append(
|
||||||
@@ -1052,6 +1298,11 @@ def guard_assistant_plan(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if allow_scene_composition and rejected:
|
||||||
|
# A reusable mode must be atomic. Never offer a draft that silently
|
||||||
|
# dropped one hallucinated, conflicting, relative, or transient step.
|
||||||
|
accepted = []
|
||||||
|
|
||||||
summaries = raw_plan.get("decision_summary", [])
|
summaries = raw_plan.get("decision_summary", [])
|
||||||
if not isinstance(summaries, Sequence) or isinstance(summaries, (str, bytes, bytearray)):
|
if not isinstance(summaries, Sequence) or isinstance(summaries, (str, bytes, bytearray)):
|
||||||
summaries = []
|
summaries = []
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from pathlib import Path
|
|||||||
from typing import Protocol
|
from typing import Protocol
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
INTERNAL_SCENE_BUILDER_MARKER = "根据该场景名称使用当前可控原子能力"
|
||||||
|
|
||||||
|
|
||||||
class CodexAssistantError(RuntimeError):
|
class CodexAssistantError(RuntimeError):
|
||||||
@@ -99,6 +100,7 @@ class _VehicleTurnContext:
|
|||||||
capability_queries: int = 0
|
capability_queries: int = 0
|
||||||
action_succeeded: bool | None = None
|
action_succeeded: bool | None = None
|
||||||
action_state: str | None = None
|
action_state: str | None = None
|
||||||
|
action_result: Mapping[str, object] | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -139,9 +141,9 @@ _MODE_PROFILE_CREATE_PATTERN = re.compile(
|
|||||||
r"(?:构建|创建|新建|新增|生成|添加)(?:一下)?"
|
r"(?:构建|创建|新建|新增|生成|添加)(?:一下)?"
|
||||||
r"(?:一个|个|一套|套)?"
|
r"(?:一个|个|一套|套)?"
|
||||||
r"(?:名为|叫做|叫)?[“\"'「『]?"
|
r"(?:名为|叫做|叫)?[“\"'「『]?"
|
||||||
r"[^,。,.!!::\s“”\"'「」『』]{1,64}?模式[”\"'」』]?"
|
r"(?P<name>[^,。,.!!::\s“”\"'「」『』]{1,64}?模式)[”\"'」』]?"
|
||||||
r"(?:配置)?(?:并(?:添加|加入)(?:到)?功能模式(?:栏)?)?"
|
r"(?:配置)?(?:并(?:添加|加入)(?:到)?功能模式(?:栏)?)?"
|
||||||
r"(?:[,,::].{1,800})?[。.!!]?$"
|
r"(?:[,,::](?P<instruction>.{1,800}))?[。.!!]?$"
|
||||||
)
|
)
|
||||||
_HOT_DISCOMFORT_PATTERN = re.compile(r"热|闷热|好热|太晒")
|
_HOT_DISCOMFORT_PATTERN = re.compile(r"热|闷热|好热|太晒")
|
||||||
_COLD_DISCOMFORT_PATTERN = re.compile(r"冷|好冷")
|
_COLD_DISCOMFORT_PATTERN = re.compile(r"冷|好冷")
|
||||||
@@ -164,12 +166,114 @@ def _normalize_instruction(value: object) -> str:
|
|||||||
return re.sub(r"[\s,。!?!?、;;::]", "", str(value).lower())
|
return re.sub(r"[\s,。!?!?、;;::]", "", str(value).lower())
|
||||||
|
|
||||||
|
|
||||||
|
def _mode_profile_create_parts(message: str) -> tuple[str, str] | None:
|
||||||
|
"""Return the requested scene name and optional explicit settings."""
|
||||||
|
|
||||||
|
normalized = "".join(str(message).strip().split())
|
||||||
|
match = _MODE_PROFILE_CREATE_PATTERN.fullmatch(normalized)
|
||||||
|
if match is None:
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
str(match.group("name") or "").strip(),
|
||||||
|
str(match.group("instruction") or "").strip(",,::。.!!"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_mode_profile_create_request(message: str) -> bool:
|
||||||
|
"""Return whether the original user text is a bounded mode-create request."""
|
||||||
|
|
||||||
|
return _mode_profile_create_parts(message) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _catalog_exact_mode_match_count(
|
||||||
|
catalog: Mapping[str, object],
|
||||||
|
requested_name: str,
|
||||||
|
) -> int | None:
|
||||||
|
"""Count exact visible mode matches; ``None`` means catalog unavailable."""
|
||||||
|
|
||||||
|
needle = "".join(str(requested_name).casefold().split())
|
||||||
|
source_status = catalog.get("source_status")
|
||||||
|
if "modes" not in catalog or (
|
||||||
|
isinstance(source_status, Mapping)
|
||||||
|
and source_status.get("modes") != "ready"
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
matched_ids: set[str] = set()
|
||||||
|
raw_modes = catalog.get("modes", [])
|
||||||
|
if not isinstance(raw_modes, Sequence) or isinstance(
|
||||||
|
raw_modes,
|
||||||
|
(str, bytes, bytearray),
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
for raw_mode in raw_modes:
|
||||||
|
if not isinstance(raw_mode, Mapping):
|
||||||
|
continue
|
||||||
|
candidates = [raw_mode.get("id"), raw_mode.get("name")]
|
||||||
|
aliases = raw_mode.get("aliases", [])
|
||||||
|
if isinstance(aliases, Sequence) and not isinstance(
|
||||||
|
aliases,
|
||||||
|
(str, bytes, bytearray),
|
||||||
|
):
|
||||||
|
candidates.extend(aliases)
|
||||||
|
if needle in {
|
||||||
|
"".join(str(candidate or "").casefold().split())
|
||||||
|
for candidate in candidates
|
||||||
|
if str(candidate or "").strip()
|
||||||
|
}:
|
||||||
|
matched_ids.add(str(raw_mode.get("id") or ""))
|
||||||
|
matched_ids.discard("")
|
||||||
|
return len(matched_ids)
|
||||||
|
|
||||||
|
|
||||||
def _vehicle_result_succeeded(result: Mapping[str, object]) -> bool:
|
def _vehicle_result_succeeded(result: Mapping[str, object]) -> bool:
|
||||||
"""Only a terminal, fully successful service result completes a tool call."""
|
"""Only a terminal, fully successful service result completes a tool call."""
|
||||||
|
|
||||||
return str(result.get("status") or "").lower() == "succeeded"
|
return str(result.get("status") or "").lower() == "succeeded"
|
||||||
|
|
||||||
|
|
||||||
|
def _mode_profile_result_succeeded(result: Mapping[str, object]) -> bool:
|
||||||
|
"""Require a complete, non-physical mode preview before claiming success."""
|
||||||
|
|
||||||
|
if (
|
||||||
|
not _vehicle_result_succeeded(result)
|
||||||
|
or str(result.get("execution_mode") or "").lower() != "dry_run"
|
||||||
|
or result.get("hardware_execution_allowed") is not False
|
||||||
|
or result.get("orchestration_route") != "mode_profile_create_preview"
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
draft = result.get("mode_profile_draft")
|
||||||
|
if not isinstance(draft, Mapping):
|
||||||
|
return False
|
||||||
|
operation_count = draft.get("operation_count")
|
||||||
|
if (
|
||||||
|
not isinstance(operation_count, int)
|
||||||
|
or isinstance(operation_count, bool)
|
||||||
|
or operation_count <= 0
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
intent_ir = result.get("intent_ir")
|
||||||
|
spec = intent_ir.get("spec") if isinstance(intent_ir, Mapping) else None
|
||||||
|
actions = spec.get("actions") if isinstance(spec, Mapping) else None
|
||||||
|
if not isinstance(actions, Sequence) or isinstance(
|
||||||
|
actions,
|
||||||
|
(str, bytes, bytearray),
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
if len(actions) != operation_count or not all(
|
||||||
|
isinstance(action, Mapping) for action in actions
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
validation = result.get("validation", [])
|
||||||
|
return not (
|
||||||
|
isinstance(validation, Sequence)
|
||||||
|
and not isinstance(validation, (str, bytes, bytearray))
|
||||||
|
and any(
|
||||||
|
isinstance(issue, Mapping) and issue.get("severity") == "error"
|
||||||
|
for issue in validation
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _vehicle_verification_status(result: Mapping[str, object]) -> str:
|
def _vehicle_verification_status(result: Mapping[str, object]) -> str:
|
||||||
"""Normalize the service's execution evidence without inventing feedback."""
|
"""Normalize the service's execution evidence without inventing feedback."""
|
||||||
|
|
||||||
@@ -231,7 +335,7 @@ def _authorize_vehicle_request(
|
|||||||
):
|
):
|
||||||
return _VehicleAuthorization(None, "knowledge_or_non_vehicle_request")
|
return _VehicleAuthorization(None, "knowledge_or_non_vehicle_request")
|
||||||
|
|
||||||
if _MODE_PROFILE_CREATE_PATTERN.fullmatch(normalized):
|
if is_mode_profile_create_request(normalized):
|
||||||
# Named-mode creation is a bounded configuration operation. The
|
# Named-mode creation is a bounded configuration operation. The
|
||||||
# vehicle service parses the same original sentence again and always
|
# vehicle service parses the same original sentence again and always
|
||||||
# validates the generated scene in dry-run before offering it to the
|
# validates the generated scene in dry-run before offering it to the
|
||||||
@@ -373,7 +477,27 @@ class CodexAssistantService:
|
|||||||
self._fast_implicit_controls and authorization.reason in _FAST_COMFORT_REASONS
|
self._fast_implicit_controls and authorization.reason in _FAST_COMFORT_REASONS
|
||||||
)
|
)
|
||||||
mode_profile_preview = authorization.reason == "explicit_mode_profile_create"
|
mode_profile_preview = authorization.reason == "explicit_mode_profile_create"
|
||||||
direct_vehicle_control = fast_comfort_control or mode_profile_preview
|
mode_parts = _mode_profile_create_parts(message) if mode_profile_preview else None
|
||||||
|
exact_mode_matches = (
|
||||||
|
_catalog_exact_mode_match_count(catalog, mode_parts[0])
|
||||||
|
if mode_parts
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
direct_mode_profile_preview = bool(
|
||||||
|
mode_parts
|
||||||
|
and (
|
||||||
|
mode_parts[1]
|
||||||
|
# Ambiguous or unavailable mode catalogs fail closed in the
|
||||||
|
# local builder. Only a known-zero match is safe to hand to
|
||||||
|
# semantic scene composition.
|
||||||
|
or exact_mode_matches != 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Explicit device settings and repository-backed scenes remain on the
|
||||||
|
# zero-model-latency path. A genuinely new name-only scene needs Codex
|
||||||
|
# to compose a candidate plan from the live capability catalog; the
|
||||||
|
# vehicle service still limits it to dry-run and requires confirmation.
|
||||||
|
direct_vehicle_control = fast_comfort_control or direct_mode_profile_preview
|
||||||
route = (
|
route = (
|
||||||
"capability_catalog"
|
"capability_catalog"
|
||||||
if authoritative_answer
|
if authoritative_answer
|
||||||
@@ -385,7 +509,7 @@ class CodexAssistantService:
|
|||||||
"n7web-capability-registry"
|
"n7web-capability-registry"
|
||||||
if authoritative_answer
|
if authoritative_answer
|
||||||
else "n7web-mode-builder"
|
else "n7web-mode-builder"
|
||||||
if mode_profile_preview
|
if direct_mode_profile_preview
|
||||||
else "n7web-control-router"
|
else "n7web-control-router"
|
||||||
if fast_comfort_control
|
if fast_comfort_control
|
||||||
else "codex-app-server"
|
else "codex-app-server"
|
||||||
@@ -414,6 +538,8 @@ class CodexAssistantService:
|
|||||||
"正在读取工程能力目录……"
|
"正在读取工程能力目录……"
|
||||||
if authoritative_answer
|
if authoritative_answer
|
||||||
else "正在构建并校验功能模式……"
|
else "正在构建并校验功能模式……"
|
||||||
|
if direct_mode_profile_preview
|
||||||
|
else "Codex 正在理解场景并组合工程能力……"
|
||||||
if mode_profile_preview
|
if mode_profile_preview
|
||||||
else "正在快速处理座舱舒适需求……"
|
else "正在快速处理座舱舒适需求……"
|
||||||
if fast_comfort_control
|
if fast_comfort_control
|
||||||
@@ -542,6 +668,8 @@ class CodexAssistantService:
|
|||||||
safe_result = self._safe_vehicle_result(result)
|
safe_result = self._safe_vehicle_result(result)
|
||||||
action_state, successful, completion_text = _vehicle_action_state(result)
|
action_state, successful, completion_text = _vehicle_action_state(result)
|
||||||
if mode_profile_preview:
|
if mode_profile_preview:
|
||||||
|
successful = _mode_profile_result_succeeded(result)
|
||||||
|
action_state = "simulated" if successful else "failed"
|
||||||
completion_text = (
|
completion_text = (
|
||||||
"功能模式预览已生成" if successful else "功能模式预览生成失败"
|
"功能模式预览已生成" if successful else "功能模式预览生成失败"
|
||||||
)
|
)
|
||||||
@@ -632,6 +760,14 @@ class CodexAssistantService:
|
|||||||
reason = clean(failure.get("reason"))
|
reason = clean(failure.get("reason"))
|
||||||
next_step = clean(failure.get("next_step"))
|
next_step = clean(failure.get("next_step"))
|
||||||
if reason and next_step:
|
if reason and next_step:
|
||||||
|
if (
|
||||||
|
INTERNAL_SCENE_BUILDER_MARKER in reason
|
||||||
|
or INTERNAL_SCENE_BUILDER_MARKER in next_step
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
"未能根据场景名称生成通过工程能力校验的座舱动作",
|
||||||
|
"请稍后重试;若该场景尚未登记,可补充希望联动的设备状态",
|
||||||
|
)
|
||||||
return reason, next_step
|
return reason, next_step
|
||||||
|
|
||||||
unresolved = result.get("unresolved_clauses")
|
unresolved = result.get("unresolved_clauses")
|
||||||
@@ -644,6 +780,8 @@ class CodexAssistantService:
|
|||||||
clean(item, limit=120)
|
clean(item, limit=120)
|
||||||
for item in unresolved[:3]
|
for item in unresolved[:3]
|
||||||
if clean(item, limit=120)
|
if clean(item, limit=120)
|
||||||
|
and INTERNAL_SCENE_BUILDER_MARKER
|
||||||
|
not in clean(item, limit=120)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if clauses:
|
if clauses:
|
||||||
@@ -716,6 +854,40 @@ class CodexAssistantService:
|
|||||||
if isinstance(draft, Mapping)
|
if isinstance(draft, Mapping)
|
||||||
else "新功能模式"
|
else "新功能模式"
|
||||||
)
|
)
|
||||||
|
if isinstance(draft, Mapping) and draft.get("existing_mode_id"):
|
||||||
|
raw_operation_count = draft.get("operation_count")
|
||||||
|
operation_count = (
|
||||||
|
raw_operation_count
|
||||||
|
if isinstance(raw_operation_count, int)
|
||||||
|
and not isinstance(raw_operation_count, bool)
|
||||||
|
and raw_operation_count >= 0
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
if draft.get("generation_source") == "registered_mode_profile":
|
||||||
|
return (
|
||||||
|
f"工程中已有“{name}”,已按当前车型载入预览并验证"
|
||||||
|
f"{operation_count}项设备动作。可以先试运行,确认后更新现有模式。"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"已按你提供的设置为现有“{name}”生成预览并验证"
|
||||||
|
f"{operation_count}项设备动作。可以先试运行,确认后更新现有模式。"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
isinstance(draft, Mapping)
|
||||||
|
and draft.get("generation_source") == "codex_scene_composition"
|
||||||
|
):
|
||||||
|
raw_operation_count = draft.get("operation_count")
|
||||||
|
operation_count = (
|
||||||
|
raw_operation_count
|
||||||
|
if isinstance(raw_operation_count, int)
|
||||||
|
and not isinstance(raw_operation_count, bool)
|
||||||
|
and raw_operation_count > 0
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"已理解“{name}”的场景意图,并从当前工程能力中组合、验证了"
|
||||||
|
f"{operation_count}项设备动作。确认预览后可保存为功能模式,也可以先试运行。"
|
||||||
|
)
|
||||||
return f"已为你生成“{name}”预览。确认动作后可保存为功能模式,也可以先试运行。"
|
return f"已为你生成“{name}”预览。确认动作后可保存为功能模式,也可以先试运行。"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -765,6 +937,9 @@ class CodexAssistantService:
|
|||||||
query_capabilities,
|
query_capabilities,
|
||||||
)
|
)
|
||||||
vehicle_control_expected = tool_context.authorization.instruction is not None
|
vehicle_control_expected = tool_context.authorization.instruction is not None
|
||||||
|
mode_profile_expected = (
|
||||||
|
tool_context.authorization.reason == "explicit_mode_profile_create"
|
||||||
|
)
|
||||||
with self._tool_contexts_lock:
|
with self._tool_contexts_lock:
|
||||||
self._tool_contexts[request_token] = tool_context
|
self._tool_contexts[request_token] = tool_context
|
||||||
|
|
||||||
@@ -938,8 +1113,6 @@ class CodexAssistantService:
|
|||||||
raise CodexAssistantError(str(detail or f"Codex turn {status}"))
|
raise CodexAssistantError(str(detail or f"Codex turn {status}"))
|
||||||
if not final_answer:
|
if not final_answer:
|
||||||
final_answer = streamed_answer.strip()
|
final_answer = streamed_answer.strip()
|
||||||
if not final_answer:
|
|
||||||
raise CodexAssistantError("Codex produced an empty response")
|
|
||||||
if vehicle_control_expected:
|
if vehicle_control_expected:
|
||||||
if not tool_context.called:
|
if not tool_context.called:
|
||||||
tool_context.action_succeeded = False
|
tool_context.action_succeeded = False
|
||||||
@@ -955,9 +1128,25 @@ class CodexAssistantService:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif tool_context.action_succeeded is not True:
|
elif tool_context.action_succeeded is not True:
|
||||||
final_answer = "车辆控制未完成,受控执行链未返回成功结果。"
|
final_answer = (
|
||||||
|
self._mode_profile_preview_answer(
|
||||||
|
tool_context.action_result or {},
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
if mode_profile_expected
|
||||||
|
else "车辆控制未完成,受控执行链未返回成功结果。"
|
||||||
|
)
|
||||||
|
elif mode_profile_expected:
|
||||||
|
final_answer = self._mode_profile_preview_answer(
|
||||||
|
tool_context.action_result or {},
|
||||||
|
True,
|
||||||
|
)
|
||||||
elif tool_context.action_state == "submitted":
|
elif tool_context.action_state == "submitted":
|
||||||
final_answer = "控制指令已提交,正在等待车辆状态确认。"
|
final_answer = "控制指令已提交,正在等待车辆状态确认。"
|
||||||
|
elif not final_answer and tool_context.action_state == "simulated":
|
||||||
|
final_answer = "车辆控制模拟编排已完成。"
|
||||||
|
elif not final_answer and tool_context.action_state == "verified":
|
||||||
|
final_answer = "车辆控制已完成并通过状态确认。"
|
||||||
if (
|
if (
|
||||||
emit(
|
emit(
|
||||||
{
|
{
|
||||||
@@ -969,6 +1158,8 @@ class CodexAssistantService:
|
|||||||
is False
|
is False
|
||||||
):
|
):
|
||||||
return {"cancelled": True, "answer": final_answer}
|
return {"cancelled": True, "answer": final_answer}
|
||||||
|
if not final_answer:
|
||||||
|
raise CodexAssistantError("Codex produced an empty response")
|
||||||
work_succeeded = (
|
work_succeeded = (
|
||||||
tool_context.called and tool_context.action_succeeded is True
|
tool_context.called and tool_context.action_succeeded is True
|
||||||
if vehicle_control_expected
|
if vehicle_control_expected
|
||||||
@@ -980,7 +1171,11 @@ class CodexAssistantService:
|
|||||||
"message_id": message_id,
|
"message_id": message_id,
|
||||||
"success": work_succeeded,
|
"success": work_succeeded,
|
||||||
"display_text": (
|
"display_text": (
|
||||||
"车辆控制模拟编排已完成"
|
"功能模式预览已生成"
|
||||||
|
if mode_profile_expected and work_succeeded
|
||||||
|
else "功能模式预览生成失败"
|
||||||
|
if mode_profile_expected
|
||||||
|
else "车辆控制模拟编排已完成"
|
||||||
if vehicle_control_expected
|
if vehicle_control_expected
|
||||||
and work_succeeded
|
and work_succeeded
|
||||||
and tool_context.action_state == "simulated"
|
and tool_context.action_state == "simulated"
|
||||||
@@ -1106,6 +1301,15 @@ class CodexAssistantService:
|
|||||||
secret=request_token,
|
secret=request_token,
|
||||||
)
|
)
|
||||||
action_state, successful, completion_text = _vehicle_action_state(result)
|
action_state, successful, completion_text = _vehicle_action_state(result)
|
||||||
|
if context.authorization.reason == "explicit_mode_profile_create":
|
||||||
|
successful = _mode_profile_result_succeeded(result)
|
||||||
|
action_state = "simulated" if successful else "failed"
|
||||||
|
completion_text = (
|
||||||
|
"功能模式预览已生成"
|
||||||
|
if successful
|
||||||
|
else "功能模式预览生成失败"
|
||||||
|
)
|
||||||
|
context.action_result = result
|
||||||
context.action_succeeded = successful
|
context.action_succeeded = successful
|
||||||
context.action_state = action_state
|
context.action_state = action_state
|
||||||
context.events.put(
|
context.events.put(
|
||||||
@@ -1158,7 +1362,17 @@ class CodexAssistantService:
|
|||||||
if "decision_summary" in normalized_plan:
|
if "decision_summary" in normalized_plan:
|
||||||
normalized_plan["decision_summary"] = ["Codex 结构化计划已提交应用安全链复核。"]
|
normalized_plan["decision_summary"] = ["Codex 结构化计划已提交应用安全链复核。"]
|
||||||
if "ambiguities" in normalized_plan:
|
if "ambiguities" in normalized_plan:
|
||||||
normalized_plan["ambiguities"] = []
|
normalized_plan["ambiguities"] = (
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"field": "assistant_plan",
|
||||||
|
"question": "Codex 候选计划包含未解决歧义。",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if isinstance(normalized_plan["ambiguities"], list)
|
||||||
|
and normalized_plan["ambiguities"]
|
||||||
|
else []
|
||||||
|
)
|
||||||
requests = normalized_plan.get("requests")
|
requests = normalized_plan.get("requests")
|
||||||
if isinstance(requests, list):
|
if isinstance(requests, list):
|
||||||
for request in requests:
|
for request in requests:
|
||||||
@@ -1482,6 +1696,9 @@ class CodexAssistantService:
|
|||||||
"vehicle_id",
|
"vehicle_id",
|
||||||
"domains",
|
"domains",
|
||||||
"operation_count",
|
"operation_count",
|
||||||
|
"existing_mode_id",
|
||||||
|
"existing_mode_revision",
|
||||||
|
"generation_source",
|
||||||
)
|
)
|
||||||
if key in mode_profile_draft
|
if key in mode_profile_draft
|
||||||
}
|
}
|
||||||
@@ -1507,6 +1724,7 @@ class CodexAssistantService:
|
|||||||
)
|
)
|
||||||
for item in unresolved_items
|
for item in unresolved_items
|
||||||
if isinstance(item, str)
|
if isinstance(item, str)
|
||||||
|
and INTERNAL_SCENE_BUILDER_MARKER not in item
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
peripheral = result.get("peripheral")
|
peripheral = result.get("peripheral")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import json
|
|||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from threading import RLock
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
@@ -419,26 +420,28 @@ class ModeProfileRegistry:
|
|||||||
self.user_root = Path(user_root) if user_root is not None else self.profile_root
|
self.user_root = Path(user_root) if user_root is not None else self.profile_root
|
||||||
self.session_user_profiles = user_root is not None
|
self.session_user_profiles = user_root is not None
|
||||||
self._profiles: list[ModeProfile] = []
|
self._profiles: list[ModeProfile] = []
|
||||||
|
self._write_lock = RLock()
|
||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
def reload(self) -> None:
|
def reload(self) -> None:
|
||||||
profiles: list[ModeProfile] = []
|
with self._write_lock:
|
||||||
if self.profile_root.exists():
|
profiles: list[ModeProfile] = []
|
||||||
for path in sorted(self.profile_root.rglob("*.yaml")):
|
if self.profile_root.exists():
|
||||||
if not path.is_file():
|
for path in sorted(self.profile_root.rglob("*.yaml")):
|
||||||
continue
|
if not path.is_file():
|
||||||
if self.session_user_profiles and _is_user_profile_path(
|
continue
|
||||||
path, self.profile_root
|
if self.session_user_profiles and _is_user_profile_path(
|
||||||
):
|
path, self.profile_root
|
||||||
continue
|
):
|
||||||
profiles.append(ModeProfile.from_yaml(path, self.profile_root))
|
continue
|
||||||
if self.session_user_profiles and self.user_root.exists():
|
profiles.append(ModeProfile.from_yaml(path, self.profile_root))
|
||||||
profiles.extend(
|
if self.session_user_profiles and self.user_root.exists():
|
||||||
ModeProfile.from_yaml(path, self.user_root)
|
profiles.extend(
|
||||||
for path in sorted(self.user_root.rglob("*.yaml"))
|
ModeProfile.from_yaml(path, self.user_root)
|
||||||
if path.is_file()
|
for path in sorted(self.user_root.rglob("*.yaml"))
|
||||||
)
|
if path.is_file()
|
||||||
self._profiles = profiles
|
)
|
||||||
|
self._profiles = profiles
|
||||||
|
|
||||||
def profiles(self) -> list[ModeProfile]:
|
def profiles(self) -> list[ModeProfile]:
|
||||||
return list(self._profiles)
|
return list(self._profiles)
|
||||||
@@ -505,6 +508,61 @@ class ModeProfileRegistry:
|
|||||||
matches.append(selected[profile.id])
|
matches.append(selected[profile.id])
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
|
def resolve_exact_name(
|
||||||
|
self,
|
||||||
|
value: str,
|
||||||
|
vehicle_id: str | None = None,
|
||||||
|
) -> ModeProfile | None:
|
||||||
|
"""Resolve one visible mode by an exact ID, name, or alias.
|
||||||
|
|
||||||
|
Scene creation must not use the substring semantics of :meth:`match_text`:
|
||||||
|
a name can contain another mode alias and accidentally combine two profiles.
|
||||||
|
Returning ``None`` for both no match and an ambiguous alias keeps that path
|
||||||
|
fail-closed so callers can choose an explicit fallback without combining modes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
needle = _normalize_match_text(str(value).strip())
|
||||||
|
if not needle:
|
||||||
|
return None
|
||||||
|
matches = [
|
||||||
|
profile
|
||||||
|
for profile in self._profiles_for_vehicle(vehicle_id)
|
||||||
|
if needle
|
||||||
|
in {
|
||||||
|
_normalize_match_text(str(candidate))
|
||||||
|
for candidate in (profile.id, profile.name, *profile.aliases)
|
||||||
|
if str(candidate).strip()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return matches[0] if len(matches) == 1 else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def profile_revision(profile: ModeProfile) -> str:
|
||||||
|
"""Return a stable semantic revision for optimistic mode updates."""
|
||||||
|
|
||||||
|
document = {
|
||||||
|
"id": profile.id,
|
||||||
|
"name": profile.name,
|
||||||
|
"aliases": list(profile.aliases),
|
||||||
|
"description": profile.description,
|
||||||
|
"icon": profile.icon,
|
||||||
|
"scope": {
|
||||||
|
"common": profile.common,
|
||||||
|
"vehicle_ids": list(profile.vehicle_ids),
|
||||||
|
},
|
||||||
|
"operations": [operation.to_yaml() for operation in profile.operations],
|
||||||
|
"timeline": profile.timeline.to_yaml() if profile.timeline is not None else None,
|
||||||
|
"instruction": profile.instruction,
|
||||||
|
"command_flow": profile.command_flow,
|
||||||
|
}
|
||||||
|
canonical = json.dumps(
|
||||||
|
document,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
)
|
||||||
|
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def requests_for_profiles(profiles: list[ModeProfile]) -> list[dict[str, Any]]:
|
def requests_for_profiles(profiles: list[ModeProfile]) -> list[dict[str, Any]]:
|
||||||
return [
|
return [
|
||||||
@@ -526,6 +584,8 @@ class ModeProfileRegistry:
|
|||||||
instruction: str = "",
|
instruction: str = "",
|
||||||
command_flow: dict[str, Any] | None = None,
|
command_flow: dict[str, Any] | None = None,
|
||||||
timeline: ModeTimeline | None = None,
|
timeline: ModeTimeline | None = None,
|
||||||
|
expected_revision: str | None = None,
|
||||||
|
create_only: bool = False,
|
||||||
) -> ModeProfile:
|
) -> ModeProfile:
|
||||||
clean_name = _clean_name(name)
|
clean_name = _clean_name(name)
|
||||||
canonical_operations = [
|
canonical_operations = [
|
||||||
@@ -566,20 +626,64 @@ class ModeProfileRegistry:
|
|||||||
if vehicle_id
|
if vehicle_id
|
||||||
else self.user_root / "common" / "user" / f"{profile_id}.yaml"
|
else self.user_root / "common" / "user" / f"{profile_id}.yaml"
|
||||||
)
|
)
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
with self._write_lock:
|
||||||
temporary = path.with_suffix(path.suffix + ".tmp")
|
# Refresh inside the same critical section as the compare-and-swap.
|
||||||
temporary.write_text(
|
# This makes concurrent assistant saves one-winner within the server
|
||||||
yaml.safe_dump(document, allow_unicode=True, sort_keys=False),
|
# process instead of letting two stale previews overwrite each other.
|
||||||
encoding="utf-8",
|
self.reload()
|
||||||
)
|
visible_profiles = self._profiles_for_vehicle(vehicle_id)
|
||||||
temporary.replace(path)
|
current = next(
|
||||||
self.reload()
|
(profile for profile in visible_profiles if profile.id == profile_id),
|
||||||
return ModeProfile.from_yaml(path, self.user_root)
|
None,
|
||||||
|
)
|
||||||
|
normalized_name = _normalize_match_text(clean_name)
|
||||||
|
name_collision = any(
|
||||||
|
profile.id != profile_id
|
||||||
|
and normalized_name
|
||||||
|
in {
|
||||||
|
_normalize_match_text(str(candidate))
|
||||||
|
for candidate in (profile.id, profile.name, *profile.aliases)
|
||||||
|
if str(candidate).strip()
|
||||||
|
}
|
||||||
|
for profile in visible_profiles
|
||||||
|
)
|
||||||
|
if name_collision:
|
||||||
|
raise ValueError("该模式名称已被其他模式使用。")
|
||||||
|
if create_only and current is not None:
|
||||||
|
raise ValueError("该模式已由其他会话创建,请刷新后重试。")
|
||||||
|
if expected_revision is not None:
|
||||||
|
expected = str(expected_revision).strip()
|
||||||
|
if (
|
||||||
|
not expected
|
||||||
|
or current is None
|
||||||
|
or self.profile_revision(current) != expected
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"功能模式已被其他会话更新,请重新生成预览后再保存。"
|
||||||
|
)
|
||||||
|
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||||
|
temporary.write_text(
|
||||||
|
yaml.safe_dump(document, allow_unicode=True, sort_keys=False),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
temporary.replace(path)
|
||||||
|
self.reload()
|
||||||
|
return ModeProfile.from_yaml(path, self.user_root)
|
||||||
|
|
||||||
def delete_user_mode(
|
def delete_user_mode(
|
||||||
self,
|
self,
|
||||||
mode_id: str,
|
mode_id: str,
|
||||||
vehicle_id: str | None = None,
|
vehicle_id: str | None = None,
|
||||||
|
) -> ModeProfile:
|
||||||
|
with self._write_lock:
|
||||||
|
return self._delete_user_mode_locked(mode_id, vehicle_id)
|
||||||
|
|
||||||
|
def _delete_user_mode_locked(
|
||||||
|
self,
|
||||||
|
mode_id: str,
|
||||||
|
vehicle_id: str | None = None,
|
||||||
) -> ModeProfile:
|
) -> ModeProfile:
|
||||||
clean_id = str(mode_id).strip()
|
clean_id = str(mode_id).strip()
|
||||||
if not clean_id:
|
if not clean_id:
|
||||||
@@ -623,6 +727,14 @@ class ModeProfileRegistry:
|
|||||||
self,
|
self,
|
||||||
profile: ModeProfile,
|
profile: ModeProfile,
|
||||||
operations: list[dict[str, Any] | ModeOperation],
|
operations: list[dict[str, Any] | ModeOperation],
|
||||||
|
) -> ModeProfile:
|
||||||
|
with self._write_lock:
|
||||||
|
return self._update_mode_locked(profile, operations)
|
||||||
|
|
||||||
|
def _update_mode_locked(
|
||||||
|
self,
|
||||||
|
profile: ModeProfile,
|
||||||
|
operations: list[dict[str, Any] | ModeOperation],
|
||||||
) -> ModeProfile:
|
) -> ModeProfile:
|
||||||
canonical_updates = _canonical_operations(operations)
|
canonical_updates = _canonical_operations(operations)
|
||||||
if not canonical_updates:
|
if not canonical_updates:
|
||||||
@@ -642,6 +754,10 @@ class ModeProfileRegistry:
|
|||||||
return ModeProfile.from_yaml(path, self._root_for_path(path))
|
return ModeProfile.from_yaml(path, self._root_for_path(path))
|
||||||
|
|
||||||
def restore_initial_mode(self, profile: ModeProfile) -> ModeProfile:
|
def restore_initial_mode(self, profile: ModeProfile) -> ModeProfile:
|
||||||
|
with self._write_lock:
|
||||||
|
return self._restore_initial_mode_locked(profile)
|
||||||
|
|
||||||
|
def _restore_initial_mode_locked(self, profile: ModeProfile) -> ModeProfile:
|
||||||
path = Path(profile.source)
|
path = Path(profile.source)
|
||||||
backup_path = _initial_backup_path(path)
|
backup_path = _initial_backup_path(path)
|
||||||
if not backup_path.is_file():
|
if not backup_path.is_file():
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
from collections import Counter
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
@@ -728,6 +729,90 @@ class SeatDemoService:
|
|||||||
vehicle_id,
|
vehicle_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def trial_mode_profile_draft(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
instruction: str,
|
||||||
|
source_request_id: str,
|
||||||
|
vehicle_id: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Replay the server-owned dry-run evidence for one assistant draft."""
|
||||||
|
|
||||||
|
clean_name = str(name).strip()
|
||||||
|
clean_instruction = str(instruction).strip()
|
||||||
|
clean_request_id = str(source_request_id).strip()
|
||||||
|
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
|
||||||
|
if not clean_name or not clean_instruction:
|
||||||
|
raise ValueError("功能模式草稿名称和指令不能为空。")
|
||||||
|
if not MODE_EDITOR_REQUEST_ID_PATTERN.fullmatch(clean_request_id):
|
||||||
|
raise ValueError("功能模式草稿来源请求编号无效。")
|
||||||
|
|
||||||
|
result_path = self.artifact_root / clean_request_id / "result.json"
|
||||||
|
if not result_path.is_file():
|
||||||
|
raise ValueError("未找到功能模式草稿,请重新生成。")
|
||||||
|
try:
|
||||||
|
source_result = json.loads(result_path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError) as exc:
|
||||||
|
raise ValueError("功能模式草稿结果无法读取,请重新生成。") from exc
|
||||||
|
if not isinstance(source_result, dict) or source_result.get(
|
||||||
|
"request_id"
|
||||||
|
) != clean_request_id:
|
||||||
|
raise ValueError("功能模式草稿来源结果不完整,请重新生成。")
|
||||||
|
if (
|
||||||
|
source_result.get("status") != "succeeded"
|
||||||
|
or source_result.get("execution_mode") != "dry_run"
|
||||||
|
):
|
||||||
|
raise ValueError("功能模式草稿没有通过完整的模拟校验。")
|
||||||
|
|
||||||
|
draft = source_result.get("mode_profile_draft")
|
||||||
|
if not isinstance(draft, dict):
|
||||||
|
raise ValueError("来源请求不是可试运行的功能模式草稿。")
|
||||||
|
if str(draft.get("source_request_id") or "").strip() != clean_request_id:
|
||||||
|
raise ValueError("功能模式草稿来源请求编号不匹配,请重新生成。")
|
||||||
|
if str(draft.get("name") or "").strip() != clean_name:
|
||||||
|
raise ValueError("功能模式草稿名称不匹配,请重新生成。")
|
||||||
|
if str(draft.get("instruction") or "").strip() != clean_instruction:
|
||||||
|
raise ValueError("功能模式草稿指令不匹配,请重新生成。")
|
||||||
|
if str(draft.get("vehicle_id") or "").strip() != resolved_vehicle_id:
|
||||||
|
raise ValueError("功能模式草稿不属于当前车型。")
|
||||||
|
|
||||||
|
intent_ir = source_result.get("intent_ir")
|
||||||
|
spec = intent_ir.get("spec") if isinstance(intent_ir, dict) else None
|
||||||
|
actions = spec.get("actions") if isinstance(spec, dict) else None
|
||||||
|
function_configs = source_result.get("function_configs")
|
||||||
|
operation_count = draft.get("operation_count")
|
||||||
|
if (
|
||||||
|
not isinstance(actions, list)
|
||||||
|
or not actions
|
||||||
|
or not isinstance(function_configs, dict)
|
||||||
|
or not function_configs
|
||||||
|
or not isinstance(operation_count, int)
|
||||||
|
or isinstance(operation_count, bool)
|
||||||
|
or len(actions) != operation_count
|
||||||
|
):
|
||||||
|
raise ValueError("功能模式草稿动作证据不完整,请重新生成。")
|
||||||
|
validation = source_result.get("validation", [])
|
||||||
|
validation = validation if isinstance(validation, list) else []
|
||||||
|
if any(
|
||||||
|
isinstance(issue, dict) and issue.get("severity") == "error"
|
||||||
|
for issue in validation
|
||||||
|
):
|
||||||
|
raise ValueError("功能模式草稿未通过安全校验,请重新生成。")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"request_id": clean_request_id,
|
||||||
|
"status": "succeeded",
|
||||||
|
"execution_mode": "dry_run",
|
||||||
|
"hardware_execution_allowed": False,
|
||||||
|
"orchestration_route": "mode_profile_draft_trial",
|
||||||
|
"mode_profile_draft": deepcopy(draft),
|
||||||
|
"intent_ir": {"spec": {"actions": deepcopy(actions)}},
|
||||||
|
"function_configs": deepcopy(function_configs),
|
||||||
|
"execution_report": deepcopy(source_result.get("execution_report")),
|
||||||
|
"validation": deepcopy(validation),
|
||||||
|
"trial_source": "server_validated_preview_artifact",
|
||||||
|
}
|
||||||
|
|
||||||
def _persist_mode_profile_card(
|
def _persist_mode_profile_card(
|
||||||
self,
|
self,
|
||||||
mode_id: str | None,
|
mode_id: str | None,
|
||||||
@@ -797,11 +882,18 @@ class SeatDemoService:
|
|||||||
if source_vehicle_id and source_vehicle_id != resolved_vehicle_id:
|
if source_vehicle_id and source_vehicle_id != resolved_vehicle_id:
|
||||||
raise ValueError("控制指令流不属于当前车型,请重新生成。")
|
raise ValueError("控制指令流不属于当前车型,请重新生成。")
|
||||||
source_draft = source_result.get("mode_profile_draft")
|
source_draft = source_result.get("mode_profile_draft")
|
||||||
|
draft_existing_mode_revision = ""
|
||||||
if isinstance(source_draft, dict):
|
if isinstance(source_draft, dict):
|
||||||
draft_request_id = str(source_draft.get("source_request_id") or "").strip()
|
draft_request_id = str(source_draft.get("source_request_id") or "").strip()
|
||||||
draft_vehicle_id = str(source_draft.get("vehicle_id") or "").strip()
|
draft_vehicle_id = str(source_draft.get("vehicle_id") or "").strip()
|
||||||
draft_instruction = str(source_draft.get("instruction") or "").strip()
|
draft_instruction = str(source_draft.get("instruction") or "").strip()
|
||||||
draft_name = str(source_draft.get("name") or "").strip()
|
draft_name = str(source_draft.get("name") or "").strip()
|
||||||
|
draft_existing_mode_id = str(
|
||||||
|
source_draft.get("existing_mode_id") or ""
|
||||||
|
).strip()
|
||||||
|
draft_existing_mode_revision = str(
|
||||||
|
source_draft.get("existing_mode_revision") or ""
|
||||||
|
).strip()
|
||||||
if draft_request_id != clean_request_id:
|
if draft_request_id != clean_request_id:
|
||||||
raise ValueError("功能模式草稿与控制指令流不匹配,请重新生成。")
|
raise ValueError("功能模式草稿与控制指令流不匹配,请重新生成。")
|
||||||
if draft_vehicle_id and draft_vehicle_id != resolved_vehicle_id:
|
if draft_vehicle_id and draft_vehicle_id != resolved_vehicle_id:
|
||||||
@@ -810,6 +902,19 @@ class SeatDemoService:
|
|||||||
raise ValueError("自然语言指令已修改,请重新生成模式预览。")
|
raise ValueError("自然语言指令已修改,请重新生成模式预览。")
|
||||||
if draft_name and draft_name != clean_name:
|
if draft_name and draft_name != clean_name:
|
||||||
raise ValueError("功能模式名称已修改,请重新生成模式预览。")
|
raise ValueError("功能模式名称已修改,请重新生成模式预览。")
|
||||||
|
if clean_mode_id and not draft_existing_mode_id:
|
||||||
|
raise ValueError("新功能模式预览不能通过已有模式的更新接口保存。")
|
||||||
|
if draft_existing_mode_id and draft_existing_mode_id != clean_mode_id:
|
||||||
|
raise ValueError("该预览属于已有功能模式,请通过对应模式的更新接口保存。")
|
||||||
|
if draft_existing_mode_id and not draft_existing_mode_revision:
|
||||||
|
raise ValueError("功能模式预览版本过旧,请重新生成后再更新。")
|
||||||
|
if (
|
||||||
|
draft_existing_mode_revision
|
||||||
|
and profile is not None
|
||||||
|
and draft_existing_mode_revision
|
||||||
|
!= self.mode_profiles.profile_revision(profile)
|
||||||
|
):
|
||||||
|
raise ValueError("功能模式已被其他会话更新,请重新生成预览后再保存。")
|
||||||
recorded_instruction = str(source_result.get("utterance", "")).strip()
|
recorded_instruction = str(source_result.get("utterance", "")).strip()
|
||||||
if not recorded_instruction:
|
if not recorded_instruction:
|
||||||
source_spans = {
|
source_spans = {
|
||||||
@@ -920,6 +1025,8 @@ class SeatDemoService:
|
|||||||
instruction=clean_instruction,
|
instruction=clean_instruction,
|
||||||
command_flow=command_flow,
|
command_flow=command_flow,
|
||||||
timeline=profile.timeline if profile is not None else None,
|
timeline=profile.timeline if profile is not None else None,
|
||||||
|
expected_revision=draft_existing_mode_revision or None,
|
||||||
|
create_only=not bool(clean_mode_id),
|
||||||
)
|
)
|
||||||
public_profile = next(
|
public_profile = next(
|
||||||
(
|
(
|
||||||
@@ -1422,15 +1529,7 @@ class SeatDemoService:
|
|||||||
"conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens"
|
"conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens"
|
||||||
)
|
)
|
||||||
vehicle_profile = self.vehicle_profiles.get(vehicle_id)
|
vehicle_profile = self.vehicle_profiles.get(vehicle_id)
|
||||||
control_plane_request = any(
|
control_plane_request = self.is_mode_profile_configuration_request(utterance)
|
||||||
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:
|
if control_plane_request:
|
||||||
result = self.process(
|
result = self.process(
|
||||||
utterance,
|
utterance,
|
||||||
@@ -2070,6 +2169,8 @@ class SeatDemoService:
|
|||||||
direct_mode_profile_id: str | None = None,
|
direct_mode_profile_id: str | None = None,
|
||||||
speech_understanding: dict[str, Any] | None = None,
|
speech_understanding: dict[str, Any] | None = None,
|
||||||
assistant_plan: dict[str, Any] | None = None,
|
assistant_plan: dict[str, Any] | None = None,
|
||||||
|
allow_fuzzy_mode_profile_match: bool = True,
|
||||||
|
allow_scene_composition: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
control_transport = self._validate_control_transport(control_transport)
|
control_transport = self._validate_control_transport(control_transport)
|
||||||
utterance = utterance.strip()
|
utterance = utterance.strip()
|
||||||
@@ -2151,7 +2252,11 @@ class SeatDemoService:
|
|||||||
[direct_mode_profile]
|
[direct_mode_profile]
|
||||||
if direct_mode_profile is not None
|
if direct_mode_profile is not None
|
||||||
else []
|
else []
|
||||||
if direct_can_batch is not None or speech_understanding is not None
|
if (
|
||||||
|
direct_can_batch is not None
|
||||||
|
or speech_understanding is not None
|
||||||
|
or not allow_fuzzy_mode_profile_match
|
||||||
|
)
|
||||||
else self.mode_profiles.match_text(
|
else self.mode_profiles.match_text(
|
||||||
utterance,
|
utterance,
|
||||||
vehicle_id=vehicle_profile.id,
|
vehicle_id=vehicle_profile.id,
|
||||||
@@ -2325,6 +2430,7 @@ class SeatDemoService:
|
|||||||
local_preview=local_preview,
|
local_preview=local_preview,
|
||||||
model_parse_source=model_parse_source,
|
model_parse_source=model_parse_source,
|
||||||
assistant_plan=assistant_plan,
|
assistant_plan=assistant_plan,
|
||||||
|
allow_scene_composition=allow_scene_composition,
|
||||||
)
|
)
|
||||||
dynamic_mode_stop = self._is_dynamic_mode_stop_request(utterance)
|
dynamic_mode_stop = self._is_dynamic_mode_stop_request(utterance)
|
||||||
if dynamic_mode_stop:
|
if dynamic_mode_stop:
|
||||||
@@ -3422,6 +3528,21 @@ class SeatDemoService:
|
|||||||
)
|
)
|
||||||
return generic.group("name").strip() if generic is not None else None
|
return generic.group("name").strip() if generic is not None else None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_mode_profile_configuration_request(cls, utterance: str) -> bool:
|
||||||
|
"""Identify mode configuration text that must never reach live executors."""
|
||||||
|
|
||||||
|
clean_utterance = str(utterance).strip()
|
||||||
|
return any(
|
||||||
|
matcher(clean_utterance) is not None
|
||||||
|
for matcher in (
|
||||||
|
cls._parse_mode_profile_save_name,
|
||||||
|
cls._parse_mode_profile_create_name,
|
||||||
|
cls._parse_mode_profile_restore_name,
|
||||||
|
cls._parse_mode_profile_update,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_mode_profile_create_name(utterance: str) -> str | None:
|
def _parse_mode_profile_create_name(utterance: str) -> str | None:
|
||||||
explicit = CREATE_MODE_PROFILE_NAME.search(utterance)
|
explicit = CREATE_MODE_PROFILE_NAME.search(utterance)
|
||||||
@@ -4378,6 +4499,109 @@ class SeatDemoService:
|
|||||||
"issues": issues,
|
"issues": issues,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _scene_plan_signatures(
|
||||||
|
assistant_plan: dict[str, Any],
|
||||||
|
) -> Counter[tuple[str, str, str]]:
|
||||||
|
signatures: Counter[tuple[str, str, str]] = Counter()
|
||||||
|
requests = assistant_plan.get("requests", [])
|
||||||
|
if not isinstance(requests, list):
|
||||||
|
return signatures
|
||||||
|
for request in requests:
|
||||||
|
if not isinstance(request, dict):
|
||||||
|
continue
|
||||||
|
domain = str(request.get("domain") or "")
|
||||||
|
capability = str(request.get("capability") or "")
|
||||||
|
target = str(request.get("target_scope") or "")
|
||||||
|
if domain == "seat" and target == "front_right":
|
||||||
|
target = "passenger"
|
||||||
|
if domain and capability and target:
|
||||||
|
signatures[(domain, capability, target)] += 1
|
||||||
|
return signatures
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _scene_action_signatures(
|
||||||
|
actions: list[dict[str, Any]],
|
||||||
|
) -> Counter[tuple[str, str, str]]:
|
||||||
|
signatures: Counter[tuple[str, str, str]] = Counter()
|
||||||
|
for action in actions:
|
||||||
|
domain = str(action.get("domain") or "")
|
||||||
|
capability = str(action.get("capability") or "")
|
||||||
|
raw_target = action.get("target")
|
||||||
|
target = (
|
||||||
|
str(raw_target.get("instance") or raw_target.get("zone") or "")
|
||||||
|
if isinstance(raw_target, dict)
|
||||||
|
else str(action.get("target_scope") or raw_target or "")
|
||||||
|
)
|
||||||
|
if domain == "seat" and target == "front_right":
|
||||||
|
target = "passenger"
|
||||||
|
if domain and capability and target:
|
||||||
|
signatures[(domain, capability, target)] += 1
|
||||||
|
return signatures
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _scene_parameters_preserved(
|
||||||
|
assistant_plan: dict[str, Any],
|
||||||
|
actions: list[dict[str, Any]],
|
||||||
|
) -> bool:
|
||||||
|
actual_parameters: dict[tuple[str, str, str], dict[str, Any]] = {}
|
||||||
|
for action in actions:
|
||||||
|
domain = str(action.get("domain") or "")
|
||||||
|
capability = str(action.get("capability") or "")
|
||||||
|
raw_target = action.get("target")
|
||||||
|
target = (
|
||||||
|
str(raw_target.get("instance") or raw_target.get("zone") or "")
|
||||||
|
if isinstance(raw_target, dict)
|
||||||
|
else str(action.get("target_scope") or raw_target or "")
|
||||||
|
)
|
||||||
|
if domain == "seat" and target == "front_right":
|
||||||
|
target = "passenger"
|
||||||
|
parameters = action.get("parameters")
|
||||||
|
if domain and capability and target and isinstance(parameters, dict):
|
||||||
|
actual_parameters[(domain, capability, target)] = parameters
|
||||||
|
|
||||||
|
requests = assistant_plan.get("requests", [])
|
||||||
|
if not isinstance(requests, list):
|
||||||
|
return False
|
||||||
|
for request in requests:
|
||||||
|
if not isinstance(request, dict):
|
||||||
|
return False
|
||||||
|
domain = str(request.get("domain") or "")
|
||||||
|
capability = str(request.get("capability") or "")
|
||||||
|
target = str(request.get("target_scope") or "")
|
||||||
|
if domain == "seat" and target == "front_right":
|
||||||
|
target = "passenger"
|
||||||
|
parameters = request.get("parameters")
|
||||||
|
if not isinstance(parameters, list):
|
||||||
|
return False
|
||||||
|
expected = {
|
||||||
|
str(parameter.get("name") or ""): parameter.get("value")
|
||||||
|
for parameter in parameters
|
||||||
|
if isinstance(parameter, dict)
|
||||||
|
}
|
||||||
|
actual = actual_parameters.get((domain, capability, target))
|
||||||
|
if actual is None or any(actual.get(name) != value for name, value in expected.items()):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _scrub_failed_scene_preview(result: dict[str, Any]) -> None:
|
||||||
|
failed_intent = result.get("intent_ir")
|
||||||
|
failed_spec = (
|
||||||
|
failed_intent.get("spec") if isinstance(failed_intent, dict) else None
|
||||||
|
)
|
||||||
|
if isinstance(failed_spec, dict):
|
||||||
|
failed_spec["actions"] = []
|
||||||
|
failed_spec["requests"] = []
|
||||||
|
result["deferred_requests"] = []
|
||||||
|
result["model_requests"] = []
|
||||||
|
result["function_config"] = None
|
||||||
|
result["function_configs"] = {}
|
||||||
|
result["command_plan"] = None
|
||||||
|
result["command_plans"] = {}
|
||||||
|
result["scene_config"] = None
|
||||||
|
result["execution_report"] = None
|
||||||
|
|
||||||
def _process_mode_profile_create(
|
def _process_mode_profile_create(
|
||||||
self,
|
self,
|
||||||
utterance: str,
|
utterance: str,
|
||||||
@@ -4405,9 +4629,25 @@ class SeatDemoService:
|
|||||||
if create_match is not None
|
if create_match is not None
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
source_utterance = requested_instruction or (
|
existing_profile = self.mode_profiles.resolve_exact_name(
|
||||||
f"进入{mode_name},请根据该场景名称使用当前可控原子能力构建并执行场景"
|
mode_name,
|
||||||
|
vehicle_id=vehicle_profile.id,
|
||||||
)
|
)
|
||||||
|
source_profile = existing_profile if not requested_instruction else None
|
||||||
|
draft_mode_name = existing_profile.name if existing_profile is not None else mode_name
|
||||||
|
generation_source = (
|
||||||
|
"registered_mode_profile"
|
||||||
|
if source_profile is not None
|
||||||
|
else "codex_scene_composition"
|
||||||
|
if assistant_plan is not None and not requested_instruction
|
||||||
|
else "explicit_scene_requirements"
|
||||||
|
)
|
||||||
|
# A name-only request may already describe a repository-backed scene.
|
||||||
|
# Reuse that vehicle-scoped profile as trusted structured input instead
|
||||||
|
# of appending a model prompt to the user's utterance. The latter was
|
||||||
|
# being parsed as a second control clause on the assistant's local fast
|
||||||
|
# path, turning a valid nine-action mode into an incomplete draft.
|
||||||
|
source_utterance = requested_instruction or f"进入{mode_name}"
|
||||||
source_result = self._process_once(
|
source_result = self._process_once(
|
||||||
source_utterance,
|
source_utterance,
|
||||||
provider,
|
provider,
|
||||||
@@ -4417,7 +4657,20 @@ class SeatDemoService:
|
|||||||
vehicle_profile.id,
|
vehicle_profile.id,
|
||||||
simulation_fault,
|
simulation_fault,
|
||||||
control_transport=control_transport,
|
control_transport=control_transport,
|
||||||
|
direct_mode_profile_id=(
|
||||||
|
source_profile.id if source_profile is not None else None
|
||||||
|
),
|
||||||
assistant_plan=assistant_plan,
|
assistant_plan=assistant_plan,
|
||||||
|
# ``source_utterance`` is synthesized for a name-only, unknown
|
||||||
|
# scene. It must not fall back to substring matching (for example
|
||||||
|
# ``我的小憩模式`` -> ``小憩模式``), otherwise the exact resolver's
|
||||||
|
# ambiguity and collision checks can be bypassed.
|
||||||
|
allow_fuzzy_mode_profile_match=bool(requested_instruction),
|
||||||
|
allow_scene_composition=(
|
||||||
|
assistant_plan is not None
|
||||||
|
and source_profile is None
|
||||||
|
and not requested_instruction
|
||||||
|
),
|
||||||
)
|
)
|
||||||
draft_context = self._context_for_vehicle(
|
draft_context = self._context_for_vehicle(
|
||||||
self.conversations.snapshot(conversation_id),
|
self.conversations.snapshot(conversation_id),
|
||||||
@@ -4434,9 +4687,12 @@ class SeatDemoService:
|
|||||||
clause
|
clause
|
||||||
for clause, _parsed, status in local_clause_results
|
for clause, _parsed, status in local_clause_results
|
||||||
if status != "complete"
|
if status != "complete"
|
||||||
and not self.mode_profiles.match_text(
|
and not (
|
||||||
clause,
|
(requested_instruction or source_profile is not None)
|
||||||
vehicle_id=vehicle_profile.id,
|
and self.mode_profiles.match_text(
|
||||||
|
clause,
|
||||||
|
vehicle_id=vehicle_profile.id,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
if provider == "local" and assistant_plan is None and unresolved_draft_clauses:
|
if provider == "local" and assistant_plan is None and unresolved_draft_clauses:
|
||||||
@@ -4468,7 +4724,49 @@ class SeatDemoService:
|
|||||||
]
|
]
|
||||||
self._persist(source_result)
|
self._persist(source_result)
|
||||||
return source_result
|
return source_result
|
||||||
|
source_intent = source_result.get("intent_ir")
|
||||||
|
source_spec = (
|
||||||
|
source_intent.get("spec") if isinstance(source_intent, dict) else None
|
||||||
|
)
|
||||||
|
source_actions = (
|
||||||
|
source_spec.get("actions", []) if isinstance(source_spec, dict) else []
|
||||||
|
)
|
||||||
|
typed_source_actions = [
|
||||||
|
action for action in source_actions if isinstance(action, dict)
|
||||||
|
] if isinstance(source_actions, list) else []
|
||||||
|
if (
|
||||||
|
source_result.get("status") == "succeeded"
|
||||||
|
and generation_source == "codex_scene_composition"
|
||||||
|
and assistant_plan is not None
|
||||||
|
and (
|
||||||
|
self._scene_plan_signatures(assistant_plan)
|
||||||
|
!= self._scene_action_signatures(typed_source_actions)
|
||||||
|
or not self._scene_parameters_preserved(
|
||||||
|
assistant_plan,
|
||||||
|
typed_source_actions,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
source_result["status"] = "validation_failed"
|
||||||
|
source_result["validation"] = [
|
||||||
|
*list(source_result.get("validation", [])),
|
||||||
|
{
|
||||||
|
"severity": "error",
|
||||||
|
"code": "MODE_PROFILE_SCENE_PLAN_INCOMPLETE",
|
||||||
|
"path": "intent_ir.spec.actions",
|
||||||
|
"message": (
|
||||||
|
"Codex 场景候选动作未全部进入当前车型的可复用执行链,"
|
||||||
|
"已拒绝生成部分功能模式。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
if source_result.get("status") != "succeeded":
|
if source_result.get("status") != "succeeded":
|
||||||
|
if generation_source == "codex_scene_composition":
|
||||||
|
# Final fail-closed barrier for errors discovered after the
|
||||||
|
# plan guard (adapter, schema, compiler, or safety validation).
|
||||||
|
# A failed reusable scene must not expose a trial-able subset.
|
||||||
|
self._scrub_failed_scene_preview(source_result)
|
||||||
source_result["mode_creation_failure"] = {
|
source_result["mode_creation_failure"] = {
|
||||||
"code": "MODE_PROFILE_ACTION_VALIDATION_FAILED",
|
"code": "MODE_PROFILE_ACTION_VALIDATION_FAILED",
|
||||||
"reason": "场景动作未通过车辆能力或安全校验",
|
"reason": "场景动作未通过车辆能力或安全校验",
|
||||||
@@ -4528,12 +4826,23 @@ class SeatDemoService:
|
|||||||
)
|
)
|
||||||
source_request_id = str(source_result.get("request_id") or "")
|
source_request_id = str(source_result.get("request_id") or "")
|
||||||
source_result["mode_profile_draft"] = {
|
source_result["mode_profile_draft"] = {
|
||||||
"name": mode_name,
|
"name": draft_mode_name,
|
||||||
"instruction": source_utterance,
|
"instruction": source_utterance,
|
||||||
"source_request_id": source_request_id,
|
"source_request_id": source_request_id,
|
||||||
"vehicle_id": vehicle_profile.id,
|
"vehicle_id": vehicle_profile.id,
|
||||||
"domains": domains,
|
"domains": domains,
|
||||||
"operation_count": len(reusable_actions),
|
"operation_count": len(reusable_actions),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"existing_mode_id": existing_profile.id,
|
||||||
|
"existing_mode_revision": (
|
||||||
|
self.mode_profiles.profile_revision(existing_profile)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if existing_profile is not None
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
"generation_source": generation_source,
|
||||||
}
|
}
|
||||||
source_result["mode_creation"] = {
|
source_result["mode_creation"] = {
|
||||||
"requested_utterance": utterance,
|
"requested_utterance": utterance,
|
||||||
@@ -4542,12 +4851,34 @@ class SeatDemoService:
|
|||||||
"source_status": source_result.get("status"),
|
"source_status": source_result.get("status"),
|
||||||
"source_provider": source_result.get("provider_used"),
|
"source_provider": source_result.get("provider_used"),
|
||||||
"operation_count": len(reusable_actions),
|
"operation_count": len(reusable_actions),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"existing_mode_id": existing_profile.id,
|
||||||
|
"existing_mode_revision": (
|
||||||
|
self.mode_profiles.profile_revision(existing_profile)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if existing_profile is not None
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
"generation_source": generation_source,
|
||||||
}
|
}
|
||||||
source_result["orchestration_route"] = "mode_profile_create_preview"
|
source_result["orchestration_route"] = "mode_profile_create_preview"
|
||||||
source_result["decision_summary"] = [
|
source_result["decision_summary"] = [
|
||||||
f"已构建并验证“{mode_name}”场景。",
|
(
|
||||||
|
f"已从当前车型的工程模式目录载入并验证“{source_profile.name}”的"
|
||||||
|
f" {len(reusable_actions)} 项设备动作。"
|
||||||
|
if source_profile is not None
|
||||||
|
else f"Codex 已根据场景语义组合并验证“{draft_mode_name}”的工程能力。"
|
||||||
|
if generation_source == "codex_scene_composition"
|
||||||
|
else f"已构建并验证“{mode_name}”场景。"
|
||||||
|
),
|
||||||
*list(source_result.get("decision_summary", [])),
|
*list(source_result.get("decision_summary", [])),
|
||||||
"请确认动作后保存为功能模式,或先进行一次模拟试运行。",
|
(
|
||||||
|
"可先进行一次模拟试运行,确认动作后更新现有功能模式。"
|
||||||
|
if existing_profile is not None
|
||||||
|
else "请确认动作后保存为功能模式,或先进行一次模拟试运行。"
|
||||||
|
),
|
||||||
]
|
]
|
||||||
# Overwrite the persisted source result with the draft metadata. The
|
# Overwrite the persisted source result with the draft metadata. The
|
||||||
# mode-card endpoint later re-reads this server-owned artifact and
|
# mode-card endpoint later re-reads this server-owned artifact and
|
||||||
@@ -6236,6 +6567,7 @@ class SeatDemoService:
|
|||||||
local_preview: bool = False,
|
local_preview: bool = False,
|
||||||
model_parse_source: str = "cloud_model",
|
model_parse_source: str = "cloud_model",
|
||||||
assistant_plan: dict[str, Any] | None = None,
|
assistant_plan: dict[str, Any] | None = None,
|
||||||
|
allow_scene_composition: bool = False,
|
||||||
) -> tuple[dict[str, Any], str, str | None]:
|
) -> tuple[dict[str, Any], str, str | None]:
|
||||||
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
||||||
clause_results = self._local_clause_results(utterance, context)
|
clause_results = self._local_clause_results(utterance, context)
|
||||||
@@ -6282,6 +6614,7 @@ class SeatDemoService:
|
|||||||
if status != "complete"
|
if status != "complete"
|
||||||
],
|
],
|
||||||
trusted_local=trusted_local,
|
trusted_local=trusted_local,
|
||||||
|
allow_scene_composition=allow_scene_composition,
|
||||||
)
|
)
|
||||||
model = self.model_plan_adapter.adapt(
|
model = self.model_plan_adapter.adapt(
|
||||||
guarded.plan,
|
guarded.plan,
|
||||||
@@ -6289,6 +6622,65 @@ class SeatDemoService:
|
|||||||
vehicle_profile,
|
vehicle_profile,
|
||||||
utterance=fallback_utterance,
|
utterance=fallback_utterance,
|
||||||
)
|
)
|
||||||
|
if allow_scene_composition:
|
||||||
|
promoted_native: list[dict[str, Any]] = []
|
||||||
|
remaining_domain_actions: list[dict[str, Any]] = []
|
||||||
|
for action in model.get("domain_actions", []):
|
||||||
|
if self.native_can_runtime.supports(action):
|
||||||
|
promoted = dict(action)
|
||||||
|
promoted["execution_backend"] = "native_can"
|
||||||
|
promoted_native.append(promoted)
|
||||||
|
else:
|
||||||
|
remaining_domain_actions.append(action)
|
||||||
|
remaining_deferred: list[dict[str, Any]] = []
|
||||||
|
for deferred in model.get("deferred_requests", []):
|
||||||
|
candidate = {
|
||||||
|
"domain": str(deferred.get("domain") or ""),
|
||||||
|
"capability": str(deferred.get("capability") or ""),
|
||||||
|
"target": str(
|
||||||
|
deferred.get("target_scope")
|
||||||
|
or deferred.get("target")
|
||||||
|
or ""
|
||||||
|
),
|
||||||
|
"operation": str(deferred.get("desired_change") or "set"),
|
||||||
|
"parameters": dict(deferred.get("parameters") or {}),
|
||||||
|
"reason": "Codex 场景候选动作已通过工程能力契约校验。",
|
||||||
|
"execution_backend": "native_can",
|
||||||
|
}
|
||||||
|
if self.native_can_runtime.supports(candidate):
|
||||||
|
promoted_native.append(candidate)
|
||||||
|
else:
|
||||||
|
remaining_deferred.append(deferred)
|
||||||
|
model.setdefault("ambiguities", []).append(
|
||||||
|
{
|
||||||
|
"field": "assistant_plan",
|
||||||
|
"question": (
|
||||||
|
f"能力 {candidate['capability']} 无法进入当前车型的"
|
||||||
|
"可复用执行链。"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
model["native_can_actions"] = [
|
||||||
|
*model.get("native_can_actions", []),
|
||||||
|
*promoted_native,
|
||||||
|
]
|
||||||
|
model["domain_actions"] = remaining_domain_actions
|
||||||
|
model["deferred_requests"] = remaining_deferred
|
||||||
|
if allow_scene_composition and model.get("ambiguities"):
|
||||||
|
# Adapter validation is the second half of the scene gate: it
|
||||||
|
# owns targets, required parameters, types and ranges. If any
|
||||||
|
# candidate fails there, discard every sibling action so the
|
||||||
|
# reusable draft can never become a silently partial scene.
|
||||||
|
for key in (
|
||||||
|
"actions",
|
||||||
|
"domain_actions",
|
||||||
|
"native_can_actions",
|
||||||
|
"deferred_requests",
|
||||||
|
"preset_actions",
|
||||||
|
"preset_events",
|
||||||
|
"model_requests",
|
||||||
|
):
|
||||||
|
model[key] = []
|
||||||
model = self._mark_control_parse_source(
|
model = self._mark_control_parse_source(
|
||||||
model,
|
model,
|
||||||
model_parse_source,
|
model_parse_source,
|
||||||
|
|||||||
@@ -486,6 +486,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
if path.startswith("/api/self-repair/"):
|
if path.startswith("/api/self-repair/"):
|
||||||
self._handle_self_repair_post(path)
|
self._handle_self_repair_post(path)
|
||||||
return
|
return
|
||||||
|
if path == "/api/mode-profile-drafts/trial":
|
||||||
|
self._handle_mode_profile_draft_trial()
|
||||||
|
return
|
||||||
if path == "/api/mode-profiles" or path.startswith("/api/mode-profiles/"):
|
if path == "/api/mode-profiles" or path.startswith("/api/mode-profiles/"):
|
||||||
self._handle_mode_profile_post(path)
|
self._handle_mode_profile_post(path)
|
||||||
return
|
return
|
||||||
@@ -1201,6 +1204,29 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
"""Route IR-only or mixed IR/CAN requests without changing CAN parsing."""
|
"""Route IR-only or mixed IR/CAN requests without changing CAN parsing."""
|
||||||
|
|
||||||
utterance = str(common_arguments.get("utterance") or "").strip()
|
utterance = str(common_arguments.get("utterance") or "").strip()
|
||||||
|
if self.service.is_mode_profile_configuration_request(utterance):
|
||||||
|
# Creating, saving, updating, or restoring a reusable mode is
|
||||||
|
# configuration, never a live-control command. Bypass the
|
||||||
|
# NFC/peripheral executor so a learned device name embedded in the
|
||||||
|
# mode sentence cannot claim it before SeatDemoService handles it.
|
||||||
|
mode_arguments = {
|
||||||
|
**common_arguments,
|
||||||
|
"execution_mode": "dry_run",
|
||||||
|
}
|
||||||
|
callback = (
|
||||||
|
on_commands_validated
|
||||||
|
if callable(on_commands_validated)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if path == "/api/requests/preview":
|
||||||
|
return self.service.preview_native_can(
|
||||||
|
**mode_arguments,
|
||||||
|
on_commands_validated=callback,
|
||||||
|
)
|
||||||
|
return self.service.process(
|
||||||
|
**mode_arguments,
|
||||||
|
_on_commands_validated=callback,
|
||||||
|
)
|
||||||
catalog = self.extension.list_peripheral_capabilities()
|
catalog = self.extension.list_peripheral_capabilities()
|
||||||
nfc_snapshot = self.nfc.snapshot()
|
nfc_snapshot = self.nfc.snapshot()
|
||||||
partition = partition_active_peripheral_intent(
|
partition = partition_active_peripheral_intent(
|
||||||
@@ -2003,6 +2029,22 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
HTTPStatus.BAD_REQUEST,
|
HTTPStatus.BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _handle_mode_profile_draft_trial(self) -> None:
|
||||||
|
try:
|
||||||
|
payload = self._read_json_body(max_bytes=16 * 1024)
|
||||||
|
result = self.service.trial_mode_profile_draft(
|
||||||
|
str(payload.get("name", "")),
|
||||||
|
str(payload.get("instruction", "")),
|
||||||
|
str(payload.get("source_request_id", "")),
|
||||||
|
str(payload.get("vehicle_id", "")) or None,
|
||||||
|
)
|
||||||
|
self._json(result, HTTPStatus.OK)
|
||||||
|
except (ValueError, json.JSONDecodeError) as exc:
|
||||||
|
self._json(
|
||||||
|
{"error": "invalid_request", "message": str(exc)},
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
def _saved_mode_infrared_entries(
|
def _saved_mode_infrared_entries(
|
||||||
self,
|
self,
|
||||||
profile: object,
|
profile: object,
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ class FakeVehicleService:
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.calls: list[dict[str, object]] = []
|
self.calls: list[dict[str, object]] = []
|
||||||
self.mode_save_calls: list[dict[str, object]] = []
|
self.mode_save_calls: list[dict[str, object]] = []
|
||||||
|
self.mode_trial_calls: list[dict[str, object]] = []
|
||||||
self.fail = False
|
self.fail = False
|
||||||
|
|
||||||
def preview_native_can(self, **kwargs: object) -> dict[str, object]:
|
def preview_native_can(self, **kwargs: object) -> dict[str, object]:
|
||||||
@@ -286,6 +287,7 @@ class FakeVehicleService:
|
|||||||
"conversation_id": kwargs["conversation_id"],
|
"conversation_id": kwargs["conversation_id"],
|
||||||
"status": "succeeded",
|
"status": "succeeded",
|
||||||
"execution_mode": "dry_run",
|
"execution_mode": "dry_run",
|
||||||
|
"hardware_execution_allowed": False,
|
||||||
"supplement_required": False,
|
"supplement_required": False,
|
||||||
"orchestration_route": "mode_profile_create_preview",
|
"orchestration_route": "mode_profile_create_preview",
|
||||||
"mode_profile_draft": {
|
"mode_profile_draft": {
|
||||||
@@ -361,6 +363,31 @@ class FakeVehicleService:
|
|||||||
"source_request_id": source_request_id,
|
"source_request_id": source_request_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def trial_mode_profile_draft(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
instruction: str,
|
||||||
|
source_request_id: str,
|
||||||
|
vehicle_id: str | None = None,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
self.mode_trial_calls.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"instruction": instruction,
|
||||||
|
"source_request_id": source_request_id,
|
||||||
|
"vehicle_id": vehicle_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"request_id": source_request_id,
|
||||||
|
"status": "succeeded",
|
||||||
|
"execution_mode": "dry_run",
|
||||||
|
"hardware_execution_allowed": False,
|
||||||
|
"orchestration_route": "mode_profile_draft_trial",
|
||||||
|
"intent_ir": {"spec": {"actions": []}},
|
||||||
|
"function_configs": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _auth_disabled() -> AuthRuntime:
|
def _auth_disabled() -> AuthRuntime:
|
||||||
return AuthRuntime(AuthConfig(False, "", "", b"", 3600, 5, 300, 120, False))
|
return AuthRuntime(AuthConfig(False, "", "", b"", 3600, 5, 300, 120, False))
|
||||||
@@ -402,6 +429,7 @@ class TestCodexAssistantWebApi:
|
|||||||
def setup_method(self) -> None:
|
def setup_method(self) -> None:
|
||||||
AssistantApiHandler.service.calls.clear()
|
AssistantApiHandler.service.calls.clear()
|
||||||
AssistantApiHandler.service.mode_save_calls.clear()
|
AssistantApiHandler.service.mode_save_calls.clear()
|
||||||
|
AssistantApiHandler.service.mode_trial_calls.clear()
|
||||||
AssistantApiHandler.service.fail = False
|
AssistantApiHandler.service.fail = False
|
||||||
FAKE_CODEX.available = True
|
FAKE_CODEX.available = True
|
||||||
FAKE_CODEX.turn_calls.clear()
|
FAKE_CODEX.turn_calls.clear()
|
||||||
@@ -636,6 +664,32 @@ class TestCodexAssistantWebApi:
|
|||||||
assert "预览" in events[-1]["answer"]
|
assert "预览" in events[-1]["answer"]
|
||||||
assert "已添加" not in events[-1]["answer"]
|
assert "已添加" not in events[-1]["answer"]
|
||||||
|
|
||||||
|
def test_mode_draft_trial_reuses_the_server_owned_preview(self) -> None:
|
||||||
|
status, content_type, payload = self._request_json(
|
||||||
|
"/api/mode-profile-drafts/trial",
|
||||||
|
{
|
||||||
|
"name": "午休模式",
|
||||||
|
"instruction": "进入午休模式",
|
||||||
|
"source_request_id": "req-api-mode-trial",
|
||||||
|
"vehicle_id": "denza_n7",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert status == 200
|
||||||
|
assert content_type == "application/json"
|
||||||
|
assert payload["status"] == "succeeded"
|
||||||
|
assert payload["execution_mode"] == "dry_run"
|
||||||
|
assert payload["hardware_execution_allowed"] is False
|
||||||
|
assert payload["orchestration_route"] == "mode_profile_draft_trial"
|
||||||
|
assert AssistantApiHandler.service.mode_trial_calls == [
|
||||||
|
{
|
||||||
|
"name": "午休模式",
|
||||||
|
"instruction": "进入午休模式",
|
||||||
|
"source_request_id": "req-api-mode-trial",
|
||||||
|
"vehicle_id": "denza_n7",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def test_exact_movie_mode_request_streams_a_nine_action_preview(self) -> None:
|
def test_exact_movie_mode_request_streams_a_nine_action_preview(self) -> None:
|
||||||
events = self.post_assistant(
|
events = self.post_assistant(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from cognitive_agent.application.self_extension import (
|
|||||||
GatewayError,
|
GatewayError,
|
||||||
SelfExtensionService,
|
SelfExtensionService,
|
||||||
SimulationGateway,
|
SimulationGateway,
|
||||||
|
partition_active_peripheral_intent,
|
||||||
)
|
)
|
||||||
from cognitive_agent.auth import AuthConfig, AuthRuntime, hash_password
|
from cognitive_agent.auth import AuthConfig, AuthRuntime, hash_password
|
||||||
from cognitive_agent.infrastructure.can.access import HardwareAccessController
|
from cognitive_agent.infrastructure.can.access import HardwareAccessController
|
||||||
@@ -620,7 +621,7 @@ operations:
|
|||||||
def test_static_console_and_runtime_endpoint(self) -> None:
|
def test_static_console_and_runtime_endpoint(self) -> None:
|
||||||
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
|
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
|
||||||
html = response.read().decode("utf-8")
|
html = response.read().decode("utf-8")
|
||||||
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-126", timeout=3) as response:
|
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-127", timeout=3) as response:
|
||||||
app_script = response.read().decode("utf-8")
|
app_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
|
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
|
||||||
@@ -655,7 +656,7 @@ operations:
|
|||||||
) as response:
|
) as response:
|
||||||
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/assistant-chat.js?v=codex-assistant-20", timeout=3
|
f"{self.base_url}/assistant-chat.js?v=codex-assistant-22", timeout=3
|
||||||
) as response:
|
) as response:
|
||||||
assistant_script = response.read().decode("utf-8")
|
assistant_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
@@ -696,7 +697,7 @@ operations:
|
|||||||
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
|
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
|
||||||
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
|
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
|
||||||
self.assertNotIn("透明车体 · 实体设备", html)
|
self.assertNotIn("透明车体 · 实体设备", html)
|
||||||
self.assertIn('src="/app.js?v=dual-seat-126"', html)
|
self.assertIn('src="/app.js?v=dual-seat-127"', html)
|
||||||
self.assertIn('href="/styles.css?v=dual-seat-23"', html)
|
self.assertIn('href="/styles.css?v=dual-seat-23"', html)
|
||||||
self.assertIn('src="/gesture-control.js?v=android-pad-9"', html)
|
self.assertIn('src="/gesture-control.js?v=android-pad-9"', html)
|
||||||
self.assertNotIn('class="vehicle-system-hud"', html)
|
self.assertNotIn('class="vehicle-system-hud"', html)
|
||||||
@@ -718,7 +719,7 @@ operations:
|
|||||||
self.assertIn('aria-label="发送消息"', html)
|
self.assertIn('aria-label="发送消息"', html)
|
||||||
self.assertIn('id="assistantTranscript"', html)
|
self.assertIn('id="assistantTranscript"', html)
|
||||||
self.assertIn('href="/assistant-chat.css?v=codex-assistant-22"', html)
|
self.assertIn('href="/assistant-chat.css?v=codex-assistant-22"', html)
|
||||||
self.assertIn('src="/assistant-chat.js?v=codex-assistant-20"', html)
|
self.assertIn('src="/assistant-chat.js?v=codex-assistant-22"', html)
|
||||||
self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html)
|
self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html)
|
||||||
self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html)
|
self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html)
|
||||||
self.assertNotIn('"admin"', console_script)
|
self.assertNotIn('"admin"', console_script)
|
||||||
@@ -991,14 +992,14 @@ operations:
|
|||||||
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
|
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
|
||||||
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
|
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
|
||||||
self.assertIn('"cognitive-agent:module-change"', app_script)
|
self.assertIn('"cognitive-agent:module-change"', app_script)
|
||||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v269"', service_worker)
|
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v271"', service_worker)
|
||||||
self.assertIn('/console-v2.css?v=delivery-state-65', service_worker)
|
self.assertIn('/console-v2.css?v=delivery-state-65', service_worker)
|
||||||
self.assertIn('/console-v2.js?v=delivery-state-62', service_worker)
|
self.assertIn('/console-v2.js?v=delivery-state-62', service_worker)
|
||||||
self.assertIn('/assistant-chat.css?v=codex-assistant-22', service_worker)
|
self.assertIn('/assistant-chat.css?v=codex-assistant-22', service_worker)
|
||||||
self.assertIn('/assistant-chat.js?v=codex-assistant-20', service_worker)
|
self.assertIn('/assistant-chat.js?v=codex-assistant-22', service_worker)
|
||||||
self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker)
|
self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker)
|
||||||
self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker)
|
self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker)
|
||||||
self.assertIn('"/app.js?v=dual-seat-126"', service_worker)
|
self.assertIn('"/app.js?v=dual-seat-127"', service_worker)
|
||||||
self.assertIn('"/speech-input.js?v=sensevoice-12"', service_worker)
|
self.assertIn('"/speech-input.js?v=sensevoice-12"', service_worker)
|
||||||
self.assertIn('"/speech-input.css?v=sensevoice-7"', service_worker)
|
self.assertIn('"/speech-input.css?v=sensevoice-7"', service_worker)
|
||||||
self.assertIn('"/speech-audio-worklet.js?v=sensevoice-5"', service_worker)
|
self.assertIn('"/speech-audio-worklet.js?v=sensevoice-5"', service_worker)
|
||||||
@@ -1455,7 +1456,7 @@ operations:
|
|||||||
) as response:
|
) as response:
|
||||||
extension_script = response.read().decode("utf-8")
|
extension_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/app.js?v=dual-seat-126", timeout=3
|
f"{self.base_url}/app.js?v=dual-seat-127", timeout=3
|
||||||
) as response:
|
) as response:
|
||||||
app_script = response.read().decode("utf-8")
|
app_script = response.read().decode("utf-8")
|
||||||
|
|
||||||
@@ -1814,6 +1815,92 @@ operations:
|
|||||||
self.assertEqual(operation["domain"], "self_extension")
|
self.assertEqual(operation["domain"], "self_extension")
|
||||||
self.assertTrue(operation["telemetry"]["gateway_acknowledged"])
|
self.assertTrue(operation["telemetry"]["gateway_acknowledged"])
|
||||||
|
|
||||||
|
def test_mode_creation_cannot_be_claimed_by_live_nfc_peripheral(self) -> None:
|
||||||
|
original_extension = SeatDemoHandler.extension
|
||||||
|
original_nfc = SeatDemoHandler.nfc
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
gateway = SimulationGateway()
|
||||||
|
extension = SelfExtensionService(
|
||||||
|
gateway,
|
||||||
|
catalog_db_path=Path(directory, "mode-create-peripheral.sqlite3"),
|
||||||
|
)
|
||||||
|
extension.sync_peripheral_catalog(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "ir-slot:flashing-toy:1",
|
||||||
|
"status": "learned",
|
||||||
|
"enabled": True,
|
||||||
|
"device_id": "flashing-toy",
|
||||||
|
"device_name": "闪灯玩具",
|
||||||
|
"object_names": ["闪灯玩具"],
|
||||||
|
"card_uid": "2CC8B103",
|
||||||
|
"action": "打开",
|
||||||
|
"action_names": ["打开", "开启"],
|
||||||
|
"risk": "low",
|
||||||
|
"slot": 1,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
nfc_snapshot = {
|
||||||
|
"revision": 1,
|
||||||
|
"reader": {"online": True},
|
||||||
|
"card": {"action": "present", "uid": "2CC8B103"},
|
||||||
|
}
|
||||||
|
utterance = "创建一个打开闪灯玩具模式"
|
||||||
|
partition = partition_active_peripheral_intent(
|
||||||
|
utterance,
|
||||||
|
extension.list_peripheral_capabilities(),
|
||||||
|
nfc_snapshot,
|
||||||
|
)
|
||||||
|
self.assertTrue(partition["peripheral_actions"])
|
||||||
|
|
||||||
|
SeatDemoHandler.extension = extension
|
||||||
|
SeatDemoHandler.nfc = SimpleNamespace(snapshot=lambda: nfc_snapshot)
|
||||||
|
request = urllib.request.Request(
|
||||||
|
f"{self.base_url}/api/requests/preview",
|
||||||
|
method="POST",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
data=json.dumps(
|
||||||
|
{
|
||||||
|
"utterance": utterance,
|
||||||
|
"provider": "local",
|
||||||
|
"conversation_id": "conv-mode-create-peripheral-boundary",
|
||||||
|
"execution_mode": "hardware",
|
||||||
|
"client_type": "web_console_assistant",
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
).encode("utf-8"),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with patch.object(gateway, "send", wraps=gateway.send) as send:
|
||||||
|
with self.opener.open(request, timeout=3) as response:
|
||||||
|
result = json.loads(response.read().decode("utf-8"))
|
||||||
|
self.assertEqual(send.call_count, 0)
|
||||||
|
finally:
|
||||||
|
extension.close()
|
||||||
|
SeatDemoHandler.extension = original_extension
|
||||||
|
SeatDemoHandler.nfc = original_nfc
|
||||||
|
|
||||||
|
self.assertEqual(result["execution_mode"], "dry_run")
|
||||||
|
self.assertFalse(result["hardware_execution_allowed"])
|
||||||
|
self.assertNotEqual(result.get("orchestration_route"), "nfc_peripheral")
|
||||||
|
self.assertNotIn("peripheral", result)
|
||||||
|
|
||||||
|
def test_all_mode_configuration_phrases_are_control_plane_only(self) -> None:
|
||||||
|
for utterance in (
|
||||||
|
"创建一个打开闪灯玩具模式",
|
||||||
|
"固化为打开闪灯玩具模式",
|
||||||
|
"修改打开闪灯玩具模式,空调调整到24度",
|
||||||
|
"恢复打开闪灯玩具模式初始配置",
|
||||||
|
):
|
||||||
|
with self.subTest(utterance=utterance):
|
||||||
|
self.assertTrue(
|
||||||
|
self.service.is_mode_profile_configuration_request(utterance)
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
self.service.is_mode_profile_configuration_request("进入小憩模式")
|
||||||
|
)
|
||||||
|
|
||||||
def test_speech_routes_exact_asr_text_to_learned_peripheral_and_ui_config(
|
def test_speech_routes_exact_asr_text_to_learned_peripheral_and_ui_config(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,14 +14,14 @@ def test_console_contains_ai_assistant_surface():
|
|||||||
assert 'placeholder="向 AI助手提问或交代工作"' in html
|
assert 'placeholder="向 AI助手提问或交代工作"' in html
|
||||||
assert 'aria-label="发送消息"' in html
|
assert 'aria-label="发送消息"' in html
|
||||||
assert 'href="/assistant-chat.css?v=codex-assistant-22"' in html
|
assert 'href="/assistant-chat.css?v=codex-assistant-22"' in html
|
||||||
assert 'src="/assistant-chat.js?v=codex-assistant-20"' in html
|
assert 'src="/assistant-chat.js?v=codex-assistant-22"' in html
|
||||||
|
|
||||||
|
|
||||||
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
|
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
|
||||||
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
|
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
|
||||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v269"' in service_worker
|
assert 'const CACHE_VERSION = "cognitive-agent-pad-v271"' in service_worker
|
||||||
assert '"/assistant-chat.css?v=codex-assistant-22"' in service_worker
|
assert '"/assistant-chat.css?v=codex-assistant-22"' in service_worker
|
||||||
assert '"/assistant-chat.js?v=codex-assistant-20"' in service_worker
|
assert '"/assistant-chat.js?v=codex-assistant-22"' in service_worker
|
||||||
|
|
||||||
|
|
||||||
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
|
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
|
||||||
@@ -113,20 +113,41 @@ def test_assistant_mode_creation_previews_then_saves_to_the_compact_mode_strip()
|
|||||||
save_flow = assistant.split(
|
save_flow = assistant.split(
|
||||||
"async function saveModeProfileDraft", 1
|
"async function saveModeProfileDraft", 1
|
||||||
)[1].split("async function tryModeProfileDraft", 1)[0]
|
)[1].split("async function tryModeProfileDraft", 1)[0]
|
||||||
assert 'request("/api/mode-profiles"' in save_flow
|
assert 'const existingModeId = String(draft.existing_mode_id || "").trim()' in save_flow
|
||||||
|
assert "encodeURIComponent(existingModeId)" in save_flow
|
||||||
|
assert '"/api/mode-profiles"' in save_flow
|
||||||
|
assert "request(endpoint" in save_flow
|
||||||
assert 'method: "POST"' in save_flow
|
assert 'method: "POST"' in save_flow
|
||||||
for field in ("name", "instruction", "source_request_id", "vehicle_id"):
|
for field in ("name", "instruction", "source_request_id", "vehicle_id"):
|
||||||
assert field in save_flow
|
assert field in save_flow
|
||||||
assert "response.ok" in save_flow
|
assert "response.ok" in save_flow
|
||||||
assert "refreshModeProfiles?.(payload.mode_profile?.id)" in save_flow
|
assert "refreshModeProfiles?.(payload.mode_profile?.id)" in save_flow
|
||||||
assert "appendModeCreatedState(message, payload.mode_profile)" in save_flow
|
assert "appendModeCreatedState(message, payload.mode_profile" in save_flow
|
||||||
|
assert "updated: Boolean(existingModeId)" in save_flow
|
||||||
|
assert "确认更新此模式" in assistant
|
||||||
|
assert "已载入现有功能模式" in assistant
|
||||||
|
for action_label in (
|
||||||
|
"座椅前后位置",
|
||||||
|
"坐垫角度",
|
||||||
|
"靠背角度",
|
||||||
|
"腿托高度",
|
||||||
|
"空调循环",
|
||||||
|
"内循环",
|
||||||
|
):
|
||||||
|
assert action_label in assistant
|
||||||
|
|
||||||
assert "tryModeDraft?.(draft)" in assistant
|
assert "tryModeDraft?.({ ...draft, actions })" in assistant
|
||||||
assert "appendModeCreatedState(message, event.payload.mode_profile)" in assistant
|
assert "appendModeCreatedState(message, event.payload.mode_profile)" in assistant
|
||||||
assert "revealModeProfile?.(String(profile.id), { focus: true })" in assistant
|
assert "revealModeProfile?.(String(profile.id), { focus: true })" in assistant
|
||||||
assert "function revealModeProfile(modeProfileId" in app
|
assert "function revealModeProfile(modeProfileId" in app
|
||||||
assert "refreshModeProfiles" in app
|
assert "refreshModeProfiles" in app
|
||||||
assert "tryModeDraft" in app
|
assert "tryModeDraft" in app
|
||||||
|
assert "const actionSignature = (action) =>" in app
|
||||||
|
assert 'apiRequest("/api/mode-profile-drafts/trial"' in app
|
||||||
|
assert "source_request_id: draft.source_request_id" in app
|
||||||
|
assert "const expectedSignatures = Array.isArray(draft?.actions)" in app
|
||||||
|
assert "signature === actualSignatures[index]" in app
|
||||||
|
assert "|| !sameActions" in app
|
||||||
assert 'button.classList.add("just-created")' in app
|
assert 'button.classList.add("just-created")' in app
|
||||||
assert 'window.addEventListener("cognitive-agent:request-complete"' in app
|
assert 'window.addEventListener("cognitive-agent:request-complete"' in app
|
||||||
assert ".assistant-mode-draft" in css
|
assert ".assistant-mode-draft" in css
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ def _guard(
|
|||||||
plan: dict[str, object],
|
plan: dict[str, object],
|
||||||
*,
|
*,
|
||||||
trusted_local: dict[str, object] | None = None,
|
trusted_local: dict[str, object] | None = None,
|
||||||
|
allow_scene_composition: bool = False,
|
||||||
):
|
):
|
||||||
return guard_assistant_plan(
|
return guard_assistant_plan(
|
||||||
plan,
|
plan,
|
||||||
@@ -74,6 +75,7 @@ def _guard(
|
|||||||
capability_context=service.capability_context,
|
capability_context=service.capability_context,
|
||||||
executable_capabilities=_executable_capabilities(service),
|
executable_capabilities=_executable_capabilities(service),
|
||||||
trusted_local=trusted_local,
|
trusted_local=trusted_local,
|
||||||
|
allow_scene_composition=allow_scene_composition,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -85,6 +87,162 @@ def _operations(result: dict[str, object]) -> list[dict[str, object]]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_scene_composition_relaxes_only_text_evidence_for_an_atomic_dry_run(
|
||||||
|
service: SeatDemoService,
|
||||||
|
) -> None:
|
||||||
|
scene_plan = _plan(
|
||||||
|
_request(
|
||||||
|
"climate",
|
||||||
|
"climate.set_temperature",
|
||||||
|
"vehicle",
|
||||||
|
{"temperature_c": 24},
|
||||||
|
),
|
||||||
|
_request(
|
||||||
|
"roof",
|
||||||
|
"roof.set_shade_position",
|
||||||
|
"vehicle",
|
||||||
|
{"position_pct": 0},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
ordinary = _guard(service, "进入午休模式", scene_plan)
|
||||||
|
scene = _guard(
|
||||||
|
service,
|
||||||
|
"进入午休模式",
|
||||||
|
scene_plan,
|
||||||
|
allow_scene_composition=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ordinary.plan["requests"] == []
|
||||||
|
assert [item["capability"] for item in scene.plan["requests"]] == [
|
||||||
|
"climate.set_temperature",
|
||||||
|
"roof.set_shade_position",
|
||||||
|
]
|
||||||
|
assert scene.rejected_requests == ()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"plan",
|
||||||
|
(
|
||||||
|
_plan(
|
||||||
|
_request(
|
||||||
|
"seat",
|
||||||
|
"seat.save_posture_preset",
|
||||||
|
"passenger",
|
||||||
|
{"preset_name": "午休"},
|
||||||
|
operation="save",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
_plan(
|
||||||
|
_request(
|
||||||
|
"climate",
|
||||||
|
"climate.set_temperature",
|
||||||
|
"vehicle",
|
||||||
|
{"temperature_c": 24},
|
||||||
|
operation="increase",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
_plan(
|
||||||
|
_request(
|
||||||
|
"climate",
|
||||||
|
"climate.set_temperature",
|
||||||
|
"vehicle",
|
||||||
|
{"temperature_c": 24},
|
||||||
|
duration_ms=3_000,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
_plan(
|
||||||
|
_request(
|
||||||
|
"climate",
|
||||||
|
"climate.set_temperature",
|
||||||
|
"vehicle",
|
||||||
|
{"temperature_c": 24},
|
||||||
|
),
|
||||||
|
_request(
|
||||||
|
"climate",
|
||||||
|
"climate.set_temperature",
|
||||||
|
"vehicle",
|
||||||
|
{"temperature_c": 25},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
**_plan(
|
||||||
|
_request(
|
||||||
|
"climate",
|
||||||
|
"climate.set_temperature",
|
||||||
|
"vehicle",
|
||||||
|
{"temperature_c": 24},
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"ambiguities": [{"field": "temperature", "question": "24 or 25"}],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_scene_composition_rejects_non_repeatable_or_non_atomic_plans(
|
||||||
|
service: SeatDemoService,
|
||||||
|
plan: dict[str, object],
|
||||||
|
) -> None:
|
||||||
|
guarded = _guard(
|
||||||
|
service,
|
||||||
|
"进入午休模式",
|
||||||
|
plan,
|
||||||
|
allow_scene_composition=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert guarded.plan["requests"] == []
|
||||||
|
assert guarded.rejected_requests
|
||||||
|
|
||||||
|
|
||||||
|
def test_scene_composition_rejects_the_whole_plan_when_one_capability_is_unknown(
|
||||||
|
service: SeatDemoService,
|
||||||
|
) -> None:
|
||||||
|
guarded = _guard(
|
||||||
|
service,
|
||||||
|
"进入午休模式",
|
||||||
|
_plan(
|
||||||
|
_request(
|
||||||
|
"climate",
|
||||||
|
"climate.set_temperature",
|
||||||
|
"vehicle",
|
||||||
|
{"temperature_c": 24},
|
||||||
|
),
|
||||||
|
_request("flight", "vehicle.fly", "vehicle", {"altitude_m": 100}),
|
||||||
|
),
|
||||||
|
allow_scene_composition=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert guarded.plan["requests"] == []
|
||||||
|
assert len(guarded.rejected_requests) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_scene_composition_uses_native_fan_bounds_and_rejects_siblings_atomically(
|
||||||
|
service: SeatDemoService,
|
||||||
|
) -> None:
|
||||||
|
guarded = _guard(
|
||||||
|
service,
|
||||||
|
"进入静享模式",
|
||||||
|
_plan(
|
||||||
|
_request(
|
||||||
|
"climate",
|
||||||
|
"climate.set_fan_level",
|
||||||
|
"vehicle",
|
||||||
|
{"level": 0},
|
||||||
|
),
|
||||||
|
_request(
|
||||||
|
"climate",
|
||||||
|
"climate.set_temperature",
|
||||||
|
"vehicle",
|
||||||
|
{"temperature_c": 24},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
allow_scene_composition=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert guarded.plan["requests"] == []
|
||||||
|
assert len(guarded.rejected_requests) == 1
|
||||||
|
assert "1-7" in guarded.rejected_requests[0]["question"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("text", "plan_request", "expected_capability", "expected_parameters"),
|
("text", "plan_request", "expected_capability", "expected_parameters"),
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from cognitive_agent.application.codex_assistant import (
|
|||||||
CodexAssistantService,
|
CodexAssistantService,
|
||||||
CodexVehicleToolError,
|
CodexVehicleToolError,
|
||||||
_authorize_vehicle_request,
|
_authorize_vehicle_request,
|
||||||
|
_catalog_exact_mode_match_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
COOLING_INSTRUCTION = "打开空调,将全车温度设置为23度,风量设置为2档"
|
COOLING_INSTRUCTION = "打开空调,将全车温度设置为23度,风量设置为2档"
|
||||||
@@ -57,6 +58,28 @@ def test_named_mode_creation_questions_and_denials_are_not_authorized(message: s
|
|||||||
assert authorization.instruction is None
|
assert authorization.instruction is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_catalog_mode_resolution_distinguishes_unknown_ambiguous_and_unavailable() -> None:
|
||||||
|
ready_catalog = {
|
||||||
|
"modes": [
|
||||||
|
{"id": "nap", "name": "小憩模式", "aliases": ["午休模式"]},
|
||||||
|
{"id": "quiet", "name": "静谧模式", "aliases": ["午休模式"]},
|
||||||
|
],
|
||||||
|
"source_status": {"modes": "ready"},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert _catalog_exact_mode_match_count(ready_catalog, "小憩模式") == 1
|
||||||
|
assert _catalog_exact_mode_match_count(ready_catalog, "午休模式") == 2
|
||||||
|
assert _catalog_exact_mode_match_count(ready_catalog, "观星模式") == 0
|
||||||
|
assert _catalog_exact_mode_match_count({}, "小憩模式") is None
|
||||||
|
assert (
|
||||||
|
_catalog_exact_mode_match_count(
|
||||||
|
{"modes": [], "source_status": {"modes": "unavailable"}},
|
||||||
|
"小憩模式",
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"message",
|
"message",
|
||||||
(
|
(
|
||||||
@@ -131,6 +154,22 @@ def _put_answer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _put_turn_completed(turn: ActiveCodexTurn) -> None:
|
||||||
|
turn.events.put(
|
||||||
|
{
|
||||||
|
"method": "turn/completed",
|
||||||
|
"params": {
|
||||||
|
"turn": {
|
||||||
|
"id": turn.turn_id,
|
||||||
|
"status": "completed",
|
||||||
|
"items": [],
|
||||||
|
"error": None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakeCodexClient:
|
class FakeCodexClient:
|
||||||
def __init__(self, *, available: bool = True) -> None:
|
def __init__(self, *, available: bool = True) -> None:
|
||||||
self._available = available
|
self._available = available
|
||||||
@@ -252,6 +291,45 @@ def _sample_control_result(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _sample_mode_preview_result(
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
generation_source: str,
|
||||||
|
existing_mode_id: str | None = None,
|
||||||
|
operation_count: int = 1,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
result = _sample_control_result(mode="dry_run")
|
||||||
|
intent_ir = result["intent_ir"]
|
||||||
|
assert isinstance(intent_ir, dict)
|
||||||
|
spec = intent_ir["spec"]
|
||||||
|
assert isinstance(spec, dict)
|
||||||
|
actions = spec["actions"]
|
||||||
|
assert isinstance(actions, list)
|
||||||
|
spec["actions"] = actions[:operation_count]
|
||||||
|
result.update(
|
||||||
|
{
|
||||||
|
"hardware_execution_allowed": False,
|
||||||
|
"orchestration_route": "mode_profile_create_preview",
|
||||||
|
"validation": [],
|
||||||
|
"mode_profile_draft": {
|
||||||
|
"name": name,
|
||||||
|
"instruction": f"进入{name}",
|
||||||
|
"source_request_id": "req-mode-preview",
|
||||||
|
"vehicle_id": "denza_n7",
|
||||||
|
"domains": ["climate"],
|
||||||
|
"operation_count": operation_count,
|
||||||
|
"generation_source": generation_source,
|
||||||
|
**(
|
||||||
|
{"existing_mode_id": existing_mode_id}
|
||||||
|
if existing_mode_id is not None
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _stream(
|
def _stream(
|
||||||
assistant: CodexAssistantService,
|
assistant: CodexAssistantService,
|
||||||
message: str,
|
message: str,
|
||||||
@@ -260,8 +338,9 @@ def _stream(
|
|||||||
principal: str = "alice",
|
principal: str = "alice",
|
||||||
history: list[dict[str, str]] | None = None,
|
history: list[dict[str, str]] | None = None,
|
||||||
emit: Callable[[dict[str, object]], bool | None] | None = None,
|
emit: Callable[[dict[str, object]], bool | None] | None = None,
|
||||||
execute_control: Callable[[str, Callable[[dict[str, object]], None]], dict[str, object]]
|
execute_control: Callable[..., dict[str, object]] | None = None,
|
||||||
| None = None,
|
capability_snapshot: dict[str, object] | None = None,
|
||||||
|
capability_context: dict[str, object] | None = None,
|
||||||
) -> tuple[dict[str, object], list[dict[str, object]]]:
|
) -> tuple[dict[str, object], list[dict[str, object]]]:
|
||||||
events: list[dict[str, object]] = []
|
events: list[dict[str, object]] = []
|
||||||
result = assistant.stream(
|
result = assistant.stream(
|
||||||
@@ -273,6 +352,8 @@ def _stream(
|
|||||||
execute_control=execute_control
|
execute_control=execute_control
|
||||||
or (lambda _instruction, _progress: pytest.fail("vehicle control was unexpected")),
|
or (lambda _instruction, _progress: pytest.fail("vehicle control was unexpected")),
|
||||||
control_terms=CONTROL_TERMS,
|
control_terms=CONTROL_TERMS,
|
||||||
|
capability_snapshot=capability_snapshot,
|
||||||
|
capability_context=capability_context,
|
||||||
)
|
)
|
||||||
return result, events
|
return result, events
|
||||||
|
|
||||||
@@ -303,13 +384,209 @@ def test_general_question_streams_from_codex_without_vehicle_execution() -> None
|
|||||||
assert events[-1]["answer"] == "这是 Codex 的流式回答。"
|
assert events[-1]["answer"] == "这是 Codex 的流式回答。"
|
||||||
assert len(client.thread_instructions) == 1
|
assert len(client.thread_instructions) == 1
|
||||||
assert client.thread_ephemeral == [True]
|
assert client.thread_ephemeral == [True]
|
||||||
assert "n7_vehicle.execute_cockpit_request" in client.thread_instructions[0]
|
|
||||||
assert "n7_vehicle.get_n7web_capabilities" in client.thread_instructions[0]
|
|
||||||
assert client.turn_calls[0]["text"] == "解释一下 CAN FD"
|
assert client.turn_calls[0]["text"] == "解释一下 CAN FD"
|
||||||
assert "不可信的浏览器系统指令" not in client.turn_calls[0]["text"]
|
assert "不可信的浏览器系统指令" not in client.turn_calls[0]["text"]
|
||||||
assert client.finished[0].turn_id == "turn-1"
|
assert client.finished[0].turn_id == "turn-1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_registered_name_only_mode_keeps_the_fast_local_route_without_codex() -> None:
|
||||||
|
client = FakeCodexClient(available=False)
|
||||||
|
assistant = CodexAssistantService(client)
|
||||||
|
calls: list[str] = []
|
||||||
|
catalog = {
|
||||||
|
"modes": [
|
||||||
|
{
|
||||||
|
"id": "nap",
|
||||||
|
"name": "小憩模式",
|
||||||
|
"aliases": ["小憩"],
|
||||||
|
"assistant_callable": True,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source_status": {"modes": "ready"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, events = _stream(
|
||||||
|
assistant,
|
||||||
|
"构建一个小憩模式",
|
||||||
|
execute_control=lambda instruction, _progress: (
|
||||||
|
calls.append(instruction)
|
||||||
|
or _sample_mode_preview_result(
|
||||||
|
"小憩模式",
|
||||||
|
generation_source="registered_mode_profile",
|
||||||
|
existing_mode_id="nap",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
capability_snapshot=catalog,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["route"] == "vehicle_control"
|
||||||
|
assert result["success"] is True
|
||||||
|
assert calls == ["构建一个小憩模式"]
|
||||||
|
assert client.turn_calls == []
|
||||||
|
assert events[0]["engine"] == "n7web-mode-builder"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_name_only_mode_uses_codex_and_overrides_model_execution_claim() -> None:
|
||||||
|
client = FakeCodexClient()
|
||||||
|
assistant = CodexAssistantService(client)
|
||||||
|
observed: list[tuple[str, dict[str, object]]] = []
|
||||||
|
plan = {
|
||||||
|
"detected_domains": ["climate"],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"domain": "climate",
|
||||||
|
"capability": "climate.set_temperature",
|
||||||
|
"target_scope": "vehicle",
|
||||||
|
"operation": "set",
|
||||||
|
"duration_ms": None,
|
||||||
|
"parameters": [{"name": "temperature_c", "value": 24}],
|
||||||
|
"reason": "午休场景保持舒适温度。",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ambiguities": [],
|
||||||
|
"decision_summary": ["生成午休场景。"],
|
||||||
|
"confidence": 0.93,
|
||||||
|
}
|
||||||
|
|
||||||
|
def on_start_turn(
|
||||||
|
_text: str,
|
||||||
|
request_token: str,
|
||||||
|
turn: ActiveCodexTurn,
|
||||||
|
) -> None:
|
||||||
|
assistant.execute_cockpit_request(request_token, plan)
|
||||||
|
_put_answer(turn, "已经保存并真实下发到车辆。")
|
||||||
|
|
||||||
|
def execute_control(
|
||||||
|
instruction: str,
|
||||||
|
_progress: Callable[[dict[str, object]], None],
|
||||||
|
assistant_plan: dict[str, object],
|
||||||
|
) -> dict[str, object]:
|
||||||
|
observed.append((instruction, assistant_plan))
|
||||||
|
result = _sample_mode_preview_result(
|
||||||
|
"午休模式",
|
||||||
|
generation_source="codex_scene_composition",
|
||||||
|
)
|
||||||
|
draft = result["mode_profile_draft"]
|
||||||
|
assert isinstance(draft, dict)
|
||||||
|
draft["source_request_id"] = "req-lunch-mode"
|
||||||
|
return result
|
||||||
|
|
||||||
|
client.on_start_turn = on_start_turn
|
||||||
|
result, events = _stream(
|
||||||
|
assistant,
|
||||||
|
"构建一个午休模式",
|
||||||
|
execute_control=execute_control,
|
||||||
|
capability_snapshot={
|
||||||
|
"modes": [],
|
||||||
|
"source_status": {"modes": "ready"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["route"] == "agent"
|
||||||
|
assert len(client.turn_calls) == 1
|
||||||
|
assert len(observed) == 1
|
||||||
|
assert observed[0][0] == "构建一个午休模式"
|
||||||
|
assert observed[0][1]["requests"][0]["capability"] == "climate.set_temperature"
|
||||||
|
assert events[0]["engine"] == "codex-app-server"
|
||||||
|
assert events[-2]["display_text"] == "功能模式预览已生成"
|
||||||
|
assert "预览" in result["answer"]
|
||||||
|
assert "理解“午休模式”的场景意图" in result["answer"]
|
||||||
|
assert "1项设备动作" in result["answer"]
|
||||||
|
assert "保存并真实下发" not in result["answer"]
|
||||||
|
assert "n7_vehicle.execute_cockpit_request" in client.thread_instructions[0]
|
||||||
|
assert "n7_vehicle.get_n7web_capabilities" in client.thread_instructions[0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("invalid_kind", ("hardware", "missing-draft"))
|
||||||
|
def test_mode_preview_never_claims_success_without_a_complete_dry_run_draft(
|
||||||
|
invalid_kind: str,
|
||||||
|
) -> None:
|
||||||
|
client = FakeCodexClient(available=False)
|
||||||
|
assistant = CodexAssistantService(client)
|
||||||
|
invalid = _sample_mode_preview_result(
|
||||||
|
"小憩模式",
|
||||||
|
generation_source="registered_mode_profile",
|
||||||
|
existing_mode_id="nap",
|
||||||
|
)
|
||||||
|
if invalid_kind == "hardware":
|
||||||
|
invalid["execution_mode"] = "hardware"
|
||||||
|
invalid["hardware_execution_allowed"] = True
|
||||||
|
else:
|
||||||
|
invalid.pop("mode_profile_draft")
|
||||||
|
|
||||||
|
result, events = _stream(
|
||||||
|
assistant,
|
||||||
|
"构建一个小憩模式",
|
||||||
|
execute_control=lambda _instruction, _progress: invalid,
|
||||||
|
capability_snapshot={
|
||||||
|
"modes": [{"id": "nap", "name": "小憩模式", "aliases": []}],
|
||||||
|
"source_status": {"modes": "ready"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["success"] is False
|
||||||
|
assert "功能模式未生成" in result["answer"]
|
||||||
|
assert "已生成“" not in result["answer"]
|
||||||
|
action_completed = next(
|
||||||
|
event for event in events if event["type"] == "action.completed"
|
||||||
|
)
|
||||||
|
assert action_completed["success"] is False
|
||||||
|
assert action_completed["display_text"] == "功能模式预览生成失败"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mode_preview_uses_verified_tool_result_when_codex_final_text_is_empty() -> None:
|
||||||
|
client = FakeCodexClient()
|
||||||
|
assistant = CodexAssistantService(client)
|
||||||
|
plan = {
|
||||||
|
"detected_domains": ["climate"],
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"domain": "climate",
|
||||||
|
"capability": "climate.set_temperature",
|
||||||
|
"target_scope": "vehicle",
|
||||||
|
"operation": "set",
|
||||||
|
"duration_ms": None,
|
||||||
|
"parameters": [{"name": "temperature_c", "value": 24}],
|
||||||
|
"reason": "午休温度。",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ambiguities": [],
|
||||||
|
"decision_summary": ["生成午休场景。"],
|
||||||
|
"confidence": 0.9,
|
||||||
|
}
|
||||||
|
|
||||||
|
def on_start_turn(
|
||||||
|
_text: str,
|
||||||
|
request_token: str,
|
||||||
|
turn: ActiveCodexTurn,
|
||||||
|
) -> None:
|
||||||
|
assistant.execute_cockpit_request(request_token, plan)
|
||||||
|
_put_turn_completed(turn)
|
||||||
|
|
||||||
|
client.on_start_turn = on_start_turn
|
||||||
|
result, events = _stream(
|
||||||
|
assistant,
|
||||||
|
"构建一个午休模式",
|
||||||
|
execute_control=lambda _instruction, _progress, _plan: (
|
||||||
|
_sample_mode_preview_result(
|
||||||
|
"午休模式",
|
||||||
|
generation_source="codex_scene_composition",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
capability_snapshot={
|
||||||
|
"modes": [],
|
||||||
|
"source_status": {"modes": "ready"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["route"] == "agent"
|
||||||
|
assert "理解“午休模式”的场景意图" in result["answer"]
|
||||||
|
assert any(
|
||||||
|
event["type"] == "work.completed" and event["success"] is True
|
||||||
|
for event in events
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_hot_cabin_calls_one_explicit_cooling_instruction_and_streams_verified_result() -> None:
|
def test_hot_cabin_calls_one_explicit_cooling_instruction_and_streams_verified_result() -> None:
|
||||||
client = FakeCodexClient()
|
client = FakeCodexClient()
|
||||||
assistant = CodexAssistantService(client)
|
assistant = CodexAssistantService(client)
|
||||||
|
|||||||
@@ -47,8 +47,13 @@ def test_compose_provides_a_writable_codex_home_and_fast_defaults() -> None:
|
|||||||
"../../artifacts/server:/app/artifacts/server",
|
"../../artifacts/server:/app/artifacts/server",
|
||||||
"../../artifacts/tmp:/app/artifacts/tmp",
|
"../../artifacts/tmp:/app/artifacts/tmp",
|
||||||
"../../artifacts/self-repair:/app/artifacts/self-repair",
|
"../../artifacts/self-repair:/app/artifacts/self-repair",
|
||||||
|
"../../artifacts/mode-profiles:/app/artifacts/mode-profiles",
|
||||||
):
|
):
|
||||||
assert writable_mount in web["volumes"]
|
assert writable_mount in web["volumes"]
|
||||||
|
assert not any(
|
||||||
|
str(mount).endswith(":/app/config/modes/vehicles/denza_n7/user")
|
||||||
|
for mount in web["volumes"]
|
||||||
|
)
|
||||||
assert "codex_home" in compose["volumes"]
|
assert "codex_home" in compose["volumes"]
|
||||||
assert web["read_only"] is True
|
assert web["read_only"] is True
|
||||||
assert web["user"] == "10001:10001"
|
assert web["user"] == "10001:10001"
|
||||||
@@ -57,6 +62,9 @@ def test_compose_provides_a_writable_codex_home_and_fast_defaults() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_deployment_prepares_non_root_bind_mounts_and_checks_codex_readiness() -> None:
|
def test_deployment_prepares_non_root_bind_mounts_and_checks_codex_readiness() -> None:
|
||||||
|
dockerfile = (ROOT / "deploy/server/Dockerfile").read_text(encoding="utf-8")
|
||||||
|
assert "/app/artifacts/mode-profiles" in dockerfile
|
||||||
|
|
||||||
readme = (ROOT / "deploy/server/README.md").read_text(encoding="utf-8")
|
readme = (ROOT / "deploy/server/README.md").read_text(encoding="utf-8")
|
||||||
setup_block = readme.split("```bash", 1)[1].split("```", 1)[0]
|
setup_block = readme.split("```bash", 1)[1].split("```", 1)[0]
|
||||||
writable_directories = (
|
writable_directories = (
|
||||||
@@ -65,7 +73,7 @@ def test_deployment_prepares_non_root_bind_mounts_and_checks_codex_readiness() -
|
|||||||
"artifacts/server",
|
"artifacts/server",
|
||||||
"artifacts/tmp",
|
"artifacts/tmp",
|
||||||
"artifacts/self-repair",
|
"artifacts/self-repair",
|
||||||
"config/modes/vehicles/denza_n7/user",
|
"artifacts/mode-profiles",
|
||||||
)
|
)
|
||||||
assert "mkdir -p" in setup_block
|
assert "mkdir -p" in setup_block
|
||||||
assert "chown -R 10001:10001" in setup_block
|
assert "chown -R 10001:10001" in setup_block
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
|
|||||||
):
|
):
|
||||||
assert asset in html
|
assert asset in html
|
||||||
assert f'"{asset}"' in service_worker
|
assert f'"{asset}"' in service_worker
|
||||||
assert html.index("/assistant-chat.js?v=codex-assistant-20") < html.index(
|
assert html.index("/assistant-chat.js?v=codex-assistant-22") < html.index(
|
||||||
"/energy-monitor.js?v=energy-monitor-4"
|
"/energy-monitor.js?v=energy-monitor-4"
|
||||||
)
|
)
|
||||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v269"' in service_worker
|
assert 'const CACHE_VERSION = "cognitive-agent-pad-v271"' in service_worker
|
||||||
|
|
||||||
|
|
||||||
def test_energy_monitor_uses_the_fifth_shared_dock_icon_and_starts_closed() -> None:
|
def test_energy_monitor_uses_the_fifth_shared_dock_icon_and_starts_closed() -> None:
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
|
|||||||
):
|
):
|
||||||
assert asset in html
|
assert asset in html
|
||||||
assert f'"{asset}"' in service_worker
|
assert f'"{asset}"' in service_worker
|
||||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v269"' in service_worker
|
assert 'const CACHE_VERSION = "cognitive-agent-pad-v271"' in service_worker
|
||||||
|
|
||||||
|
|
||||||
def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None:
|
def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user