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

901 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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, { updated = false } = {}) {
if (!message?.body || !profile?.id) return;
const selector = `[data-assistant-mode-created="${CSS.escape(String(profile.id))}"]`;
if ($(selector, message.body)) return;
const card = document.createElement("div");
card.className = "assistant-mode-created";
card.dataset.assistantModeCreated = String(profile.id);
const icon = document.createElement("span");
icon.className = "assistant-mode-created-icon";
icon.innerHTML = '<i data-lucide="circle-check"></i>';
const copy = document.createElement("span");
copy.className = "assistant-mode-created-copy";
const label = document.createElement("small");
label.textContent = updated ? "已更新「我的模式」" : "已添加到「我的模式」";
const name = document.createElement("strong");
name.textContent = String(profile.name || "新功能模式");
copy.append(label, name);
const reveal = document.createElement("button");
reveal.type = "button";
reveal.textContent = "查看";
reveal.addEventListener("click", () => {
window.CognitiveAgentConsole?.revealModeProfile?.(String(profile.id), { focus: true });
});
card.append(icon, copy, reveal);
message.body.append(card);
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
followLatest(true);
}
function modeDraftRows(draft, rawActions) {
const instruction = String(draft?.instruction || "");
const actions = Array.isArray(rawActions)
? rawActions.filter((item) => item && typeof item === "object")
: [];
const consumed = new Set();
const rows = [];
const targetZone = (action) => String(
action?.target?.instance || action?.target?.zone || action?.target || "",
);
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
? '<i data-lucide="refresh-cw"></i><span>确认更新此模式</span>'
: '<i data-lucide="bookmark-plus"></i><span>保存为功能模式</span>';
const trial = document.createElement("button");
trial.type = "button";
trial.className = "assistant-mode-draft-trial";
trial.textContent = "先试运行";
controls.append(save, trial);
const status = document.createElement("p");
status.className = "assistant-mode-draft-status";
status.hidden = true;
status.setAttribute("role", "status");
const buttons = [save, trial];
save.addEventListener("click", () => {
void saveModeProfileDraft(message, card, draft, buttons, status);
});
trial.addEventListener("click", () => {
void tryModeProfileDraft(card, draft, actions, buttons, status);
});
card.append(heading, list, controls, status);
message.body.append(card);
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
followLatest(true);
}
function startModeCreation() {
setDrawerOpen(true);
const input = $("#utterance");
if (!input) return;
const helper = createMessage(
"assistant",
"告诉我模式名称和希望联动的座舱状态。比如:创建宝宝睡眠模式,副驾半躺、空调 24℃、关闭阅读灯。我会先生成动作预览,确认后再加入「我的模式」。",
);
helper.article.classList.add("assistant-mode-guide");
input.value = "请帮我创建一个";
input.dispatchEvent(new Event("input", { bubbles: true }));
input.focus({ preventScroll: true });
input.setSelectionRange(input.value.length, input.value.length);
}
function setComposerPending(pending) {
const button = $("#runButton");
const input = $("#utterance");
if (!button || !input) return;
document.body.classList.toggle("assistant-drawer-busy", pending);
button.classList.toggle("assistant-stop", pending);
button.setAttribute("aria-label", pending ? "停止生成" : "发送消息");
button.title = pending ? "停止生成" : "发送消息";
// General questions must stay available even when no physical CAN adapter
// is connected; hardware eligibility is evaluated only for control routes.
button.disabled = false;
button.innerHTML = pending
? '<i data-lucide="square"></i><span>停止</span>'
: '<i data-lucide="send-horizontal"></i><span>发送</span>';
input.setAttribute("aria-busy", String(pending));
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}
function requestBody(message) {
const executionMode = window.CognitiveAgentConsole?.getExecutionMode?.() || "dry_run";
const controlTransport = localStorage.getItem("cognitive-agent.control-transport") || "wired_can";
const hardwareClientId = localStorage.getItem("cognitive-agent.hardware-client-id");
const vehicleId = $("#vehicleSelect")?.value || null;
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 };
}
}());