(function initializeUnifiedAssistant() { const STORAGE_KEY = "cognitive-agent.assistant-messages"; const CONVERSATION_STORAGE_KEY = "cognitive-agent.conversation-id"; const MAX_STORED_MESSAGES = 24; const COMPOSER_MIN_HEIGHT = 64; const COMPOSER_MAX_HEIGHT = 116; const WELCOME_TEXT = "你好,我是 AI助手。你可以向我提问,也可以直接交代需要完成的工作。"; let pendingController = null; let pendingMessage = null; let latestControlResult = 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助手'; ($("#v2PanelControlDock") || 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 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 appendModeCreatedState(message, profile, { updated = false } = {}) { if (!message?.body || !profile?.id) return; const selector = `[data-assistant-mode-created="${CSS.escape(String(profile.id))}"]`; if ($(selector, message.body)) return; const card = document.createElement("div"); card.className = "assistant-mode-created"; card.dataset.assistantModeCreated = String(profile.id); const icon = document.createElement("span"); icon.className = "assistant-mode-created-icon"; icon.innerHTML = ''; const copy = document.createElement("span"); copy.className = "assistant-mode-created-copy"; const label = document.createElement("small"); label.textContent = updated ? "已更新「我的模式」" : "已添加到「我的模式」"; const name = document.createElement("strong"); name.textContent = String(profile.name || "新功能模式"); copy.append(label, name); const reveal = document.createElement("button"); reveal.type = "button"; reveal.textContent = "查看"; reveal.addEventListener("click", () => { window.CognitiveAgentConsole?.revealModeProfile?.(String(profile.id), { focus: true }); }); card.append(icon, copy, reveal); message.body.append(card); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); followLatest(true); } function modeDraftRows(draft, rawActions) { const instruction = String(draft?.instruction || ""); const actions = Array.isArray(rawActions) ? rawActions.filter((item) => item && typeof item === "object") : []; const consumed = new Set(); const rows = []; const targetZone = (action) => String( action?.target?.instance || action?.target?.zone || action?.target || "", ); const parameters = (action) => ( action?.parameters && typeof action.parameters === "object" ? action.parameters : {} ); const instructionOrder = (...terms) => { const indexes = terms.map((term) => instruction.indexOf(term)).filter((index) => index >= 0); return indexes.length ? Math.min(...indexes) : 10_000 + rows.length; }; const addRow = (label, value, icon, order, indexes = []) => { indexes.forEach((index) => consumed.add(index)); rows.push({ label, value, icon, order }); }; const zeroGravityIndexes = actions .map((action, index) => ({ action, index })) .filter(({ action }) => String(action.capability || "").startsWith("seat.set_") && ["passenger", "front_right"].includes(targetZone(action))) .map(({ index }) => index); if (/零重力/.test(instruction) && zeroGravityIndexes.length >= 2) { addRow( "副驾座椅", "零重力模式", "armchair", instructionOrder("副驾", "零重力"), zeroGravityIndexes, ); } const lightIndexes = actions .map((action, index) => ({ action, index })) .filter(({ action }) => ["interior_light", "ambient_light"].includes(String(action.domain || ""))) .map(({ index }) => index); if ( lightIndexes.length >= 2 && /(?:灯光.*(?:全部|所有|全车).*(?:关闭|关掉|熄灭)|(?:全部|所有|全车).*灯光.*(?:关闭|关掉|熄灭))/.test(instruction) ) { addRow( "座舱灯光", "全部关闭", "lightbulb-off", instructionOrder("灯光"), lightIndexes, ); } const motionLabel = (value) => ({ open: "打开", close: "关闭", stop: "停止", half: "半开", on: "开启", off: "关闭", }[String(value)] || String(value || "已配置")); actions.forEach((action, index) => { if (consumed.has(index)) return; const capability = String(action.capability || ""); const domain = String(action.domain || ""); const zone = targetZone(action); const params = parameters(action); let label = capability || domain || "座舱设备"; let value = "已配置"; let icon = "sliders-horizontal"; let order = 10_000 + index; if (capability === "roof.set_shade_motion" || capability === "roof.set_shade_position") { label = "遮阳帘"; value = params.position_pct != null ? `${params.position_pct}%` : motionLabel(params.motion); icon = "blinds"; order = instructionOrder("遮阳帘", "天幕帘"); } else if (domain === "window") { const windowLabels = { all: "全部车窗", front: "前排车窗", rear: "后排车窗", front_left: "主驾车窗", front_right: "副驾车窗", rear_left: "左后车窗", rear_right: "右后车窗", }; label = windowLabels[zone] || "车窗"; value = params.position_pct != null ? `${params.position_pct}%` : motionLabel(params.motion); icon = "panels-top-left"; order = instructionOrder("车窗"); } else if (domain === "seat") { const seatLabel = ["passenger", "front_right"].includes(zone) ? "副驾" : "主驾"; const seatCapabilityLabels = { "seat.set_longitudinal_position": "座椅前后位置", "seat.set_cushion_tilt": "坐垫角度", "seat.set_backrest_recline": "靠背角度", "seat.set_legrest_height": "腿托高度", }; label = `${seatLabel}${seatCapabilityLabels[capability] || "座椅"}`; value = params.position_pct != null ? `${params.position_pct}%${params.angle_deg != null ? `(${params.angle_deg}°)` : ""}` : motionLabel(params.state || params.mode_id); icon = "armchair"; order = instructionOrder("座椅", "副驾", "主驾"); } else if (capability === "climate.set_temperature") { label = "空调温度"; value = `${params.temperature_c ?? "--"}℃`; icon = "thermometer"; order = instructionOrder("空调", "温度"); } else if (domain === "climate") { const cycleLabels = { internal: "内循环", external: "外循环", auto: "自动循环" }; label = capability === "climate.set_fan_level" ? "空调风量" : capability === "climate.set_cycle_mode" ? "空调循环" : "空调"; value = params.level != null ? `${params.level}档` : params.mode != null ? (cycleLabels[String(params.mode)] || String(params.mode)) : motionLabel(params.state); icon = "fan"; order = instructionOrder("空调", "风量"); } else if (capability === "interior_light.set_reading_power") { label = "阅读灯"; value = motionLabel(params.state); icon = "book-open"; order = instructionOrder("阅读灯", "灯光"); } else if (["interior_light", "ambient_light"].includes(domain)) { label = domain === "ambient_light" ? "氛围灯" : "室内灯"; value = motionLabel(params.state); icon = "lightbulb"; order = instructionOrder(label, "灯光"); } addRow(label, value, icon, order, [index]); }); return rows.sort((left, right) => left.order - right.order); } async function saveModeProfileDraft(message, card, draft, buttons, status) { buttons.forEach((button) => { button.disabled = true; }); card.classList.add("is-busy"); status.hidden = false; status.className = "assistant-mode-draft-status working"; const existingModeId = String(draft.existing_mode_id || "").trim(); status.textContent = existingModeId ? "正在将验证结果保存为模式更新…" : "正在保存到「我的模式」…"; try { const endpoint = existingModeId ? `/api/mode-profiles/${encodeURIComponent(existingModeId)}` : "/api/mode-profiles"; const response = await window.CognitiveAgentNetwork.request(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: draft.name, instruction: draft.instruction, source_request_id: draft.source_request_id, vehicle_id: draft.vehicle_id || $("#vehicleSelect")?.value || null, }), timeoutMs: 45 * 1000, retries: 0, }); const payload = await response.json(); if (response.status === 401) { window.location.replace("/login"); return; } if (!response.ok) throw new Error(payload.message || payload.error || `HTTP ${response.status}`); card.classList.remove("is-busy"); card.classList.add("is-saved"); status.className = "assistant-mode-draft-status success"; status.textContent = existingModeId ? "模式已更新" : "模式已保存"; card.querySelector(".assistant-mode-draft-actions")?.setAttribute("hidden", ""); 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 appendModeDraftState(message, draft, actions) { if (!message?.body || !draft?.name || !draft?.source_request_id) return; 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); message.article?.classList.add("assistant-message-mode-draft"); const heading = document.createElement("header"); const headingCopy = document.createElement("span"); const eyebrow = document.createElement("small"); eyebrow.textContent = draft.existing_mode_id ? "已载入现有功能模式" : "已为你生成功能模式"; const title = document.createElement("strong"); title.textContent = `「${draft.name}」`; headingCopy.append(eyebrow, title); const badge = document.createElement("span"); badge.className = "assistant-mode-draft-badge"; badge.textContent = `${Number(draft.operation_count) || 0} 项动作`; heading.append(headingCopy, badge); const list = document.createElement("div"); list.className = "assistant-mode-draft-list"; modeDraftRows(draft, actions).forEach((item) => { const row = document.createElement("div"); row.className = "assistant-mode-draft-row"; const icon = document.createElement("i"); icon.dataset.lucide = item.icon; const label = document.createElement("span"); label.textContent = item.label; const value = document.createElement("strong"); value.textContent = item.value; row.append(icon, label, value); list.append(row); }); const controls = document.createElement("div"); controls.className = "assistant-mode-draft-actions"; const save = document.createElement("button"); save.type = "button"; save.className = "assistant-mode-draft-save"; save.innerHTML = draft.existing_mode_id ? '确认更新此模式' : '保存为功能模式'; const trial = document.createElement("button"); trial.type = "button"; trial.className = "assistant-mode-draft-trial"; trial.textContent = "先试运行"; controls.append(save, trial); const status = document.createElement("p"); status.className = "assistant-mode-draft-status"; status.hidden = true; status.setAttribute("role", "status"); const buttons = [save, trial]; save.addEventListener("click", () => { void saveModeProfileDraft(message, card, draft, buttons, status); }); trial.addEventListener("click", () => { void tryModeProfileDraft(card, draft, actions, buttons, status); }); card.append(heading, list, controls, status); message.body.append(card); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); followLatest(true); } function startModeCreation() { setDrawerOpen(true); const input = $("#utterance"); if (!input) return; const helper = createMessage( "assistant", "告诉我模式名称和希望联动的座舱状态。比如:创建宝宝睡眠模式,副驾半躺、空调 24℃、关闭阅读灯。我会先生成动作预览,确认后再加入「我的模式」。", ); helper.article.classList.add("assistant-mode-guide"); input.value = "请帮我创建一个"; input.dispatchEvent(new Event("input", { bubbles: true })); input.focus({ preventScroll: true }); input.setSelectionRange(input.value.length, input.value.length); } function setComposerPending(pending) { const button = $("#runButton"); const input = $("#utterance"); if (!button || !input) return; document.body.classList.toggle("assistant-drawer-busy", pending); button.classList.toggle("assistant-stop", pending); button.setAttribute("aria-label", pending ? "停止生成" : "发送消息"); button.title = pending ? "停止生成" : "发送消息"; // General questions must stay available even when no physical CAN adapter // is connected; hardware eligibility is evaluated only for control routes. button.disabled = false; button.innerHTML = pending ? '停止' : '发送'; input.setAttribute("aria-busy", String(pending)); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); } function requestBody(message) { const executionMode = window.CognitiveAgentConsole?.getExecutionMode?.() || "dry_run"; const controlTransport = localStorage.getItem("cognitive-agent.control-transport") || "wired_can"; const hardwareClientId = localStorage.getItem("cognitive-agent.hardware-client-id"); const vehicleId = $("#vehicleSelect")?.value || null; return { message, conversation_id: conversationId(), client_type: "web_console_codex_assistant", execution_mode: executionMode, control_transport: controlTransport, ...(vehicleId ? { vehicle_id: vehicleId } : {}), ...(executionMode === "hardware" && controlTransport !== "wireless_mqtt" && hardwareClientId ? { hardware_client_id: hardwareClientId } : {}), ...(executionMode === "simulated_hardware" ? { simulation_fault: $("#simulationFault")?.value || "normal" } : {}), }; } async function readErrorResponse(response) { try { const payload = await response.json(); return payload.message || payload.error || `HTTP ${response.status}`; } catch (_error) { return `HTTP ${response.status}`; } } function applyAssistantEvent(event, message) { if (event.type === "answer.delta") { setMessageContent(message, `${message.content}${event.delta || ""}`); return; } if (["action.started", "action.progress", "work.started", "work.progress"].includes(event.type)) { const defaultText = event.type.startsWith("work.") ? "AI助手正在处理工作……" : "正在执行座舱控制……"; setActionStatus(message, event.display_text || defaultText, "working"); if (event.type === "action.progress" && event.payload) { window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-control-activity", { detail: event.payload, })); } return; } if (event.type === "action.completed" || event.type === "work.completed") { latestControlResult = event.payload || latestControlResult; const completed = event.success !== false; const defaultText = event.type === "work.completed" ? (completed ? "AI助手已完成工作" : "AI助手未能完成工作") : (completed ? "座舱控制已完成" : "座舱控制未完成"); setActionStatus( message, event.display_text || defaultText, completed ? "success" : "failed", ); if (event.payload) { window.CognitiveAgentConsole?.applySyncedResult?.(event.payload); 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 && event.payload.orchestration_route === "mode_profile_create" ) { appendModeCreatedState(message, event.payload.mode_profile); } } return; } if (event.type === "answer.completed") { const authoritativeAnswer = Object.prototype.hasOwnProperty.call(event, "answer") ? String(event.answer || "") : message.content; setMessageContent(message, authoritativeAnswer, { final: true }); 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() { if (pendingController) { 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 messageText = input?.value.trim() || ""; if (!messageText) { window.CognitiveAgentConsole?.notify?.("请输入问题或工作需求"); return; } createMessage("user", messageText); 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; pendingController = new AbortController(); setComposerPending(true); let completedError = null; try { await streamAssistant(messageText, assistantMessage, pendingController.signal); setMessageContent(assistantMessage, assistantMessage.content, { final: true }); persistMessages(); } catch (error) { completedError = error; if (pendingController.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 { pendingController = null; pendingMessage = null; setComposerPending(false); input.focus({ preventScroll: true }); window.dispatchEvent(new CustomEvent("cognitive-agent:request-complete", { detail: { utterance: messageText, result: latestControlResult, error: completedError, }, })); } } 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 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(); 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; sessionStorage.removeItem(STORAGE_KEY); sessionStorage.setItem(CONVERSATION_STORAGE_KEY, createConversationId()); }, true); document.addEventListener("DOMContentLoaded", initialize); window.CognitiveAgentAssistant = { submit: submitAssistant, open: () => setDrawerOpen(true), close: () => setDrawerOpen(false), startModeCreation, 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 }; } }());