Files
Cognitive_Agent/apps/web-console/assistant-chat.js
T

880 lines
34 KiB
JavaScript

(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 = '<i data-lucide="bot-message-square"></i><span class="visually-hidden">AI助手</span><b aria-hidden="true"></b>';
($("#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 = '<i data-lucide="waypoints"></i>';
article.append(avatar);
}
const body = document.createElement("div");
body.className = "assistant-message-body";
const contentNode = document.createElement("div");
contentNode.className = "assistant-message-content";
if (pending && !content) {
contentNode.classList.add("assistant-thinking");
contentNode.innerHTML = '<span>正在理解你的需求</span><i></i><i></i><i></i>';
} else if (role === "assistant") {
renderRichText(contentNode, content);
} else {
contentNode.textContent = content;
}
body.append(contentNode);
const action = document.createElement("div");
action.className = "assistant-action-status";
action.hidden = true;
action.setAttribute("role", "status");
body.append(action);
article.append(body);
$("#assistantMessages").append(article);
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
followLatest(true);
return { article, body, contentNode, action, content };
}
function setMessageContent(message, content, { final = false } = {}) {
message.content = content;
message.article.dataset.messageContent = content;
message.contentNode.classList.remove("assistant-thinking");
if (final) renderRichText(message.contentNode, content);
else message.contentNode.textContent = content;
followLatest();
}
function setActionStatus(message, text, state = "working") {
message.action.hidden = !text;
message.action.className = `assistant-action-status ${state}`;
message.action.replaceChildren();
if (!text) return;
const icon = document.createElement("i");
icon.setAttribute(
"data-lucide",
state === "success" ? "circle-check" : state === "failed" ? "circle-x" : "loader-circle",
);
const label = document.createElement("span");
label.textContent = text;
message.action.append(icon, label);
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
followLatest();
}
function appendModeCreatedState(message, profile) {
if (!message?.body || !profile?.id) return;
const selector = `[data-assistant-mode-created="${CSS.escape(String(profile.id))}"]`;
if ($(selector, message.body)) return;
const card = document.createElement("div");
card.className = "assistant-mode-created";
card.dataset.assistantModeCreated = String(profile.id);
const icon = document.createElement("span");
icon.className = "assistant-mode-created-icon";
icon.innerHTML = '<i data-lucide="circle-check"></i>';
const copy = document.createElement("span");
copy.className = "assistant-mode-created-copy";
const label = document.createElement("small");
label.textContent = "已添加到「我的模式」";
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") {
label = ["passenger", "front_right"].includes(zone) ? "副驾座椅" : "主驾座椅";
value = params.position_pct != null
? `${params.position_pct}%`
: 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") {
label = "空调";
value = params.level != null ? `${params.level}档` : 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";
status.textContent = "正在保存到「我的模式」…";
try {
const response = await window.CognitiveAgentNetwork.request("/api/mode-profiles", {
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 = "模式已保存";
card.querySelector(".assistant-mode-draft-actions")?.setAttribute("hidden", "");
await window.CognitiveAgentConsole?.refreshModeProfiles?.(payload.mode_profile?.id);
appendModeCreatedState(message, payload.mode_profile);
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, 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);
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 = "已为你生成功能模式";
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 = '<i data-lucide="bookmark-plus"></i><span>保存为功能模式</span>';
const trial = document.createElement("button");
trial.type = "button";
trial.className = "assistant-mode-draft-trial";
trial.textContent = "先试运行";
controls.append(save, trial);
const status = document.createElement("p");
status.className = "assistant-mode-draft-status";
status.hidden = true;
status.setAttribute("role", "status");
const buttons = [save, trial];
save.addEventListener("click", () => {
void saveModeProfileDraft(message, card, draft, buttons, status);
});
trial.addEventListener("click", () => {
void tryModeProfileDraft(card, draft, buttons, status);
});
card.append(heading, list, controls, status);
message.body.append(card);
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
followLatest(true);
}
function startModeCreation() {
setDrawerOpen(true);
const input = $("#utterance");
if (!input) return;
const helper = createMessage(
"assistant",
"告诉我模式名称和希望联动的座舱状态。比如:创建宝宝睡眠模式,副驾半躺、空调 24℃、关闭阅读灯。我会先生成动作预览,确认后再加入「我的模式」。",
);
helper.article.classList.add("assistant-mode-guide");
input.value = "请帮我创建一个";
input.dispatchEvent(new Event("input", { bubbles: true }));
input.focus({ preventScroll: true });
input.setSelectionRange(input.value.length, input.value.length);
}
function setComposerPending(pending) {
const button = $("#runButton");
const input = $("#utterance");
if (!button || !input) return;
document.body.classList.toggle("assistant-drawer-busy", pending);
button.classList.toggle("assistant-stop", pending);
button.setAttribute("aria-label", pending ? "停止生成" : "发送消息");
button.title = pending ? "停止生成" : "发送消息";
// General questions must stay available even when no physical CAN adapter
// is connected; hardware eligibility is evaluated only for control routes.
button.disabled = false;
button.innerHTML = pending
? '<i data-lucide="square"></i><span>停止</span>'
: '<i data-lucide="send-horizontal"></i><span>发送</span>';
input.setAttribute("aria-busy", String(pending));
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}
function requestBody(message) {
const executionMode = window.CognitiveAgentConsole?.getExecutionMode?.() || "dry_run";
const controlTransport = localStorage.getItem("cognitive-agent.control-transport") || "wired_can";
const hardwareClientId = localStorage.getItem("cognitive-agent.hardware-client-id");
const vehicleId = $("#vehicleSelect")?.value || null;
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 };
}
}());