(function initializeUnifiedAssistant() {
const STORAGE_KEY = "cognitive-agent.assistant-messages";
const CONVERSATION_STORAGE_KEY = "cognitive-agent.conversation-id";
const ACTIVE_MODE_DRAFT_STORAGE_KEY = "cognitive-agent.active-mode-draft";
const MAX_STORED_MESSAGES = 24;
const COMPOSER_MIN_HEIGHT = 64;
const COMPOSER_MAX_HEIGHT = 116;
const MAX_CONTROL_SUGGESTIONS = 4;
const WELCOME_TEXT = "你好,我是 AI助手。你可以向我提问,也可以直接交代需要完成的工作。";
let pendingController = null;
let pendingMessage = null;
let latestControlResult = null;
let activeModeDraftContext = null;
let initialized = false;
const $ = (selector, root = document) => root.querySelector(selector);
function setDrawerOpen(open, { restoreFocus = false } = {}) {
const drawer = $("#assistantDrawer");
const trigger = $("#assistantDrawerToggle");
if (!drawer || !trigger) return;
document.body.classList.toggle("assistant-drawer-open", open);
trigger.classList.toggle("active", open);
trigger.setAttribute("aria-pressed", String(open));
trigger.setAttribute("aria-expanded", String(open));
const triggerLabel = open ? "收起AI助手" : "展开AI助手";
trigger.setAttribute("aria-label", triggerLabel);
trigger.title = triggerLabel;
drawer.setAttribute("aria-hidden", String(!open));
drawer.toggleAttribute("inert", !open);
const speech = window.CognitiveAgentSpeech;
if (typeof speech?.setAssistantOpen === "function") {
void speech.setAssistantOpen(open);
}
if (open) {
requestAnimationFrame(() => $("#utterance")?.focus({ preventScroll: true }));
} else {
// A collapsed assistant must never leave an invisible microphone or
// preview request running in the background.
if (typeof speech?.setAssistantOpen !== "function") {
void speech?.stop?.("AI助手已收起,语音输入已停止");
}
if (restoreFocus) trigger.focus({ preventScroll: true });
}
}
function installAssistantDrawer() {
const drawer = $(".v2-operation-shell");
const heading = $(".v2-operation-heading", drawer);
if (!drawer || !heading || $("#assistantDrawerToggle")) return;
drawer.id = "assistantDrawer";
drawer.setAttribute("role", "dialog");
drawer.setAttribute("aria-label", "AI助手");
const actions = document.createElement("div");
actions.className = "assistant-drawer-actions";
const reset = $("#newConversation");
if (reset) {
reset.classList.add("assistant-drawer-new");
actions.append(reset);
}
heading.append(actions);
const trigger = document.createElement("button");
trigger.id = "assistantDrawerToggle";
trigger.className = "assistant-drawer-toggle v2-panel-control-button";
trigger.type = "button";
trigger.setAttribute("aria-controls", drawer.id);
trigger.setAttribute("aria-pressed", "false");
trigger.setAttribute("aria-expanded", "false");
trigger.setAttribute("aria-label", "展开AI助手");
trigger.title = "展开AI助手";
trigger.innerHTML = 'AI助手';
const panelDock = $("#v2PanelControlDock");
if (panelDock) {
const firstRightPanelToggle = $("[data-panel-toggle]", panelDock);
if (firstRightPanelToggle) panelDock.insertBefore(trigger, firstRightPanelToggle);
else panelDock.append(trigger);
} else {
document.body.append(trigger);
}
trigger.addEventListener("click", () => {
setDrawerOpen(!document.body.classList.contains("assistant-drawer-open"));
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && document.body.classList.contains("assistant-drawer-open")) {
event.preventDefault();
setDrawerOpen(false, { restoreFocus: true });
}
});
window.addEventListener("cognitive-agent:module-change", (event) => {
if (event.detail?.module === "self-extension") setDrawerOpen(false);
});
setDrawerOpen(true);
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}
function createMessageId() {
if (globalThis.crypto?.randomUUID) return `ui-${globalThis.crypto.randomUUID()}`;
return `ui-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
}
function createConversationId() {
if (globalThis.crypto?.randomUUID) return `conv-${globalThis.crypto.randomUUID()}`;
return `conv-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
}
function conversationId() {
const existing = sessionStorage.getItem(CONVERSATION_STORAGE_KEY)
|| window.CognitiveAgentConsole?.getConversationId?.();
if (existing) return existing;
const created = createConversationId();
sessionStorage.setItem(CONVERSATION_STORAGE_KEY, created);
return created;
}
function normalizeModeName(value) {
return String(value || "").trim().replace(/\s+/g, "").toLowerCase();
}
function sanitizeModeDraftContext(value) {
if (!value || typeof value !== "object") return null;
const name = String(value.name || "").trim();
const sourceRequestId = String(value.source_request_id || "").trim();
const vehicleId = String(value.vehicle_id || "").trim();
const draftConversationId = String(value.conversation_id || "").trim();
if (
!name
|| name.length > 64
|| !/^req-[a-z0-9-]{6,100}$/.test(sourceRequestId)
) return null;
if (!vehicleId || vehicleId.length > 80 || draftConversationId !== conversationId()) return null;
return {
name,
source_request_id: sourceRequestId,
vehicle_id: vehicleId,
conversation_id: draftConversationId,
};
}
function loadActiveModeDraftContext() {
try {
return sanitizeModeDraftContext(
JSON.parse(sessionStorage.getItem(ACTIVE_MODE_DRAFT_STORAGE_KEY) || "null"),
);
} catch (_error) {
return null;
}
}
function rememberModeDraftContext(draft) {
const candidate = sanitizeModeDraftContext({
name: draft?.name,
source_request_id: draft?.source_request_id,
vehicle_id: draft?.vehicle_id || $("#vehicleSelect")?.value || null,
conversation_id: conversationId(),
});
if (!candidate) return;
activeModeDraftContext = candidate;
sessionStorage.setItem(ACTIVE_MODE_DRAFT_STORAGE_KEY, JSON.stringify(candidate));
}
function clearActiveModeDraftContext(sourceRequestId = null) {
if (
sourceRequestId
&& activeModeDraftContext?.source_request_id !== String(sourceRequestId)
) return;
activeModeDraftContext = null;
sessionStorage.removeItem(ACTIVE_MODE_DRAFT_STORAGE_KEY);
}
function isModeDraftUpdateRequest(message, context) {
const compact = String(message || "").trim().replace(/\s+/g, "");
if (!compact || !context?.name) return false;
const politePrefix = "(?:(?:请|麻烦)(?:帮我|帮忙|给我)?|帮我|帮忙|给我)?";
const prefixUpdateVerb = "(?:修改一下|修改|调整|更改|更新|改一下|变更)";
const changePredicate = "(?:修改|改成|改为|更改|更新|变更|变为|变成|换成|调整|调节|调高|调低|调到|调成|调为|调至|设置|设为|设成|设到|设至|打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|复位|半开|开到|关到|拉开|拉上|上锁|解锁)";
const modeName = "([^,。!?!?、;;::]{1,64}?模式)";
const modePossessiveBoundary = "(?:中的|里的|内的|之中的|的|中|里|内)";
const modeMemberBoundary = `(?:${modePossessiveBoundary}|[,,::])`;
const question = /为什么|什么|怎么|如何|介绍|解释|作用|区别|原理|哪些|多少|几(?:度|档|级|个|项)?|是否|能否|可不可以|合适吗|合理吗|会怎样|列出|告诉我|[??]|(?:吗|么|呢)[。.!!]?$/;
const candidates = [
{
pattern: new RegExp(`^${politePrefix}${prefixUpdateVerb}${modeName}${modeMemberBoundary}[,,::]?(.*)$`),
predicateRequired: false,
},
{
pattern: new RegExp(`^${politePrefix}(?:把|将)${modeName}${modeMemberBoundary}[,,::]?(.+)$`),
predicateRequired: true,
},
{
pattern: new RegExp(`^${politePrefix}${modeName}${modeMemberBoundary}[,,::]?(.+)$`),
predicateRequired: true,
},
];
return candidates.some(({ pattern, predicateRequired }) => {
const matched = compact.match(pattern);
const change = String(matched?.[2] || "");
return Boolean(
matched
&& normalizeModeName(matched[1]) === normalizeModeName(context.name)
&& !question.test(change)
&& (!predicateRequired || new RegExp(changePredicate).test(change))
);
});
}
function requestModeDraftContext(message) {
const context = activeModeDraftContext || loadActiveModeDraftContext();
if (!context || !isModeDraftUpdateRequest(message, context)) return null;
activeModeDraftContext = context;
return {
name: context.name,
source_request_id: context.source_request_id,
vehicle_id: context.vehicle_id,
};
}
function loadStoredMessages() {
try {
const stored = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "[]");
return Array.isArray(stored)
? stored.filter((item) => item && ["user", "assistant"].includes(item.role))
: [];
} catch (_error) {
return [];
}
}
function persistMessages() {
const messages = [...document.querySelectorAll("#assistantMessages [data-message-role]")]
.map((node) => ({
role: node.dataset.messageRole,
content: node.dataset.messageContent || "",
}))
.filter((item) => item.content && item.content !== WELCOME_TEXT)
.slice(-MAX_STORED_MESSAGES);
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(messages));
}
function appendInlineText(container, text) {
const parts = String(text).split(/(`[^`\n]+`)/g);
parts.forEach((part) => {
if (part.startsWith("`") && part.endsWith("`") && part.length > 2) {
const code = document.createElement("code");
code.textContent = part.slice(1, -1);
container.append(code);
} else {
container.append(document.createTextNode(part));
}
});
}
function renderRichText(container, value) {
container.replaceChildren();
const sections = String(value).split(/```(?:[\w+-]+)?\n?([\s\S]*?)```/g);
sections.forEach((section, index) => {
if (index % 2 === 1) {
const pre = document.createElement("pre");
const code = document.createElement("code");
code.textContent = section.replace(/\n$/, "");
pre.append(code);
container.append(pre);
return;
}
const lines = section.split("\n");
let list = null;
lines.forEach((line) => {
const bullet = line.match(/^\s*[-*]\s+(.+)$/);
if (bullet) {
if (!list) {
list = document.createElement("ul");
container.append(list);
}
const item = document.createElement("li");
appendInlineText(item, bullet[1]);
list.append(item);
return;
}
list = null;
if (!line.trim()) return;
const heading = line.match(/^#{1,3}\s+(.+)$/);
const block = document.createElement(heading ? "strong" : "p");
if (heading) block.className = "assistant-inline-heading";
appendInlineText(block, heading ? heading[1] : line);
container.append(block);
});
});
}
function shouldFollowLatest(transcript) {
return transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 90;
}
function followLatest(force = false) {
const transcript = $("#assistantTranscript");
if (!transcript || (!force && !shouldFollowLatest(transcript))) return;
requestAnimationFrame(() => {
transcript.scrollTop = transcript.scrollHeight;
});
}
function resizeComposerInput(input = $("#utterance")) {
if (!input) return;
input.style.height = "auto";
const contentHeight = input.scrollHeight;
const nextHeight = Math.max(
COMPOSER_MIN_HEIGHT,
Math.min(contentHeight, COMPOSER_MAX_HEIGHT),
);
input.style.height = `${nextHeight}px`;
const scrollable = contentHeight > COMPOSER_MAX_HEIGHT + 1;
input.classList.toggle("is-scrollable", scrollable);
if (!scrollable) input.scrollTop = 0;
}
function createMessage(role, content = "", { pending = false } = {}) {
const article = document.createElement("article");
article.className = `assistant-message assistant-message-${role}`;
article.dataset.messageId = createMessageId();
article.dataset.messageRole = role;
article.dataset.messageContent = content;
if (role === "assistant") {
const avatar = document.createElement("span");
avatar.className = "assistant-avatar";
avatar.setAttribute("aria-hidden", "true");
avatar.innerHTML = '';
article.append(avatar);
}
const body = document.createElement("div");
body.className = "assistant-message-body";
const contentNode = document.createElement("div");
contentNode.className = "assistant-message-content";
if (pending && !content) {
contentNode.classList.add("assistant-thinking");
contentNode.innerHTML = '正在理解你的需求';
} else if (role === "assistant") {
renderRichText(contentNode, content);
} else {
contentNode.textContent = content;
}
body.append(contentNode);
const action = document.createElement("div");
action.className = "assistant-action-status";
action.hidden = true;
action.setAttribute("role", "status");
body.append(action);
article.append(body);
$("#assistantMessages").append(article);
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
followLatest(true);
return { article, body, contentNode, action, content };
}
function setMessageContent(message, content, { final = false } = {}) {
message.content = content;
message.article.dataset.messageContent = content;
message.contentNode.classList.remove("assistant-thinking");
if (final) renderRichText(message.contentNode, content);
else message.contentNode.textContent = content;
followLatest();
}
function setActionStatus(message, text, state = "working") {
message.action.hidden = !text;
message.action.className = `assistant-action-status ${state}`;
message.action.replaceChildren();
if (!text) return;
const icon = document.createElement("i");
icon.setAttribute(
"data-lucide",
state === "success" ? "circle-check" : state === "failed" ? "circle-x" : "loader-circle",
);
const label = document.createElement("span");
label.textContent = text;
message.action.append(icon, label);
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
followLatest();
}
function normalizeControlSuggestions(rawSuggestions) {
if (!Array.isArray(rawSuggestions)) return [];
const seenCommands = new Set();
const suggestions = [];
rawSuggestions.forEach((rawSuggestion, index) => {
const source = typeof rawSuggestion === "string"
? { command: rawSuggestion, label: rawSuggestion }
: rawSuggestion;
if (!source || typeof source !== "object") return;
const command = String(source.command || "").trim().replace(/\s+/g, " ");
if (!command || command.length > 160) return;
const commandKey = command.toLocaleLowerCase("zh-CN").replace(/\s+/g, "");
if (seenCommands.has(commandKey)) return;
seenCommands.add(commandKey);
suggestions.push({
id: String(source.id || `suggestion-${index + 1}`).trim(),
label: String(source.label || command).trim().slice(0, 64) || command,
command,
domain: String(source.domain || "").trim().toLowerCase(),
});
});
return suggestions.slice(0, MAX_CONTROL_SUGGESTIONS);
}
function controlSuggestionIcon(domain) {
if (["climate", "hvac"].includes(domain)) return "wind";
if (domain.includes("seat")) return "armchair";
if (["roof", "shade", "sunshade", "window"].includes(domain)) return "panel-top";
if (["light", "lighting", "ambient_light", "interior_light"].includes(domain)) {
return "lightbulb";
}
return "circle-play";
}
function setSuggestionGroupPending(group, activeButton, pending) {
group.classList.toggle("is-sending", pending);
group.setAttribute("aria-busy", String(pending));
group.querySelectorAll("button").forEach((button) => {
button.disabled = pending;
const label = button.querySelector("strong");
if (label && button === activeButton) {
label.textContent = pending ? "正在发送…" : button.dataset.suggestionLabel;
}
button.classList.toggle("is-sending", pending && button === activeButton);
button.setAttribute("aria-busy", String(pending && button === activeButton));
});
}
async function submitSuggestedCommand(command, group, button) {
if (button.disabled) return;
if (pendingController) {
window.CognitiveAgentConsole?.notify?.("请等待当前回答完成后再执行其他指令");
return;
}
setSuggestionGroupPending(group, button, true);
try {
await submitAssistant(command);
} finally {
setSuggestionGroupPending(group, button, false);
}
}
function appendControlSuggestions(message, rawSuggestions) {
if (!message?.body) return;
const suggestions = normalizeControlSuggestions(rawSuggestions);
if (!suggestions.length) return;
message.article?.classList.add("assistant-message-control-suggestions");
const signature = JSON.stringify(suggestions.map(({ id, command }) => [id, command]));
let group = $(".assistant-control-suggestions", message.body);
if (group?.dataset.suggestionSignature === signature) return;
if (!group) {
group = document.createElement("section");
group.className = "assistant-control-suggestions";
group.setAttribute("role", "group");
group.setAttribute("aria-label", "可以尝试的其他控制指令");
message.body.append(group);
}
group.dataset.suggestionSignature = signature;
group.replaceChildren();
const heading = document.createElement("p");
heading.className = "assistant-control-suggestions-title";
const headingIcon = document.createElement("i");
headingIcon.setAttribute("data-lucide", "sparkles");
const headingText = document.createElement("span");
headingText.textContent = "你还可以试试";
heading.append(headingIcon, headingText);
group.append(heading);
const list = document.createElement("div");
list.className = "assistant-control-suggestion-list";
suggestions.forEach((suggestion) => {
const button = document.createElement("button");
button.type = "button";
button.className = "assistant-control-suggestion";
button.dataset.suggestionId = suggestion.id;
button.dataset.suggestionCommand = suggestion.command;
button.dataset.suggestionLabel = suggestion.label;
button.setAttribute("aria-label", `执行控制指令:${suggestion.label}`);
button.setAttribute("aria-busy", "false");
button.title = suggestion.command;
const icon = document.createElement("span");
icon.className = "assistant-control-suggestion-icon";
icon.innerHTML = ``;
const copy = document.createElement("span");
copy.className = "assistant-control-suggestion-copy";
const hint = document.createElement("small");
hint.textContent = "点击直接执行";
const label = document.createElement("strong");
label.textContent = suggestion.label;
copy.append(label, hint);
const arrow = document.createElement("i");
arrow.setAttribute("data-lucide", "arrow-up-right");
arrow.className = "assistant-control-suggestion-arrow";
button.append(icon, copy, arrow);
button.addEventListener("click", () => {
void submitSuggestedCommand(suggestion.command, group, button);
});
list.append(button);
});
group.append(list);
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
followLatest(true);
}
const assistantControlLabels = {
"climate.set_power": "空调电源",
"climate.set_temperature": "空调温度",
"climate.set_fan_level": "空调风量",
"climate.set_outlet_mode": "出风模式",
"climate.set_control_mode": "空调控制方式",
"climate.set_cycle_mode": "空调循环",
"interior_light.set_indoor_power": "室内灯",
"interior_light.set_reading_power": "阅读灯",
"ambient_light.set_power": "氛围灯",
"ambient_light.set_color_index": "氛围灯颜色",
"ambient_light.set_brightness_level": "氛围灯亮度",
"ambient_light.set_illumination": "氛围灯颜色与亮度",
"ambient_light.set_mode": "氛围灯模式",
"roof.set_shade_motion": "遮阳帘",
"roof.set_shade_position": "遮阳帘位置",
"window.set_motion": "车窗",
"window.set_position": "车窗位置",
"door.set_rear_lock": "后排门锁",
"door.set_vehicle_lock": "整车门锁",
"infrared.emit": "红外外设",
};
function assistantControlCategory(operation = {}, fallback = "控制指令") {
const capability = String(operation.capability || "");
if (capability === "infrared.emit") {
const deviceName = String(
operation.target?.name || operation.target?.device || "红外外设",
).trim();
return deviceName || "红外外设";
}
if (assistantControlLabels[capability]) return assistantControlLabels[capability];
if (capability.startsWith("seat.")) {
const target = String(operation.target?.zone || operation.target?.instance || "")
.replaceAll("_", "-");
return `${["passenger", "front-right"].includes(target) ? "副驾" : "主驾"}座椅`;
}
if (capability.startsWith("outlet.")) return "空调风口";
return String(fallback || capability || "控制指令");
}
function fallbackAssistantControlCommands(result = {}) {
return (Array.isArray(result.validated_control_commands)
? result.validated_control_commands
: [])
.map((presentation) => {
const operation = presentation?.operation || {};
const command = String(presentation?.actual_command || "").trim();
if (!command) return null;
const capability = String(operation.capability || "");
const target = operation.target || {};
const targetValue = target.zone || target.instance || target.device;
const semanticArguments = [];
if (targetValue != null && String(targetValue).trim()) {
semanticArguments.push(`target=${String(targetValue).replaceAll("_", "-")}`);
}
Object.entries(operation.parameters || {})
.sort(([left], [right]) => left.localeCompare(right))
.forEach(([key, value]) => semanticArguments.push(`${key}=${String(value)}`));
return {
category: assistantControlCategory(operation, capability),
channel: String(presentation?.channel || ""),
command,
deliveryState: "not_submitted",
operation,
operationRef: String(operation.id || ""),
semantic: `${capability || "control"}(${semanticArguments.join(", ")})`,
};
})
.filter(Boolean);
}
function assistantDispatchedCommands(result = {}) {
const projections = Array.isArray(result.validated_control_commands)
? result.validated_control_commands
: [];
const projectionByRef = new Map(projections.map((presentation) => [
String(presentation?.operation?.id || ""),
presentation?.operation || {},
]));
let commands = [];
try {
const sharedCommands = window.CognitiveAgentConsoleV2?.getParsedCanCommands?.(result);
if (Array.isArray(sharedCommands)) commands = sharedCommands;
} catch (_error) {
commands = [];
}
if (!commands.length) return fallbackAssistantControlCommands(result);
return commands.map((item, index) => {
const operation = projectionByRef.get(String(item.operationRef || ""))
|| projections[index]?.operation
|| {};
return {
...item,
category: assistantControlCategory(operation, item.category),
operation,
};
});
}
function localizedAssistantControlValue(value) {
const normalized = String(value ?? "").trim().toLowerCase();
return {
on: "开启",
open: "开启",
true: "开启",
off: "关闭",
close: "关闭",
false: "关闭",
half: "一半",
stop: "停止",
lock: "上锁",
unlock: "解锁",
auto: "自动",
manual: "手动",
internal: "内循环",
external: "外循环",
}[normalized] || String(value ?? "").trim();
}
function assistantControlValue(item = {}) {
const parameters = item.operation?.parameters || {};
const numericValue = (value) => (
value !== null && value !== undefined && value !== "" && Number.isFinite(Number(value))
? Number(value)
: null
);
const temperature = numericValue(parameters.temperature_c);
if (temperature !== null) return `${temperature}°C`;
const position = numericValue(parameters.position_pct ?? parameters.shade_position_pct);
if (position !== null) return `${position}%`;
const illumination = String(parameters.color_hex || "").trim();
const brightnessPercent = numericValue(parameters.brightness_pct);
if (illumination && brightnessPercent !== null) return `${illumination} · ${brightnessPercent}%`;
if (illumination) return illumination;
if (brightnessPercent !== null) return `${brightnessPercent}%`;
const brightness = numericValue(parameters.brightness_level);
if (brightness !== null) return `${brightness}级`;
const level = numericValue(parameters.level ?? parameters.fan_level);
if (level !== null) return `${level}档`;
const state = parameters.motion
?? parameters.state
?? parameters.power
?? parameters.action
?? parameters.mode;
if (state !== null && state !== undefined && String(state).trim()) {
return localizedAssistantControlValue(state);
}
const commandTail = String(item.command || "").trim().split(/\s+/).at(-1);
return localizedAssistantControlValue(commandTail) || String(item.command || "控制");
}
function assistantControlIcon(item = {}) {
const capability = String(item.operation?.capability || "");
const category = String(item.category || "");
if (capability.startsWith("climate.") || category.includes("空调")) return "thermometer";
if (capability.includes("reading") || category.includes("阅读灯")) return "book-open";
if (capability.includes("light") || category.includes("灯")) return "lightbulb";
if (capability.startsWith("roof.") || category.includes("遮阳帘")) return "panel-top";
if (capability.startsWith("window.") || category.includes("车窗")) return "panels-top-left";
if (capability.startsWith("seat.") || category.includes("座椅")) return "armchair";
if (capability.startsWith("door.") || category.includes("门锁")) return "lock-keyhole";
if (capability === "infrared.emit") return "radio-tower";
return "binary";
}
function appendDispatchedControls(message, result = {}) {
if (!message?.body || !result || typeof result !== "object") return;
const previous = message.dispatchedControlResult || {};
const mergedResult = { ...previous, ...result };
if (!Array.isArray(result.validated_control_commands)
&& Array.isArray(previous.validated_control_commands)) {
mergedResult.validated_control_commands = previous.validated_control_commands;
}
message.dispatchedControlResult = mergedResult;
const commands = assistantDispatchedCommands(mergedResult);
if (!commands.length) return;
const signature = JSON.stringify(commands.map((item) => [
item.operationRef,
item.category,
item.command,
item.semantic,
item.deliveryState,
item.deliveryLabel,
item.deliveryDetail,
]));
const vueRenderer = window.CognitiveAgentVueUI?.renderDispatchedControls;
if (typeof vueRenderer === "function") {
let host = $(".assistant-dispatched-controls-vue-host", message.body);
if (host?.dataset.commandSignature === signature) return;
if (!host) {
host = document.createElement("div");
host.className = "assistant-dispatched-controls-vue-host";
message.body.append(host);
}
const viewCommands = commands.map((item, index) => ({
key: item.operationRef || `${item.semantic || item.command}-${index}`,
category: String(item.category || "控制指令"),
value: assistantControlValue(item),
icon: assistantControlIcon(item),
semantic: String(item.semantic || ""),
command: String(item.command || ""),
deliveryState: String(item.deliveryState || "not_submitted"),
deliveryLabel: String(item.deliveryLabel || "未下发"),
deliveryDetail: String(item.deliveryDetail || ""),
}));
if (vueRenderer(host, viewCommands)) {
host.dataset.commandSignature = signature;
message.article?.classList.add("assistant-message-with-dispatched-controls");
followLatest(true);
return;
}
host.remove();
}
let card = $(".assistant-dispatched-controls", message.body);
if (card?.dataset.commandSignature === signature) return;
if (!card) {
card = document.createElement("details");
card.className = "assistant-dispatched-controls";
card.open = true;
const summary = document.createElement("summary");
const headingIcon = document.createElement("span");
headingIcon.className = "assistant-dispatched-controls-heading-icon";
headingIcon.innerHTML = '';
const heading = document.createElement("b");
heading.textContent = "下发控制指令";
const count = document.createElement("strong");
count.className = "assistant-dispatched-controls-count";
const chevron = document.createElement("i");
chevron.className = "assistant-dispatched-controls-chevron";
chevron.setAttribute("data-lucide", "chevron-down");
summary.append(headingIcon, heading, count, chevron);
const list = document.createElement("ol");
list.className = "assistant-dispatched-controls-list";
card.append(summary, list);
message.body.append(card);
}
card.dataset.commandSignature = signature;
const count = $(".assistant-dispatched-controls-count", card);
const list = $(".assistant-dispatched-controls-list", card);
if (count) count.textContent = String(commands.length);
if (!list) return;
list.replaceChildren();
commands.forEach((item) => {
const row = document.createElement("li");
row.dataset.deliveryState = String(item.deliveryState || "not_submitted");
const titleParts = [];
if (item.semantic) titleParts.push(`语义控制指令:${item.semantic}`);
if (item.command) titleParts.push(`实际控制指令:${item.command}`);
if (item.deliveryLabel) {
titleParts.push(`下发状态:${item.deliveryLabel}${item.deliveryDetail ? `(${item.deliveryDetail})` : ""}`);
}
row.title = titleParts.join("\n");
const icon = document.createElement("span");
icon.className = "assistant-dispatched-control-icon";
icon.innerHTML = ``;
const label = document.createElement("span");
label.className = "assistant-dispatched-control-label";
label.textContent = String(item.category || "控制指令");
const value = document.createElement("strong");
value.className = "assistant-dispatched-control-value";
value.textContent = assistantControlValue(item);
const accessibleCommand = document.createElement("small");
accessibleCommand.className = "visually-hidden";
accessibleCommand.textContent = titleParts.join(";");
row.append(icon, label, value, accessibleCommand);
list.append(row);
});
message.article?.classList.add("assistant-message-with-dispatched-controls");
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
followLatest(true);
}
function appendModeCreatedState(message, profile, { updated = false } = {}) {
if (!message?.body || !profile?.id) return;
const selector = `[data-assistant-mode-created="${CSS.escape(String(profile.id))}"]`;
if ($(selector, message.body)) return;
const card = document.createElement("div");
card.className = "assistant-mode-created";
card.dataset.assistantModeCreated = String(profile.id);
const icon = document.createElement("span");
icon.className = "assistant-mode-created-icon";
icon.innerHTML = '';
const copy = document.createElement("span");
copy.className = "assistant-mode-created-copy";
const label = document.createElement("small");
label.textContent = updated ? "已更新「我的模式」" : "已添加到「我的模式」";
const name = document.createElement("strong");
name.textContent = String(profile.name || "新功能模式");
copy.append(label, name);
const reveal = document.createElement("button");
reveal.type = "button";
reveal.textContent = "查看";
reveal.addEventListener("click", () => {
window.CognitiveAgentConsole?.revealModeProfile?.(String(profile.id), { focus: true });
});
card.append(icon, copy, reveal);
message.body.append(card);
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
followLatest(true);
}
function modeDraftRows(draft, rawActions) {
const instruction = String(draft?.instruction || "");
const actions = Array.isArray(rawActions)
? rawActions.filter((item) => item && typeof item === "object")
: [];
const consumed = new Set();
const rows = [];
const targetZone = (action) => String(
action?.target?.instance || action?.target?.zone || action?.target || "",
);
const parameters = (action) => (
action?.parameters && typeof action.parameters === "object" ? action.parameters : {}
);
const instructionOrder = (...terms) => {
const indexes = terms.map((term) => instruction.indexOf(term)).filter((index) => index >= 0);
return indexes.length ? Math.min(...indexes) : 10_000 + rows.length;
};
const addRow = (label, value, icon, order, indexes = []) => {
indexes.forEach((index) => consumed.add(index));
rows.push({ label, value, icon, order });
};
const zeroGravityIndexes = actions
.map((action, index) => ({ action, index }))
.filter(({ action }) => String(action.capability || "").startsWith("seat.set_")
&& ["passenger", "front_right"].includes(targetZone(action)))
.map(({ index }) => index);
if (/零重力/.test(instruction) && zeroGravityIndexes.length >= 2) {
addRow(
"副驾座椅",
"零重力模式",
"armchair",
instructionOrder("副驾", "零重力"),
zeroGravityIndexes,
);
}
const lightIndexes = actions
.map((action, index) => ({ action, index }))
.filter(({ action }) => ["interior_light", "ambient_light"].includes(String(action.domain || "")))
.map(({ index }) => index);
if (
lightIndexes.length >= 2
&& /(?:灯光.*(?:全部|所有|全车).*(?:关闭|关掉|熄灭)|(?:全部|所有|全车).*灯光.*(?:关闭|关掉|熄灭))/.test(instruction)
) {
addRow(
"座舱灯光",
"全部关闭",
"lightbulb-off",
instructionOrder("灯光"),
lightIndexes,
);
}
const motionLabel = (value) => ({
open: "打开",
close: "关闭",
stop: "停止",
half: "半开",
on: "开启",
off: "关闭",
}[String(value)] || String(value || "已配置"));
const shadePositionLabel = (value) => {
if (value == null || String(value).trim() === "") return "目标开度待确认";
const position = Number(value);
if (!Number.isFinite(position) || position < 0 || position > 100) {
return "目标开度待确认";
}
const target = `目标开度 ${position}%`;
if (position === 0) return `完全关闭(${target})`;
if (position === 50) return `半开(${target})`;
if (position === 100) return `完全打开(${target})`;
return target;
};
actions.forEach((action, index) => {
if (consumed.has(index)) return;
const capability = String(action.capability || "");
const domain = String(action.domain || "");
const zone = targetZone(action);
const params = parameters(action);
let label = capability || domain || "座舱设备";
let value = "已配置";
let icon = "sliders-horizontal";
let order = 10_000 + index;
if (capability === "roof.set_shade_motion" || capability === "roof.set_shade_position") {
label = "遮阳帘";
value = params.position_pct != null
? shadePositionLabel(params.position_pct)
: motionLabel(params.motion);
icon = "blinds";
order = instructionOrder("遮阳帘", "天幕帘");
} else if (domain === "window") {
const windowLabels = {
all: "全部车窗",
front: "前排车窗",
rear: "后排车窗",
front_left: "主驾车窗",
front_right: "副驾车窗",
rear_left: "左后车窗",
rear_right: "右后车窗",
};
label = windowLabels[zone] || "车窗";
value = params.position_pct != null ? `${params.position_pct}%` : motionLabel(params.motion);
icon = "panels-top-left";
order = instructionOrder("车窗");
} else if (domain === "seat") {
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
? `${params.position_pct}%${params.angle_deg != null ? `(${params.angle_deg}°)` : ""}`
: motionLabel(params.state || params.mode_id);
icon = "armchair";
order = instructionOrder("座椅", "副驾", "主驾");
} else if (capability === "climate.set_temperature") {
label = "空调温度";
value = `${params.temperature_c ?? "--"}℃`;
icon = "thermometer";
order = instructionOrder("空调", "温度");
} else if (domain === "climate") {
const cycleLabels = { internal: "内循环", external: "外循环", auto: "自动循环" };
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";
order = instructionOrder("空调", "风量");
} else if (capability === "interior_light.set_reading_power") {
label = "阅读灯";
value = motionLabel(params.state);
icon = "book-open";
order = instructionOrder("阅读灯", "灯光");
} else if (["interior_light", "ambient_light"].includes(domain)) {
label = domain === "ambient_light" ? "氛围灯" : "室内灯";
value = motionLabel(params.state);
icon = "lightbulb";
order = instructionOrder(label, "灯光");
}
addRow(label, value, icon, order, [index]);
});
return rows.sort((left, right) => left.order - right.order);
}
async function saveModeProfileDraft(message, card, draft, buttons, status) {
buttons.forEach((button) => { button.disabled = true; });
card.classList.add("is-busy");
status.hidden = false;
status.className = "assistant-mode-draft-status working";
const existingModeId = String(draft.existing_mode_id || "").trim();
status.textContent = existingModeId
? "正在将验证结果保存为模式更新…"
: "正在保存到「我的模式」…";
try {
const endpoint = existingModeId
? `/api/mode-profiles/${encodeURIComponent(existingModeId)}`
: "/api/mode-profiles";
const response = await window.CognitiveAgentNetwork.request(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: draft.name,
instruction: draft.instruction,
source_request_id: draft.source_request_id,
vehicle_id: draft.vehicle_id || $("#vehicleSelect")?.value || null,
}),
timeoutMs: 45 * 1000,
retries: 0,
});
const payload = await response.json();
if (response.status === 401) {
window.location.replace("/login");
return;
}
if (!response.ok) throw new Error(payload.message || payload.error || `HTTP ${response.status}`);
card.classList.remove("is-busy");
card.classList.add("is-saved");
status.className = "assistant-mode-draft-status success";
status.textContent = existingModeId ? "模式已更新" : "模式已保存";
card.querySelector(".assistant-mode-draft-actions")?.setAttribute("hidden", "");
clearActiveModeDraftContext(draft.source_request_id);
await window.CognitiveAgentConsole?.refreshModeProfiles?.(payload.mode_profile?.id);
appendModeCreatedState(message, payload.mode_profile, { updated: Boolean(existingModeId) });
persistMessages();
} catch (error) {
card.classList.remove("is-busy");
status.className = "assistant-mode-draft-status failed";
status.textContent = error.message || "保存失败,请重试。";
buttons.forEach((button) => { button.disabled = false; });
}
}
async function tryModeProfileDraft(card, draft, actions, buttons, status) {
buttons.forEach((button) => { button.disabled = true; });
card.classList.add("is-busy");
status.hidden = false;
status.className = "assistant-mode-draft-status working";
status.textContent = "正在进行模拟试运行…";
try {
const result = await window.CognitiveAgentConsole?.tryModeDraft?.({ ...draft, actions });
if (!result || result.status !== "succeeded") throw new Error("试运行未通过,请检查动作设置。");
status.className = "assistant-mode-draft-status success";
status.textContent = "试运行通过 · 尚未保存";
} catch (error) {
status.className = "assistant-mode-draft-status failed";
status.textContent = error.message || "试运行失败,请重试。";
} finally {
card.classList.remove("is-busy");
buttons.forEach((button) => { button.disabled = false; });
}
}
function supersedeModeDraftCards(draft) {
const nextName = normalizeModeName(draft?.name);
const nextRequestId = String(draft?.source_request_id || "");
if (!nextName || !nextRequestId) return;
document.querySelectorAll("[data-assistant-mode-draft]").forEach((card) => {
if (
card.dataset.assistantModeName !== nextName
|| card.dataset.assistantModeDraft === nextRequestId
) return;
card.classList.remove("is-busy");
card.classList.add("is-superseded");
card.querySelectorAll("button").forEach((button) => {
button.disabled = true;
});
card.querySelector(".assistant-mode-draft-actions")?.setAttribute("hidden", "");
const priorStatus = card.querySelector(".assistant-mode-draft-status");
if (priorStatus) {
priorStatus.hidden = false;
priorStatus.className = "assistant-mode-draft-status superseded";
priorStatus.textContent = "已由下方最新草稿替换";
}
});
}
function appendModeDraftState(message, draft, actions) {
if (!message?.body || !draft?.name || !draft?.source_request_id) return;
rememberModeDraftContext(draft);
supersedeModeDraftCards(draft);
const selector = `[data-assistant-mode-draft="${CSS.escape(String(draft.source_request_id))}"]`;
if ($(selector, message.body)) return;
const card = document.createElement("section");
card.className = "assistant-mode-draft";
card.dataset.assistantModeDraft = String(draft.source_request_id);
card.dataset.assistantModeName = normalizeModeName(draft.name);
message.article?.classList.add("assistant-message-mode-draft");
const heading = document.createElement("header");
const headingCopy = document.createElement("span");
const eyebrow = document.createElement("small");
eyebrow.textContent = draft.existing_mode_id
? "已载入现有功能模式"
: "已为你生成功能模式";
const title = document.createElement("strong");
title.textContent = `「${draft.name}」`;
headingCopy.append(eyebrow, title);
const badge = document.createElement("span");
badge.className = "assistant-mode-draft-badge";
badge.textContent = `${Number(draft.operation_count) || 0} 项动作`;
heading.append(headingCopy, badge);
const list = document.createElement("div");
list.className = "assistant-mode-draft-list";
modeDraftRows(draft, actions).forEach((item) => {
const row = document.createElement("div");
row.className = "assistant-mode-draft-row";
const icon = document.createElement("i");
icon.dataset.lucide = item.icon;
const label = document.createElement("span");
label.textContent = item.label;
const value = document.createElement("strong");
value.textContent = item.value;
row.append(icon, label, value);
list.append(row);
});
const controls = document.createElement("div");
controls.className = "assistant-mode-draft-actions";
const save = document.createElement("button");
save.type = "button";
save.className = "assistant-mode-draft-save";
save.innerHTML = draft.existing_mode_id
? '确认更新此模式'
: '保存为功能模式';
const trial = document.createElement("button");
trial.type = "button";
trial.className = "assistant-mode-draft-trial";
trial.textContent = "先试运行";
controls.append(save, trial);
const status = document.createElement("p");
status.className = "assistant-mode-draft-status";
status.hidden = true;
status.setAttribute("role", "status");
const buttons = [save, trial];
save.addEventListener("click", () => {
void saveModeProfileDraft(message, card, draft, buttons, status);
});
trial.addEventListener("click", () => {
void tryModeProfileDraft(card, draft, actions, buttons, status);
});
card.append(heading, list, controls, status);
message.body.append(card);
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
followLatest(true);
}
function startModeCreation() {
setDrawerOpen(true);
const input = $("#utterance");
if (!input) return;
const helper = createMessage(
"assistant",
"告诉我模式名称和希望联动的座舱状态。比如:创建宝宝睡眠模式,副驾半躺、空调 24℃、关闭阅读灯。我会先生成动作预览,确认后再加入「我的模式」。",
);
helper.article.classList.add("assistant-mode-guide");
input.value = "请帮我创建一个";
input.dispatchEvent(new Event("input", { bubbles: true }));
input.focus({ preventScroll: true });
input.setSelectionRange(input.value.length, input.value.length);
}
function setComposerPending(pending) {
const button = $("#runButton");
const input = $("#utterance");
if (!button || !input) return;
document.body.classList.toggle("assistant-drawer-busy", pending);
button.classList.toggle("assistant-stop", pending);
button.setAttribute("aria-label", pending ? "停止生成" : "发送消息");
button.title = pending ? "停止生成" : "发送消息";
// General questions must stay available even when no physical CAN adapter
// is connected; hardware eligibility is evaluated only for control routes.
button.disabled = false;
button.innerHTML = pending
? '停止'
: '发送';
input.setAttribute("aria-busy", String(pending));
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}
function requestBody(message) {
const executionMode = window.CognitiveAgentConsole?.getExecutionMode?.() || "dry_run";
const controlTransport = localStorage.getItem("cognitive-agent.control-transport") || "wired_can";
const hardwareClientId = localStorage.getItem("cognitive-agent.hardware-client-id");
const vehicleId = $("#vehicleSelect")?.value || null;
const modeProfileDraftContext = requestModeDraftContext(message);
return {
message,
conversation_id: conversationId(),
client_type: "web_console_codex_assistant",
execution_mode: executionMode,
control_transport: controlTransport,
...(vehicleId ? { vehicle_id: vehicleId } : {}),
...(modeProfileDraftContext
? { mode_profile_draft_context: modeProfileDraftContext }
: {}),
...(executionMode === "hardware" && controlTransport !== "wireless_mqtt" && hardwareClientId
? { hardware_client_id: hardwareClientId }
: {}),
...(executionMode === "simulated_hardware"
? { simulation_fault: $("#simulationFault")?.value || "normal" }
: {}),
};
}
async function readErrorResponse(response) {
try {
const payload = await response.json();
return payload.message || payload.error || `HTTP ${response.status}`;
} catch (_error) {
return `HTTP ${response.status}`;
}
}
function applyAssistantEvent(event, message) {
if (event.type === "validated_control_commands" && event.payload) {
window.dispatchEvent(new CustomEvent(
"cognitive-agent:validated-can-commands",
{ detail: event.payload },
));
appendDispatchedControls(message, event.payload);
return;
}
if (event.type === "answer.delta") {
setMessageContent(message, `${message.content}${event.delta || ""}`);
return;
}
if (["action.started", "action.progress", "work.started", "work.progress"].includes(event.type)) {
const defaultText = event.type.startsWith("work.")
? "AI助手正在处理工作……"
: "正在执行座舱控制……";
setActionStatus(message, event.display_text || defaultText, "working");
if (event.type === "action.progress" && event.payload) {
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-control-activity", {
detail: event.payload,
}));
}
return;
}
if (event.type === "action.completed" || event.type === "work.completed") {
latestControlResult = event.payload || latestControlResult;
const completed = event.success !== false;
const defaultText = event.type === "work.completed"
? (completed ? "AI助手已完成工作" : "AI助手未能完成工作")
: (completed ? "座舱控制已完成" : "座舱控制未完成");
setActionStatus(
message,
event.display_text || defaultText,
completed ? "success" : "failed",
);
if (event.payload) {
window.CognitiveAgentConsole?.applySyncedResult?.(event.payload);
appendDispatchedControls(message, event.payload);
if (
event.payload.mode_profile_draft
&& event.payload.orchestration_route === "mode_profile_create_preview"
) {
appendModeDraftState(
message,
event.payload.mode_profile_draft,
event.payload.actions,
);
} else if (
event.payload.mode_profile
&& ["mode_profile_create", "mode_profile_update"].includes(
event.payload.orchestration_route,
)
) {
if (
event.payload.orchestration_route === "mode_profile_create"
|| normalizeModeName(activeModeDraftContext?.name)
=== normalizeModeName(event.payload.mode_profile.name)
) {
clearActiveModeDraftContext();
}
void window.CognitiveAgentConsole?.refreshModeProfiles?.(
event.payload.mode_profile.id,
);
appendModeCreatedState(message, event.payload.mode_profile, {
updated: event.payload.orchestration_route === "mode_profile_update",
});
} else if (
event.payload.deleted_mode_profile
&& event.payload.orchestration_route === "mode_profile_delete"
) {
if (
normalizeModeName(activeModeDraftContext?.name)
=== normalizeModeName(event.payload.deleted_mode_profile.name)
) {
clearActiveModeDraftContext();
}
void window.CognitiveAgentConsole?.refreshModeProfiles?.();
}
appendControlSuggestions(
message,
event.payload.suggestions || event.payload.recommended_commands,
);
}
return;
}
if (event.type === "control.suggestions") {
appendControlSuggestions(message, event.suggestions || event.recommended_commands);
return;
}
if (event.type === "answer.completed") {
const authoritativeAnswer = Object.prototype.hasOwnProperty.call(event, "answer")
? String(event.answer || "")
: message.content;
setMessageContent(message, authoritativeAnswer, { final: true });
appendControlSuggestions(message, event.suggestions || event.recommended_commands);
return;
}
if (event.type === "error") {
const error = new Error(event.message || "请求处理失败");
error.code = event.error;
throw error;
}
}
async function streamAssistant(messageText, assistantMessage, signal) {
const response = await window.CognitiveAgentNetwork.request("/api/assistant/stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/x-ndjson",
},
body: JSON.stringify(requestBody(messageText)),
timeoutMs: 15 * 60 * 1000,
retries: 0,
signal,
});
if (!response.ok) throw new Error(await readErrorResponse(response));
if (!response.body?.getReader) throw new Error("当前浏览器不支持流式问答");
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffered = "";
let answerCompleted = false;
const abortError = () => {
const error = new Error("助手响应已取消");
error.name = "AbortError";
return error;
};
const cancelReader = () => {
// CognitiveAgentNetwork resolves once response headers arrive. Cancelling
// the body reader keeps the Stop button effective for the remaining
// long-lived NDJSON stream as well.
reader.cancel(signal.reason).catch(() => {});
};
signal.addEventListener("abort", cancelReader, { once: true });
try {
if (signal.aborted) {
await reader.cancel(signal.reason).catch(() => {});
throw abortError();
}
while (true) {
const { done, value } = await reader.read();
if (signal.aborted) throw abortError();
buffered += decoder.decode(value || new Uint8Array(), { stream: !done });
const lines = buffered.split("\n");
buffered = done ? "" : lines.pop() || "";
for (const line of lines) {
if (!line.trim()) continue;
const event = JSON.parse(line);
applyAssistantEvent(event, assistantMessage);
if (event.type === "answer.completed") answerCompleted = true;
}
if (done) break;
}
if (buffered.trim()) {
const event = JSON.parse(buffered);
applyAssistantEvent(event, assistantMessage);
if (event.type === "answer.completed") answerCompleted = true;
}
if (!answerCompleted) throw new Error("助手响应意外中断,请重试。");
} finally {
signal.removeEventListener("abort", cancelReader);
reader.releaseLock();
}
}
async function submitAssistant(messageOverride = null) {
if (pendingController) {
if (typeof messageOverride === "string") return;
pendingController.abort("user_stop");
return;
}
const input = $("#utterance");
const speechPhase = window.CognitiveAgentSpeech?.phase;
const speechInputMode = window.CognitiveAgentSpeech?.inputMode;
if (["capturing", "finalizing"].includes(speechPhase)
|| (speechPhase === "arming" && speechInputMode === "manual")) {
window.CognitiveAgentConsole?.notify?.("请等待语音整理完成后再发送");
return;
}
const suggestedMessage = typeof messageOverride === "string" ? messageOverride.trim() : "";
const messageText = suggestedMessage || input?.value.trim() || "";
if (!messageText) {
window.CognitiveAgentConsole?.notify?.("请输入问题或工作需求");
return;
}
window.dispatchEvent(new CustomEvent(
"cognitive-agent:control-request-start",
{
detail: {
utterance: messageText,
speech_utterance_id: suggestedMessage
? ""
: String(input?.dataset.speechUtteranceId || ""),
speech_value: suggestedMessage
? ""
: String(input?.dataset.speechValue || ""),
},
},
));
createMessage("user", messageText);
if (!suggestedMessage) {
input.value = "";
delete input.dataset.speechStage;
delete input.dataset.speechUtteranceId;
delete input.dataset.speechValue;
input.dispatchEvent(new Event("input", { bubbles: true }));
}
const assistantMessage = createMessage("assistant", "", { pending: true });
pendingMessage = assistantMessage;
latestControlResult = null;
const requestController = new AbortController();
pendingController = requestController;
setComposerPending(true);
let completedError = null;
let resetForNewConversation = false;
try {
await streamAssistant(messageText, assistantMessage, requestController.signal);
setMessageContent(assistantMessage, assistantMessage.content, { final: true });
persistMessages();
} catch (error) {
completedError = error;
resetForNewConversation = requestController.signal.aborted
&& requestController.signal.reason === "new_conversation";
if (resetForNewConversation) return;
if (requestController.signal.aborted) {
const stopped = assistantMessage.content
? `${assistantMessage.content}\n\n已停止继续生成。已下发的车辆操作仍会由安全执行链路完成。`
: "已停止生成。已下发的车辆操作仍会由安全执行链路完成。";
setMessageContent(assistantMessage, stopped, { final: true });
setActionStatus(assistantMessage, "已停止显示", "failed");
} else {
setMessageContent(assistantMessage, error.message || "请求处理失败,请稍后再试。", {
final: true,
});
setActionStatus(assistantMessage, "本次请求未完成", "failed");
}
persistMessages();
} finally {
if (pendingController === requestController) {
pendingController = null;
pendingMessage = null;
setComposerPending(false);
}
if (!resetForNewConversation) {
input.focus({ preventScroll: true });
window.dispatchEvent(new CustomEvent("cognitive-agent:request-complete", {
detail: {
utterance: messageText,
result: latestControlResult,
error: completedError,
},
}));
}
}
}
function announceModeExecution(profile, result = null, { command = "", error = null } = {}) {
const modeName = String(profile?.name || profile?.id || "功能模式");
const commandText = String(command || `进入${modeName}`).trim();
const status = String(result?.status || "");
const dryRun = result?.execution_mode === "dry_run";
const waitingForFeedback = result?.execution_outcome === "submitted_unverified";
setDrawerOpen(true);
createMessage("user", commandText);
let content = `${modeName}执行未完成,请查看控制指令状态。`;
let statusText = "功能模式执行未完成";
let statusState = "failed";
if (error) {
content = `${modeName}执行失败:${error.message || "请稍后重试。"}`;
statusText = "功能模式执行失败";
} else if (status === "succeeded") {
content = dryRun
? `已完成模拟编排:${commandText}。`
: waitingForFeedback
? `已提交执行:${commandText},正在等待车辆状态确认。`
: `已完成执行:${commandText}。`;
statusText = dryRun
? "功能模式模拟编排已完成"
: waitingForFeedback
? "模式指令已提交,等待车辆状态确认"
: "功能模式执行已完成";
statusState = "success";
} else if (status === "partially_succeeded") {
content = `${modeName}仅部分完成,请查看未完成的控制指令。`;
statusText = "功能模式部分完成";
}
const message = createMessage("assistant", content);
setActionStatus(message, statusText, statusState);
if (result) appendDispatchedControls(message, result);
persistMessages();
}
function renderInitialMessages() {
const container = $("#assistantMessages");
if (!container) return;
const stored = loadStoredMessages();
container.replaceChildren();
createMessage("assistant", WELCOME_TEXT);
stored.forEach((message) => createMessage(message.role, String(message.content || "")));
}
function resetConversation() {
const activeController = pendingController;
pendingController = null;
pendingMessage = null;
latestControlResult = null;
if (activeController) activeController.abort("new_conversation");
sessionStorage.removeItem(STORAGE_KEY);
clearActiveModeDraftContext();
const nextConversationId = createConversationId();
sessionStorage.setItem(CONVERSATION_STORAGE_KEY, nextConversationId);
const input = $("#utterance");
if (input) {
input.value = "";
delete input.dataset.speechStage;
delete input.dataset.speechUtteranceId;
delete input.dataset.speechValue;
input.dispatchEvent(new Event("input", { bubbles: true }));
resizeComposerInput(input);
}
renderInitialMessages();
setComposerPending(false);
setDrawerOpen(true);
requestAnimationFrame(() => input?.focus({ preventScroll: true }));
window.dispatchEvent(new CustomEvent("cognitive-agent:new-conversation", {
detail: { conversationId: nextConversationId },
}));
return nextConversationId;
}
function initialize() {
if (initialized || !$("#requestForm") || !$("#assistantMessages")) return;
initialized = true;
document.body.classList.add("assistant-chat-enabled");
const title = $(".v2-operation-heading h2");
if (title) title.textContent = "AI助手";
const input = $("#utterance");
input.value = "";
input.placeholder = "向 AI助手提问或交代工作";
input.setAttribute("aria-label", "向 AI助手提问或交代工作");
installAssistantDrawer();
input.addEventListener("input", () => resizeComposerInput(input));
resizeComposerInput(input);
// Some browsers restore the last submitted textarea value after
// DOMContentLoaded. Clear that restoration once the initial page settles so
// a reload cannot accidentally resend an already handled vehicle command.
window.setTimeout(() => {
if (pendingController) return;
input.value = "";
resizeComposerInput(input);
}, 300);
renderInitialMessages();
activeModeDraftContext = loadActiveModeDraftContext();
setComposerPending(false);
}
document.addEventListener("submit", (event) => {
if (!(event.target instanceof HTMLFormElement) || event.target.id !== "requestForm") return;
event.preventDefault();
event.stopImmediatePropagation();
submitAssistant();
}, true);
document.addEventListener("click", (event) => {
const target = event.target instanceof Element ? event.target : null;
if (!target?.closest("#newConversation")) return;
event.preventDefault?.();
resetConversation();
}, true);
document.addEventListener("DOMContentLoaded", initialize);
window.CognitiveAgentAssistant = {
submit: submitAssistant,
open: () => setDrawerOpen(true),
close: () => setDrawerOpen(false),
resetConversation,
startModeCreation,
announceModeExecution,
announceModeCreated(profile) {
if (!profile?.id) return;
setDrawerOpen(true);
const message = createMessage("assistant", `已完成「${profile.name || "新功能模式"}」的配置。`);
appendModeCreatedState(message, profile);
persistMessages();
},
stop() {
pendingController?.abort("user_stop");
},
isPending: () => Boolean(pendingMessage),
};
if (window.__N7WEB_ASSISTANT_TEST__ === true) {
window.CognitiveAgentAssistant.__test = { setDrawerOpen };
}
}());