(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 = '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 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 = ''; 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": "红外外设", "android.launch_app": "打开视频媒体", }; 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": "腿托", "seat.set_ventilation": "通风", "seat.set_heating": "加热", "seat.set_driver_ventilation_native": "通风", "seat.set_driver_heating_native": "加热", "seat.set_driver_massage_native": "按摩", }[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 "空调风口"; const domain = String(operation.domain || capability.split(".", 1)[0] || ""); return { climate: "空调", window: "车窗", roof: "遮阳帘", ambient_light: "氛围灯", interior_light: "室内灯", self_extension: "红外外设", door: "车门", outlet: "空调风口", }[domain] || (/^[\u4e00-\u9fff]/.test(String(fallback || "")) ? String(fallback) : "控制指令"); } 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 previewOnly = String(result.execution_mode || "dry_run") !== "hardware" || result.orchestration_route === "mode_profile_create_preview"; 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) commands = 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" && !previewOnly ? { deliveryState: "submitting", deliveryLabel: "正在下发", deliveryDetail: "", } : {}; const previewDelivery = previewOnly ? { deliveryState: "not_submitted", deliveryLabel: "仅预览 · 未下发", deliveryDetail: "创建模式期间不会发送控制指令", } : {}; return { ...item, ...pendingDelivery, ...previewDelivery, 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: "外循环", romantic: "浪漫", cinema: "影院", rest: "休息", camping: "露营", meditation: "冥想", }[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); const capability = String(item.operation?.capability || ""); if (capability === "android.launch_app") { const packageName = String(parameters.package_name || "").trim(); const activityName = String(parameters.activity_name || "").trim(); return [ packageName ? `应用 ${packageName}` : "视频应用", activityName ? `页面 ${activityName}` : "", ].filter(Boolean).join(" · "); } if ( level !== null && ["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"] .includes(capability) ) { return ({ 1: "3档", 2: "1档" })[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"; if (capability === "android.launch_app") return "clapperboard"; 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 previewOnly = String(mergedResult.execution_mode || "dry_run") !== "hardware" || mergedResult.orchestration_route === "mode_profile_create_preview"; const headingText = previewOnly ? "控制指令预览(未下发)" : "下发控制指令"; 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, { heading: headingText })) { 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 = ''; const heading = document.createElement("b"); heading.textContent = headingText; 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 heading = $("summary b", card); if (heading) heading.textContent = headingText; 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"); 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 = ''; 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 domainLabels = { climate: "空调", window: "车窗", roof: "遮阳帘", seat: "座椅", ambient_light: "氛围灯", interior_light: "室内灯", self_extension: "红外外设", door: "车门", outlet: "空调风口", }; const capabilityLabels = { "climate.set_power": "空调电源", "climate.set_temperature": "空调温度", "climate.set_fan_level": "空调风量", "climate.set_cycle_mode": "空调循环", "climate.set_control_mode": "空调控制模式", "climate.set_outlet_mode": "空调出风模式", "window.set_position": "车窗位置", "roof.set_shade_position": "遮阳帘位置", "ambient_light.set_power": "氛围灯", "ambient_light.set_mode": "氛围灯模式", "infrared.emit": "红外外设", "android.launch_app": "打开视频媒体", }; 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; }; const localizedParameterValue = (value) => ({ on: "开启", off: "关闭", open: "打开", close: "关闭", stop: "停止", auto: "自动", manual: "手动", internal: "内循环", external: "外循环", romantic: "浪漫", cinema: "影院", rest: "休息", camping: "露营", }[String(value)] || String(value)); const genericParameterText = (params) => Object.entries(params) .filter(([key]) => key !== "slot" && key !== "capability_ref") .map(([key, rawValue]) => { const name = { state: "状态", motion: "动作", action: "动作", mode: "模式", level: "档位", temperature_c: "温度", position_pct: "目标开度", brightness_pct: "亮度", brightness_level: "亮度档位", color_hex: "颜色", color_index: "色号", }[key] || "参数"; const suffix = key === "temperature_c" ? "℃" : ["position_pct", "brightness_pct"].includes(key) ? "%" : key === "level" || key === "brightness_level" ? "档" : ""; return `${name}:${localizedParameterValue(rawValue)}${suffix}`; }) .join(" · "); const passengerComfort = actions .map((action, index) => ({ action, index })) .filter(({ action, index }) => !consumed.has(index) && ["passenger", "front_right"].includes(targetZone(action)) && [ "seat.set_ventilation", "seat.set_backrest_recline", "seat.set_legrest_height", ].includes(String(action.capability || ""))); const comfortByCapability = new Map(passengerComfort.map(({ action, index }) => [ String(action.capability || ""), { action, index }, ])); if ([ "seat.set_ventilation", "seat.set_backrest_recline", "seat.set_legrest_height", ].every((capability) => comfortByCapability.has(capability))) { const ventilation = parameters(comfortByCapability.get("seat.set_ventilation").action); const backrest = parameters(comfortByCapability.get("seat.set_backrest_recline").action); const legrest = parameters(comfortByCapability.get("seat.set_legrest_height").action); const comfortMentions = ["座椅", "副驾", "舒适"] .map((term) => instruction.indexOf(term)) .filter((index) => index >= 0); const comfortOrder = comfortMentions.length ? Math.min(...comfortMentions) : 10_000 + Math.max(...passengerComfort.map(({ index }) => index)); addRow( "副驾座椅舒适模式", [ `通风${ventilation.state === "off" ? "关闭" : `开启 ${ventilation.level ?? 1}档`}`, `靠背 ${backrest.position_pct ?? "--"}%`, `腿托 ${legrest.position_pct ?? "--"}%`, ].join(" · "), "armchair", comfortOrder, passengerComfort.map(({ index }) => index), ); } 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 = capabilityLabels[capability] || domainLabels[domain] || "座舱设备"; let value = "已配置"; let icon = "sliders-horizontal"; let order = 10_000 + index; if (capability === "infrared.emit" || domain === "self_extension") { label = String(action?.target?.name || action?.target?.device || "红外外设").trim() || "红外外设"; value = params.action != null ? `动作:${localizedParameterValue(params.action)}` : "动作已配置"; icon = "radio-tower"; order = instructionOrder(label, "外设", "灯"); } else 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 ? Number(params.position_pct) === 0 ? "完全关闭(目标开度 0%)" : Number(params.position_pct) === 100 ? "完全打开(目标开度 100%)" : `目标开度 ${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 encodes high as 1 and low as 2 internally. Present the // occupant-facing three-speed scale instead of leaking CAN enums. const level = { 1: "3档(高)", 2: "1档(低)" }[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_power" ? "空调电源" : 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, "灯光"); } if (value === "已配置") { value = genericParameterText(params) || "参数已配置"; } 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 = "正在重新进行 dry-run 校验(不会下发)…"; 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 previewNotice = document.createElement("p"); previewNotice.className = "assistant-mode-draft-preview-notice"; previewNotice.textContent = "创建阶段仅生成并校验控制方案,不会向 CAN 或红外外设发送指令。"; list.append(previewNotice); 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 isModeCreationPreviewRequest(message) { const compact = String(message || "").trim().replace(/\s+/g, ""); if (!compact || /取消(?:创建|构建|生成)|(?:不要|别|禁止)(?:创建|构建|生成)/.test(compact)) { return false; } return /(?:构建|创建|新建|新增|生成|添加)(?:一下)?(?:一个|个|一套|套)?[^,。!?!?]{1,64}?模式/.test(compact); } function validatedHardwareClientId() { const sharedClientId = window.CognitiveAgentConsole?.getHardwareClientId?.(); let storedClientId = null; try { storedClientId = localStorage.getItem("cognitive-agent.hardware-client-id"); } catch (_error) { // The shared console identity remains authoritative when storage is unavailable. } return [sharedClientId, storedClientId] .map((value) => String(value || "").trim()) .find((value) => /^hwc-[a-z0-9-]{8,80}$/.test(value)) || null; } function requestBody(message) { const configuredExecutionMode = window.CognitiveAgentConsole?.getExecutionMode?.() || "dry_run"; const controlTransport = localStorage.getItem("cognitive-agent.control-transport") || "wired_can"; const hardwareClientId = validatedHardwareClientId(); const vehicleId = $("#vehicleSelect")?.value || null; const modeProfileDraftContext = requestModeDraftContext(message); const modeConfigurationPreview = Boolean(modeProfileDraftContext) || isModeCreationPreviewRequest(message); const executionMode = modeConfigurationPreview ? "dry_run" : configuredExecutionMode; 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"; const delegatedCount = Array.isArray(result?.deferred_requests) ? result.deferred_requests.filter( (item) => item?.execution_backend === "n7_vehicle_transfer", ).length : 0; 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 = delegatedCount > 0 ? `${modeName}指令已提交,未由 PC 执行的 ${delegatedCount} 项动作将由车机继续处理。` : waitingForFeedback ? `${modeName}指令已提交,正在等待车辆状态确认。` : `${modeName}执行结果已更新,请查看各控制指令状态。`; statusText = waitingForFeedback || delegatedCount > 0 ? "功能模式指令已提交" : "功能模式执行结果已更新"; statusState = waitingForFeedback || delegatedCount > 0 ? "working" : "success"; } 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 }; } }());