c812a1a595
Integrate the remote firmware workbench, OTA delivery, and timed mode schedules while preserving the local N7 scene and runtime changes.
1970 lines
83 KiB
JavaScript
1970 lines
83 KiB
JavaScript
(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 activeSchedulePoll = 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 = '<i data-lucide="bot-message-square"></i><span class="visually-hidden">AI助手</span><b aria-hidden="true"></b>';
|
||
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 addPredicate = "(?:加入|添加|增加|补充|加上|追加|新增)";
|
||
const changePredicate = `(?:${addPredicate}|修改|改成|改为|更改|更新|变更|变为|变成|换成|调整|调节|调高|调低|调到|调成|调为|调至|设置|设为|设成|设到|设至|打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|复位|半开|开到|关到|拉开|拉上|上锁|解锁)`;
|
||
const modeName = "([^,。!?!?、;;::]{1,64}?模式)";
|
||
const modePossessiveBoundary = "(?:中的|里的|内的|之中的|的|中|里|内)";
|
||
const modeMemberBoundary = `(?:${modePossessiveBoundary}|[,,::])`;
|
||
const question = /为什么|什么|怎么|如何|介绍|解释|作用|区别|原理|哪些|多少|几(?:度|档|级|个|项)?|是否|能否|可不可以|合适吗|合理吗|会怎样|列出|告诉我|[??]|(?:吗|么|呢)[。.!!]?$/;
|
||
const declinedAddition = new RegExp(`^(?:先|暂时)?(?:不要|别|不用|不必|无需|取消|撤销)(?:再|继续)?${addPredicate}`);
|
||
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,
|
||
},
|
||
{
|
||
pattern: new RegExp(`^${politePrefix}(?:在|给|为|往|向)${modeName}(?:${modePossessiveBoundary})?[,,::]?(.+)$`),
|
||
predicateRequired: true,
|
||
},
|
||
{
|
||
pattern: new RegExp(`^${politePrefix}${modeName}((?:再|也|还)?${addPredicate}.+)$`),
|
||
predicateRequired: true,
|
||
},
|
||
{
|
||
pattern: new RegExp(`^${politePrefix}(?:把|将)(.+?)${addPredicate}(?:到|进)?${modeName}(?:中|里|内)?[。.!!]?$`),
|
||
predicateRequired: false,
|
||
nameIndex: 2,
|
||
changeIndex: 1,
|
||
},
|
||
];
|
||
return candidates.some(({ pattern, predicateRequired, nameIndex = 1, changeIndex = 2 }) => {
|
||
const matched = compact.match(pattern);
|
||
const change = String(matched?.[changeIndex] || "");
|
||
return Boolean(
|
||
matched
|
||
&& normalizeModeName(matched[nameIndex]) === normalizeModeName(context.name)
|
||
&& !question.test(change)
|
||
&& !declinedAddition.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 = '<i data-lucide="waypoints"></i>';
|
||
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 = '<span>正在理解你的需求</span><i></i><i></i><i></i>';
|
||
} 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 = `<i data-lucide="${controlSuggestionIcon(suggestion.domain)}"></i>`;
|
||
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("_", "-");
|
||
const seat = ["passenger", "front-right"].includes(target) ? "副驾座椅" : "主驾座椅";
|
||
const component = {
|
||
"seat.set_longitudinal_position": "前后位置",
|
||
"seat.set_cushion_tilt": "坐盆",
|
||
"seat.set_backrest_recline": "靠背",
|
||
"seat.set_legrest_height": "腿托",
|
||
"seat.set_legrest_extension": "腿托",
|
||
}[capability];
|
||
if (component) return `${seat}${component}`;
|
||
if (capability === "seat.set_driver_axis_native") {
|
||
const axis = String(operation.parameters?.axis || "").replaceAll("_", "-");
|
||
const axisComponent = {
|
||
horizontal: "前后位置",
|
||
height: "坐盆",
|
||
basin: "坐盆",
|
||
backrest: "靠背",
|
||
legrest: "腿托",
|
||
}[axis];
|
||
if (axisComponent) return `${seat}${axisComponent}`;
|
||
}
|
||
return seat;
|
||
}
|
||
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 || {},
|
||
]));
|
||
const operationByRef = new Map();
|
||
const configurations = result.function_configs && typeof result.function_configs === "object"
|
||
? Object.values(result.function_configs)
|
||
: [];
|
||
if (result.function_config && typeof result.function_config === "object") {
|
||
configurations.push(result.function_config);
|
||
}
|
||
configurations.forEach((configuration) => {
|
||
const operations = Array.isArray(configuration?.spec?.operations)
|
||
? configuration.spec.operations
|
||
: [];
|
||
operations.forEach((operation) => {
|
||
const operationRef = String(operation?.id || "");
|
||
if (operationRef && !operationByRef.has(operationRef)) {
|
||
operationByRef.set(operationRef, 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 || ""))
|
||
|| operationByRef.get(String(item.operationRef || ""))
|
||
|| projections[index]?.operation
|
||
|| {};
|
||
const pendingDelivery = result.status === "processing"
|
||
? {
|
||
deliveryState: "submitting",
|
||
deliveryLabel: "正在下发",
|
||
deliveryDetail: "",
|
||
}
|
||
: {};
|
||
return {
|
||
...item,
|
||
...pendingDelivery,
|
||
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 focusLatestDispatchedControls(container) {
|
||
requestAnimationFrame(() => {
|
||
const transcript = $("#assistantMessages");
|
||
const activeCard = container?.matches?.(".assistant-dispatched-controls")
|
||
? container
|
||
: $(".assistant-dispatched-controls", container);
|
||
if (!transcript || !activeCard) return;
|
||
transcript.querySelectorAll(".assistant-dispatched-controls").forEach((card) => {
|
||
card.open = card === activeCard;
|
||
});
|
||
});
|
||
}
|
||
|
||
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");
|
||
focusLatestDispatchedControls(host);
|
||
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;
|
||
card.addEventListener("toggle", () => {
|
||
if (!card.open) return;
|
||
const transcript = $("#assistantMessages");
|
||
transcript?.querySelectorAll(".assistant-dispatched-controls[open]").forEach((otherCard) => {
|
||
if (otherCard !== card) otherCard.open = false;
|
||
});
|
||
});
|
||
const summary = document.createElement("summary");
|
||
const headingIcon = document.createElement("span");
|
||
headingIcon.className = "assistant-dispatched-controls-heading-icon";
|
||
headingIcon.innerHTML = '<i data-lucide="list-tree"></i>';
|
||
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 = `<i data-lucide="${assistantControlIcon(item)}"></i>`;
|
||
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");
|
||
focusLatestDispatchedControls(card);
|
||
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 = '<i data-lucide="circle-check"></i>';
|
||
|
||
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_scope || 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 zoneLabel = {
|
||
driver: "主驾", passenger: "副驾", front_left: "主驾", front_right: "副驾",
|
||
front_row: "前排", rear_row: "后排", rear_left: "左后", rear_right: "右后",
|
||
}[zone] || "";
|
||
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": "腿托高度",
|
||
"seat.set_ventilation": "座椅通风",
|
||
"seat.set_heating": "座椅加热",
|
||
"seat.set_driver_ventilation_native": "座椅通风",
|
||
"seat.set_driver_heating_native": "座椅加热",
|
||
"seat.set_driver_massage_native": "座椅按摩",
|
||
};
|
||
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);
|
||
if (["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"].includes(capability)) {
|
||
// CH1 driver controls encode high as 1 and low as 2; passenger
|
||
// comfort levels use a different scale and must not reuse this map.
|
||
const level = { 1: "高档", 2: "低档" }[params.level ?? 2] || "档位待确认";
|
||
value = params.state === "off" ? "关闭"
|
||
: params.state === "on" ? `开启 · ${level}` : "开关状态待确认";
|
||
} else if (["seat.set_ventilation", "seat.set_heating"].includes(capability)) {
|
||
value = params.state === "off" ? "关闭"
|
||
: params.state === "on"
|
||
? `开启${params.level != null ? ` · ${params.level}档` : ""}`
|
||
: "开关状态待确认";
|
||
} else if (capability === "seat.set_driver_massage_native") {
|
||
const massageModes = {
|
||
wave: "波浪", pulse: "脉冲", stretch: "舒展", slack: "舒缓", relax: "放松",
|
||
};
|
||
const massageStrengths = { 1: "轻柔", 2: "中等", 3: "较强" };
|
||
value = params.action === "off" ? "关闭"
|
||
: params.action === "mode"
|
||
? (massageModes[params.mode] ? `${massageModes[params.mode]}模式` : "按摩模式待确认")
|
||
: params.action === "strength"
|
||
? (massageStrengths[params.strength] ? `${massageStrengths[params.strength]}力度` : "按摩力度待确认")
|
||
: "按摩动作待确认";
|
||
}
|
||
icon = "armchair";
|
||
order = instructionOrder("座椅", "副驾", "主驾");
|
||
} else if (capability === "climate.set_temperature") {
|
||
label = `${zoneLabel}空调温度`;
|
||
value = `${params.temperature_c ?? "--"}℃`;
|
||
icon = "thermometer";
|
||
order = instructionOrder("空调", "温度");
|
||
} else if (domain === "climate") {
|
||
const climateModeLabels = {
|
||
"climate.set_cycle_mode": { internal: "内循环", external: "外循环" },
|
||
"climate.set_control_mode": { manual: "手动控制", auto: "自动控制" },
|
||
"climate.set_outlet_mode": {
|
||
face: "吹面", face_feet: "吹面 / 吹脚", feet: "吹脚",
|
||
feet_defrost: "吹脚 / 除霜", defrost: "除霜",
|
||
face_feet_defrost: "吹面 / 吹脚 / 除霜", face_defrost: "吹面 / 除霜",
|
||
},
|
||
};
|
||
label = capability === "climate.set_fan_level" ? "空调风量"
|
||
: capability === "climate.set_cycle_mode" ? "空调循环"
|
||
: capability === "climate.set_control_mode" ? "空调控制模式"
|
||
: capability === "climate.set_outlet_mode" ? "空调出风模式" : "空调";
|
||
label = `${zoneLabel}${label}`;
|
||
value = params.level != null ? `${params.level}档`
|
||
: params.mode != null ? (climateModeLabels[capability]?.[String(params.mode)] || String(params.mode))
|
||
: motionLabel(params.state);
|
||
icon = "fan";
|
||
order = instructionOrder("空调", "风量");
|
||
} else if (capability === "interior_light.set_reading_power") {
|
||
label = `${zoneLabel}阅读灯`;
|
||
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);
|
||
if (capability === "ambient_light.set_brightness_level") {
|
||
label = "氛围灯亮度";
|
||
value = params.brightness_level != null ? `${params.brightness_level}档` : "亮度档位待确认";
|
||
} else if (capability === "ambient_light.set_color_index") {
|
||
label = "氛围灯颜色";
|
||
value = params.color_index != null ? `色号 ${params.color_index}` : "颜色待确认";
|
||
} else if (capability === "ambient_light.set_illumination") {
|
||
value = [
|
||
params.color_hex != null ? `颜色 ${params.color_hex}` : "颜色待确认",
|
||
params.brightness_pct != null ? `亮度 ${params.brightness_pct}%` : "亮度待确认",
|
||
].join(" · ");
|
||
}
|
||
icon = "lightbulb";
|
||
order = instructionOrder(label, "灯光");
|
||
}
|
||
addRow(label, value, icon, order, [index]);
|
||
});
|
||
return rows.sort((left, right) => left.order - right.order);
|
||
}
|
||
|
||
function modeDraftScheduleView(draft) {
|
||
const schedule = draft?.schedule;
|
||
if (
|
||
schedule?.schema !== "cognitive-agent-mode-schedule/v1"
|
||
|| !Array.isArray(schedule.phases) || !schedule.phases.length
|
||
|| !Number.isInteger(schedule.repeat_count)
|
||
|| schedule.repeat_count < 1 || schedule.repeat_count > 20
|
||
|| schedule.phases.some((phase) => !Number.isFinite(phase?.duration_ms)
|
||
|| phase.duration_ms <= 0 || !Array.isArray(phase.operations))
|
||
) return null;
|
||
const durationLabel = (durationMs) => {
|
||
const minutes = Math.floor(durationMs / 60000);
|
||
const seconds = Number(((durationMs % 60000) / 1000).toFixed(3));
|
||
return minutes ? `${minutes}分${seconds ? `${seconds}秒` : ""}` : `${seconds}秒`;
|
||
};
|
||
let offsetMs = 0;
|
||
const phases = schedule.phases.map((phase, index) => {
|
||
// Each phase is rendered separately, without instruction-based grouping:
|
||
// the same seat or temperature can have a different target later on.
|
||
const rows = modeDraftRows({ instruction: "" }, phase.operations.map((operation) => ({
|
||
...operation,
|
||
target: operation.target || (operation.domain === "seat"
|
||
? { instance: operation.target_scope }
|
||
: { zone: operation.target_scope }),
|
||
})));
|
||
const view = {
|
||
id: String(phase.id || `phase-${index + 1}`),
|
||
name: String(phase.name || `阶段 ${index + 1}`),
|
||
timing: `+${durationLabel(offsetMs)} 开始 · 持续 ${durationLabel(phase.duration_ms)}`,
|
||
rows,
|
||
};
|
||
offsetMs += phase.duration_ms;
|
||
return view;
|
||
});
|
||
return {
|
||
phases,
|
||
operationCount: schedule.phases.reduce((count, phase) => count + phase.operations.length, 0),
|
||
summary: `${phases.length} 个阶段 · 重复 ${schedule.repeat_count} 次 · 总时长 ${durationLabel(offsetMs * schedule.repeat_count)}`,
|
||
stopHint: "执行后可输入“停止动态模式”提前结束,已执行状态不自动回滚。各阶段按顺序执行,时间为每轮的相对时间。",
|
||
};
|
||
}
|
||
|
||
function appendModeDraftRows(container, rows) {
|
||
rows.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);
|
||
container.append(row);
|
||
});
|
||
}
|
||
|
||
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";
|
||
const scheduleView = modeDraftScheduleView(draft);
|
||
badge.textContent = `${scheduleView?.operationCount ?? (Number(draft.operation_count) || 0)} 项动作`;
|
||
heading.append(headingCopy, badge);
|
||
|
||
const list = document.createElement("div");
|
||
list.className = "assistant-mode-draft-list";
|
||
if (scheduleView) {
|
||
card.classList.add("assistant-mode-draft-scheduled");
|
||
const summary = document.createElement("p");
|
||
summary.className = "assistant-mode-schedule-summary";
|
||
summary.textContent = scheduleView.summary;
|
||
list.append(summary);
|
||
scheduleView.phases.forEach((phase, index) => {
|
||
const section = document.createElement("section");
|
||
section.className = "assistant-mode-schedule-phase";
|
||
section.dataset.modeSchedulePhase = phase.id;
|
||
const phaseHeading = document.createElement("header");
|
||
const name = document.createElement("strong");
|
||
name.textContent = `${index + 1}. ${phase.name}`;
|
||
const timing = document.createElement("small");
|
||
timing.textContent = phase.timing;
|
||
phaseHeading.append(name, timing);
|
||
section.append(phaseHeading);
|
||
appendModeDraftRows(section, phase.rows);
|
||
list.append(section);
|
||
});
|
||
const hint = document.createElement("p");
|
||
hint.className = "assistant-mode-schedule-hint";
|
||
hint.textContent = scheduleView.stopHint;
|
||
list.append(hint);
|
||
} else {
|
||
appendModeDraftRows(list, modeDraftRows(draft, actions));
|
||
}
|
||
|
||
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
|
||
? '<i data-lucide="refresh-cw"></i><span>确认更新此模式</span>'
|
||
: '<i data-lucide="bookmark-plus"></i><span>保存为功能模式</span>';
|
||
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
|
||
? '<i data-lucide="square"></i><span>停止</span>'
|
||
: '<i data-lucide="send-horizontal"></i><span>发送</span>';
|
||
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 stopScheduleStatusPoll() {
|
||
if (!activeSchedulePoll) return;
|
||
window.clearTimeout(activeSchedulePoll.timer);
|
||
activeSchedulePoll.controller?.abort();
|
||
activeSchedulePoll = null;
|
||
}
|
||
|
||
function startScheduleStatusPoll(payload, message) {
|
||
stopScheduleStatusPoll();
|
||
const vehicleId = String(payload?.vehicle_id || payload?.vehicle?.id
|
||
|| payload?.mode_profile?.vehicle_id || $("#vehicleSelect")?.value || "");
|
||
if (!vehicleId) return;
|
||
const poll = {
|
||
vehicleId, conversationId: conversationId(), message,
|
||
sessionId: String(payload?.schedule_execution?.session_id || ""),
|
||
runId: String(payload?.schedule_execution?.run_id || ""),
|
||
modeProfileId: String(payload?.mode_profile?.id || ""),
|
||
timer: null, controller: null,
|
||
};
|
||
activeSchedulePoll = poll;
|
||
const isCurrent = () => activeSchedulePoll === poll
|
||
&& conversationId() === poll.conversationId
|
||
&& (!$("#vehicleSelect")?.value || $("#vehicleSelect").value === poll.vehicleId);
|
||
const update = async () => {
|
||
if (!isCurrent()) {
|
||
if (activeSchedulePoll === poll) stopScheduleStatusPoll();
|
||
return;
|
||
}
|
||
poll.controller = new AbortController();
|
||
try {
|
||
const response = await window.CognitiveAgentNetwork.request(
|
||
`/api/modes/schedule/status?vehicle_id=${encodeURIComponent(poll.vehicleId)}`,
|
||
{ method: "GET", signal: poll.controller.signal, timeoutMs: 8000, retries: 0 },
|
||
);
|
||
if (!isCurrent()) return;
|
||
if (response.status === 401) {
|
||
setActionStatus(message, "登录状态已失效,无法继续查询时间轴状态;请重新登录查看。", "failed");
|
||
stopScheduleStatusPoll();
|
||
return;
|
||
}
|
||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||
const status = await response.json();
|
||
if (!isCurrent()) return;
|
||
if (status.vehicle_id && String(status.vehicle_id) !== poll.vehicleId) {
|
||
throw new Error("时间轴状态车型不匹配");
|
||
}
|
||
if ((poll.runId && status.run_id && String(status.run_id) !== poll.runId)
|
||
|| (poll.sessionId && status.session_id && String(status.session_id) !== poll.sessionId)) {
|
||
setActionStatus(message, "当前时间轴已被新的运行任务替换,请查看最新模式消息。", "working");
|
||
stopScheduleStatusPoll();
|
||
return;
|
||
}
|
||
const terminal = ["completed", "failed", "cancelled", "stopped", "idle"].includes(status.status);
|
||
if (status.status === "running") {
|
||
const phase = Number.isInteger(status.phase_index) && status.phase_index >= 0
|
||
? ` · 阶段 ${status.phase_index + 1}` : "";
|
||
const repeat = Number.isInteger(status.repeat_index) && status.repeat_index >= 0
|
||
? ` · 第 ${status.repeat_index + 1} 轮` : "";
|
||
setActionStatus(message, `动态时间轴执行中${repeat}${phase}。可输入“停止动态模式”;已执行状态不自动回滚。`, "working");
|
||
} else if (terminal) {
|
||
const stopped = ["cancelled", "stopped"].includes(status.status);
|
||
const failed = status.status === "failed";
|
||
const copy = failed
|
||
? `动态时间轴执行失败:${status.error || "某个阶段未通过执行校验"}。后续阶段不再执行,已执行状态不自动回滚。`
|
||
: stopped
|
||
? "动态时间轴已停止,后续阶段不再执行;已执行状态不自动回滚。"
|
||
: status.status === "completed"
|
||
? "动态时间轴已结束,全部计划阶段已处理;请以车辆反馈确认设备实际状态。"
|
||
: "当前没有正在运行的动态时间轴。";
|
||
setActionStatus(message, copy, failed ? "failed" : "success");
|
||
window.CognitiveAgentConsole?.updateScheduleExecutionStatus?.(poll.modeProfileId, status.status);
|
||
stopScheduleStatusPoll();
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
if (!isCurrent() || poll.controller?.signal.aborted) return;
|
||
setActionStatus(message, "暂时无法获取时间轴状态,正在重新查询;需要停止时可输入“停止动态模式”。", "working");
|
||
} finally {
|
||
if (isCurrent()) poll.timer = window.setTimeout(update, 2000);
|
||
}
|
||
};
|
||
poll.timer = window.setTimeout(update, 2000);
|
||
}
|
||
|
||
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 scheduleRunning = completed
|
||
&& event.payload?.orchestration_route === "mode_schedule_execute"
|
||
&& (event.payload?.status === "running" || event.payload?.schedule_execution?.status === "running");
|
||
const defaultText = event.type === "work.completed"
|
||
? (completed ? "AI助手已完成工作" : "AI助手未能完成工作")
|
||
: (completed ? "座舱控制已完成" : "座舱控制未完成");
|
||
setActionStatus(
|
||
message,
|
||
scheduleRunning
|
||
? "已启动动态时间轴;后续动作将分阶段执行。可输入“停止动态模式”提前结束,已执行的设备状态不会自动回滚。"
|
||
: event.display_text || defaultText,
|
||
scheduleRunning ? "working" : completed ? "success" : "failed",
|
||
);
|
||
if (scheduleRunning) startScheduleStatusPoll(event.payload, message);
|
||
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 modeExecutionPreviewResult(profile) {
|
||
const flow = profile?.command_flow && typeof profile.command_flow === "object"
|
||
? profile.command_flow
|
||
: {};
|
||
return {
|
||
status: "processing",
|
||
function_configs: flow.function_configs && typeof flow.function_configs === "object"
|
||
? flow.function_configs
|
||
: {},
|
||
command_plans: flow.plans && typeof flow.plans === "object" ? flow.plans : {},
|
||
control_transport: String(flow.control_transport || ""),
|
||
execution_report: null,
|
||
operation_results: [],
|
||
};
|
||
}
|
||
|
||
function beginModeExecution(profile, { command = "" } = {}) {
|
||
const modeName = String(profile?.name || profile?.id || "功能模式");
|
||
const commandText = String(command || `进入${modeName}`).trim();
|
||
setDrawerOpen(true);
|
||
createMessage("user", commandText);
|
||
const message = createMessage("assistant", `已加载${modeName}控制指令流,正在下发。`);
|
||
setActionStatus(message, "模式控制指令正在下发", "working");
|
||
appendDispatchedControls(message, modeExecutionPreviewResult(profile));
|
||
persistMessages();
|
||
return {
|
||
message,
|
||
modeName,
|
||
commandText,
|
||
profileId: String(profile?.id || ""),
|
||
};
|
||
}
|
||
|
||
function completeModeExecution(
|
||
handle,
|
||
profile,
|
||
result = null,
|
||
{ command = "", error = null } = {},
|
||
) {
|
||
const modeName = String(handle?.modeName || profile?.name || profile?.id || "功能模式");
|
||
const commandText = String(handle?.commandText || command || `进入${modeName}`).trim();
|
||
const activeHandle = handle?.message?.body
|
||
? handle
|
||
: beginModeExecution(profile, { command: commandText });
|
||
const message = activeHandle.message;
|
||
const status = String(result?.status || "");
|
||
const scheduleRunning = status === "running"
|
||
&& result?.schedule_execution?.status === "running";
|
||
const dryRun = result?.execution_mode === "dry_run";
|
||
const waitingForFeedback = result?.execution_outcome === "submitted_unverified";
|
||
|
||
let content = `${modeName}执行未完成,请查看控制指令状态。`;
|
||
let statusText = "功能模式执行未完成";
|
||
let statusState = "failed";
|
||
if (error) {
|
||
content = `${modeName}执行失败:${error.message || "请稍后重试。"}`;
|
||
statusText = "功能模式执行失败";
|
||
} else if (scheduleRunning) {
|
||
content = `已启动动态时间轴:${commandText}。后续动作将分阶段执行。`;
|
||
statusText = "动态时间轴执行中,可输入“停止动态模式”提前结束";
|
||
statusState = "working";
|
||
} else if (status === "succeeded") {
|
||
content = dryRun
|
||
? `已完成模拟编排:${commandText}。`
|
||
: waitingForFeedback
|
||
? `已提交执行:${commandText},正在等待车辆状态确认。`
|
||
: `已完成执行:${commandText}。`;
|
||
statusText = dryRun
|
||
? "功能模式模拟编排已完成"
|
||
: waitingForFeedback
|
||
? "模式指令已提交,等待车辆状态确认"
|
||
: "功能模式执行已完成";
|
||
statusState = "success";
|
||
} else if (status === "partially_succeeded") {
|
||
content = `${modeName}仅部分完成,请查看未完成的控制指令。`;
|
||
statusText = "功能模式部分完成";
|
||
}
|
||
|
||
setMessageContent(message, content, { final: true });
|
||
setActionStatus(message, statusText, statusState);
|
||
if (result) appendDispatchedControls(message, result);
|
||
if (scheduleRunning) startScheduleStatusPoll(result, message);
|
||
persistMessages();
|
||
return activeHandle;
|
||
}
|
||
|
||
function announceModeExecution(profile, result = null, { command = "", error = null } = {}) {
|
||
const handle = beginModeExecution(profile, { command });
|
||
return completeModeExecution(handle, profile, result, { command, error });
|
||
}
|
||
|
||
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?.();
|
||
stopScheduleStatusPoll();
|
||
resetConversation();
|
||
}, true);
|
||
|
||
document.addEventListener("change", (event) => {
|
||
if (event.target?.id === "vehicleSelect") stopScheduleStatusPoll();
|
||
});
|
||
window.addEventListener("pagehide", stopScheduleStatusPoll);
|
||
|
||
document.addEventListener("DOMContentLoaded", initialize);
|
||
window.CognitiveAgentAssistant = {
|
||
submit: submitAssistant,
|
||
open: () => setDrawerOpen(true),
|
||
close: () => setDrawerOpen(false),
|
||
resetConversation,
|
||
startModeCreation,
|
||
beginModeExecution,
|
||
completeModeExecution,
|
||
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 };
|
||
}
|
||
}());
|