793 lines
36 KiB
JavaScript
793 lines
36 KiB
JavaScript
export function createSidebarPageAiSessionRuntime(context) {
|
|
const {
|
|
currentDocumentId,
|
|
currentRootUri,
|
|
currentSourceKind,
|
|
documentRef,
|
|
pageAiApplyRuntimeState,
|
|
pageAiCurrentAgentId,
|
|
pageAiCurrentProfile,
|
|
pageAiErrorMessage,
|
|
pageAiNormalizeAgentId,
|
|
pageAiNormalizeArray,
|
|
pageAiNormalizeChatOnlyProfileId,
|
|
pageAiPermissionMessage,
|
|
pageAiPreviewValue,
|
|
pageAiRunProfile,
|
|
pageAiSetActiveProfile,
|
|
pageAiTimestamp,
|
|
pageUiState,
|
|
renderPageAiControls,
|
|
renderPageAiConversation,
|
|
resolveWorkspaceId,
|
|
sessionStorageVersion,
|
|
windowRef,
|
|
} = context;
|
|
const doc = documentRef || document;
|
|
const win = windowRef || window;
|
|
|
|
function pageAiStorageKey() {
|
|
return 'hermes_page_ai_session:' + currentDocumentId();
|
|
}
|
|
|
|
function pageAiBackendSessionQuery(extra) {
|
|
var params = new URLSearchParams();
|
|
params.set('source', 'acp');
|
|
params.set('workspaceId', resolveWorkspaceId(doc.body));
|
|
params.set('documentId', currentDocumentId());
|
|
params.set('profile', pageAiRunProfile());
|
|
params.set('sourceKind', currentSourceKind());
|
|
if (currentRootUri()) params.set('rootUri', currentRootUri());
|
|
Object.keys(extra || {}).forEach(function(key) {
|
|
var value = extra[key];
|
|
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
|
params.set(key, String(value));
|
|
}
|
|
});
|
|
return params.toString();
|
|
}
|
|
|
|
function pageAiNewSession(title) {
|
|
var now = Date.now();
|
|
var agentId = pageAiCurrentAgentId();
|
|
var profile = agentId === 'chat_only' ? pageAiNormalizeChatOnlyProfileId(pageAiCurrentProfile()) : pageAiCurrentProfile();
|
|
return {
|
|
id: 'sess_' + now + '_' + Math.random().toString(16).slice(2, 8),
|
|
title: title || '新会话',
|
|
agentId: agentId,
|
|
profileId: pageUiState.pageAiAcpRuntime === 'hermes' ? profile : '',
|
|
profile: profile,
|
|
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
source: 'draft',
|
|
usage: null,
|
|
status: 'draft',
|
|
messages: []
|
|
};
|
|
}
|
|
|
|
function pageAiNormalizeSessions(sessions) {
|
|
return (Array.isArray(sessions) ? sessions : [])
|
|
.slice(0, 20)
|
|
.map(function(session) {
|
|
var sessionStorage = String(session && (session.sessionStorage || session.session_storage) || '').trim();
|
|
var persistence = String(session && session.persistence || '').trim();
|
|
var agentId = pageAiNormalizeAgentId(session && (session.agentId || session.agent_id));
|
|
var profileId = String(session && (session.profileId || session.profile_id || '') || '').trim();
|
|
var profile = String(session && session.profile || pageAiCurrentProfile()).trim() || 'default';
|
|
if (!profileId && agentId !== 'reasonix') profileId = profile;
|
|
if (agentId === 'chat_only') {
|
|
profileId = pageAiNormalizeChatOnlyProfileId(profileId || profile);
|
|
profile = profileId;
|
|
}
|
|
return {
|
|
id: String(session && session.id || '').trim() || pageAiNewSession().id,
|
|
title: String(session && session.title || '').trim() || '新会话',
|
|
agentId: agentId,
|
|
profileId: profileId,
|
|
profile: profile,
|
|
acpRuntime: String(session && session.acpRuntime || 'reasonix').trim(),
|
|
createdAt: pageAiTimestamp(session && session.createdAt),
|
|
updatedAt: pageAiTimestamp(session && session.updatedAt),
|
|
source: String(session && session.source || 'local').trim() || 'local',
|
|
persistence: persistence,
|
|
sessionStorage: sessionStorage,
|
|
permissionLevel: String(session && (session.permissionLevel || session.permission_level) || '').trim(),
|
|
shareId: String(session && (session.shareId || session.share_id) || '').trim(),
|
|
acpSessionId: String(session && (session.acpSessionId || session.acp_session_id) || '').trim(),
|
|
runId: String(session && (session.runId || session.run_id) || '').trim(),
|
|
status: String(session && session.status || '').trim(),
|
|
usage: session && session.usage && typeof session.usage === 'object' ? session.usage : null,
|
|
preview: String(session && session.preview || '').trim(),
|
|
messages: Array.isArray(session && session.messages) ? session.messages.slice(-300) : []
|
|
};
|
|
})
|
|
.sort(function(a, b) {
|
|
return Number(b.updatedAt || 0) - Number(a.updatedAt || 0);
|
|
});
|
|
}
|
|
|
|
function pageAiNormalizeBackendSessionRow(row) {
|
|
if (!row || typeof row !== 'object') return null;
|
|
var payload = row.payload && typeof row.payload === 'object' ? row.payload : {};
|
|
var sessionId = String(row.sessionId || row.session_id || '').trim();
|
|
if (!sessionId) return null;
|
|
var title = String(row.title || payload.title || payload.message || '').trim();
|
|
if (title.length > 28) title = title.slice(0, 28) + '…';
|
|
var persistence = String(row.persistence || payload.persistence || '').trim();
|
|
var sessionStorage = String(row.sessionStorage || row.session_storage || payload.sessionStorage || '').trim();
|
|
var status = String(row.status || (row.runtime && row.runtime.status) || '').trim();
|
|
var hasConversationSignal = String(payload.message || payload.input || row.snippet || '').trim()
|
|
|| pageAiNormalizeArray(row.messages).length > 0;
|
|
if (status === 'session.created' && !hasConversationSignal) return null;
|
|
var agentId = pageAiNormalizeAgentId(row.agentId || row.agent_id || payload.agentId || payload.agent_id);
|
|
var profileId = String(row.profileId || row.profile_id || payload.profileId || payload.profile_id || '').trim();
|
|
var profile = String(row.profile || payload.profile || pageAiRunProfile()).trim() || 'default';
|
|
if (!profileId && agentId !== 'reasonix') profileId = profile;
|
|
if (agentId === 'chat_only') {
|
|
profileId = pageAiNormalizeChatOnlyProfileId(profileId || profile);
|
|
profile = profileId;
|
|
}
|
|
if (!sessionStorage && persistence === 'convex_acp_runtime_store') sessionStorage = 'cloud';
|
|
if (!sessionStorage && persistence === 'local_ai_session_jsonl') sessionStorage = String(row.shareId || payload.shareId || '').trim() ? 'local_shared' : 'local_private';
|
|
return {
|
|
id: sessionId,
|
|
title: title || '当前页问答',
|
|
agentId: agentId,
|
|
profileId: profileId,
|
|
profile: profile,
|
|
acpRuntime: String(row.acpRuntime || row.acp_runtime || payload.acpRuntime || 'reasonix').trim(),
|
|
createdAt: pageAiTimestamp(row.createdAt || row.created_at),
|
|
updatedAt: pageAiTimestamp(row.updatedAt || row.updated_at),
|
|
source: sessionStorage === 'local_private' || sessionStorage === 'local_shared' ? 'local' : 'acp',
|
|
persistence: persistence,
|
|
sessionStorage: sessionStorage,
|
|
permissionLevel: String(row.permissionLevel || row.permission_level || payload.permissionLevel || '').trim(),
|
|
shareId: String(row.shareId || row.share_id || payload.shareId || '').trim(),
|
|
acpSessionId: String(row.acpSessionId || row.acp_session_id || payload.acpSessionId || payload.acp_session_id || '').trim(),
|
|
runId: String(row.runId || row.run_id || '').trim(),
|
|
status: status,
|
|
usage: row.usage && typeof row.usage === 'object' ? row.usage : null,
|
|
preview: String(payload.message || row.snippet || '').trim(),
|
|
messages: []
|
|
};
|
|
}
|
|
|
|
function pageAiMergeSessions(localSessions, backendSessions) {
|
|
var byId = {};
|
|
pageAiNormalizeSessions(localSessions).forEach(function(session) {
|
|
byId[session.id] = session;
|
|
});
|
|
pageAiNormalizeSessions(backendSessions).forEach(function(session) {
|
|
var existing = byId[session.id];
|
|
byId[session.id] = Object.assign({}, existing || {}, session, {
|
|
messages: session.messages.length ? session.messages : (existing && existing.messages || [])
|
|
});
|
|
});
|
|
return pageAiNormalizeSessions(Object.keys(byId).map(function(id) { return byId[id]; }));
|
|
}
|
|
|
|
function pageAiDedupeSessions(sessions) {
|
|
var byId = {};
|
|
var ordered = [];
|
|
pageAiNormalizeSessions(sessions).forEach(function(session) {
|
|
var id = String(session && session.id || '').trim();
|
|
if (!id || byId[id]) return;
|
|
byId[id] = true;
|
|
ordered.push(session);
|
|
});
|
|
return ordered;
|
|
}
|
|
|
|
function pageAiSessionStorageLabel(session) {
|
|
var storage = String(session && (session.sessionStorage || session.session_storage) || '').trim();
|
|
var persistence = String(session && session.persistence || '').trim();
|
|
if (storage === 'local_shared') return '共享会话';
|
|
if (storage === 'local_private') return '本地私有';
|
|
if (storage === 'sqlite_control_plane' || persistence === 'sqlite_acp_runtime_store') return '账号会话';
|
|
if (storage === 'cloud' || persistence === 'convex_acp_runtime_store') return '云端会话';
|
|
if (persistence === 'local_ai_session_jsonl') return '本地私有';
|
|
return String(session && session.source || '').trim() === 'acp' ? '云端会话' : '本地私有';
|
|
}
|
|
|
|
function pageAiLoadSessions() {
|
|
if (pageUiState.pageAiSessions.length && pageUiState.pageAiActiveSessionId) return;
|
|
try {
|
|
var raw = win.localStorage.getItem(pageAiStorageKey());
|
|
var parsed = raw ? JSON.parse(raw) : null;
|
|
var activeProfile = String(parsed && parsed.activeProfileName || '').trim();
|
|
var activeAcpRuntime = String(parsed && parsed.activeAcpRuntime || '').trim();
|
|
var storageVersion = Number(parsed && parsed.version || 0);
|
|
if (storageVersion >= sessionStorageVersion && activeAcpRuntime) pageUiState.pageAiAcpRuntime = activeAcpRuntime;
|
|
if (activeProfile) pageAiSetActiveProfile(activeProfile);
|
|
} catch (_) {}
|
|
var fresh = pageAiNewSession();
|
|
pageUiState.pageAiSessions = [fresh];
|
|
pageUiState.pageAiActiveSessionId = fresh.id;
|
|
pageUiState.pageAiMessages = [];
|
|
}
|
|
|
|
async function pageAiLoadBackendSessions() {
|
|
var response = await fetch('/api/hermes/client/sessions?' + pageAiBackendSessionQuery({ limit: 50 }), {
|
|
headers: { 'accept': 'application/json' }
|
|
});
|
|
var payload = await response.json().catch(function(){ return null; });
|
|
if (!response.ok || !payload || payload.ok !== true) {
|
|
throw new Error(pageAiErrorMessage(payload, 'session_list_failed_' + response.status));
|
|
}
|
|
var backendSessions = pageAiNormalizeArray(payload.sessions).map(pageAiNormalizeBackendSessionRow).filter(Boolean);
|
|
var draftSessions = pageAiNormalizeArray(pageUiState.pageAiSessions).filter(function(session) {
|
|
var id = String(session && session.id || '').trim();
|
|
return id && !id.startsWith('mnote_')
|
|
&& (id === pageUiState.pageAiActiveSessionId || (Array.isArray(session.messages) && session.messages.length > 0));
|
|
});
|
|
if (!backendSessions.length) {
|
|
if (!draftSessions.length && !pageUiState.pageAiActiveSessionId) {
|
|
var fresh = pageAiNewSession();
|
|
pageUiState.pageAiSessions = [fresh];
|
|
pageUiState.pageAiActiveSessionId = fresh.id;
|
|
pageUiState.pageAiMessages = [];
|
|
}
|
|
pageUiState.pageAiSessionError = '';
|
|
renderPageAiConversation();
|
|
renderPageAiControls();
|
|
return [];
|
|
}
|
|
pageUiState.pageAiSessions = pageAiDedupeSessions(backendSessions.concat(draftSessions));
|
|
if (!pageUiState.pageAiActiveSessionId || !pageUiState.pageAiSessions.find(function(session) { return session.id === pageUiState.pageAiActiveSessionId; })) {
|
|
pageUiState.pageAiActiveSessionId = pageUiState.pageAiSessions[0].id;
|
|
}
|
|
var active = pageAiCurrentSession();
|
|
if (active) {
|
|
if (active.profile) pageAiSetActiveProfile(active.profile);
|
|
if (!pageUiState.pageAiAcpRuntime && active.acpRuntime) pageUiState.pageAiAcpRuntime = active.acpRuntime;
|
|
pageUiState.pageAiMessages = Array.isArray(active.messages) ? active.messages.slice() : pageUiState.pageAiMessages;
|
|
}
|
|
pageUiState.pageAiSessionError = '';
|
|
pageAiPersistSessions();
|
|
renderPageAiConversation();
|
|
renderPageAiControls();
|
|
return backendSessions;
|
|
}
|
|
|
|
function pageAiMessageFromRuntimeEvent(event) {
|
|
if (!event || typeof event !== 'object') return null;
|
|
var eventType = String(event.eventType || event.event_type || event.event || '').trim();
|
|
var payload = event.payload && typeof event.payload === 'object' ? event.payload : event;
|
|
if (eventType === 'message.delta') {
|
|
var delta = String(payload.delta || payload.text || payload.output_text || '');
|
|
return delta ? { role: 'assistant', content: delta } : null;
|
|
}
|
|
if (eventType === 'thought.delta') {
|
|
var thought = String(payload.delta || payload.text || '').trim();
|
|
return thought ? { role: 'assistant', kind: 'thought', content: thought } : null;
|
|
}
|
|
if (eventType === 'tool.started' || eventType === 'tool.completed' || eventType === 'tool.failed') {
|
|
var toolName = String(payload.toolName || payload.tool || payload.name || eventType).trim();
|
|
var rawLocations = payload.locations;
|
|
return {
|
|
role: 'tool',
|
|
content: toolName,
|
|
toolName: toolName,
|
|
toolCallId: String(payload.toolCallId || payload.tool_call_id || payload.id || ''),
|
|
toolKind: String(payload.kind || ''),
|
|
status: eventType === 'tool.completed' ? 'completed' : (eventType === 'tool.failed' ? 'failed' : 'running'),
|
|
argsSummary: pageAiPreviewValue(payload.args || payload.arguments || payload.input),
|
|
resultSummary: pageAiPreviewValue(payload.summary || payload.result || payload.output || payload.error),
|
|
locations: Array.isArray(rawLocations) ? rawLocations.filter(function(l) { return typeof l === 'string' || (typeof l === 'object' && l && l.path); }).map(function(l) { return typeof l === 'string' ? l : l.path; }) : [],
|
|
traceId: String(payload.traceId || payload.trace_id || ''),
|
|
auditId: String(payload.auditId || payload.audit_id || '')
|
|
};
|
|
}
|
|
if (eventType === 'permission.requested' || eventType === 'permission.denied' || eventType === 'permission.allowed') {
|
|
return pageAiPermissionMessage(payload, eventType);
|
|
}
|
|
if (eventType === 'run.completed') {
|
|
var output = String(payload.output || payload.text || '').trim();
|
|
return output ? { role: 'assistant', content: output } : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function pageAiApplyBackendSessionDetail(payload) {
|
|
var sessionPayload = payload && payload.session ? payload.session : {};
|
|
var runs = pageAiNormalizeArray(sessionPayload.runs);
|
|
var latest = runs.length ? pageAiNormalizeBackendSessionRow(runs[0]) : null;
|
|
var events = pageAiNormalizeArray(payload && payload.events);
|
|
var storedMessages = pageAiNormalizeArray(sessionPayload.messages).map(function(message) {
|
|
return {
|
|
role: message.role === 'user' ? 'user' : (message.role === 'tool' ? 'tool' : 'assistant'),
|
|
content: String(message.content || '')
|
|
};
|
|
}).filter(function(message) { return message.content; });
|
|
var eventsByRunId = {};
|
|
events.forEach(function(event) {
|
|
var runId = String(event && (event.runId || event.run_id) || '').trim();
|
|
if (!runId) return;
|
|
if (!eventsByRunId[runId]) eventsByRunId[runId] = [];
|
|
eventsByRunId[runId].push(event);
|
|
});
|
|
var messages = [];
|
|
if (runs.length) {
|
|
runs.slice().reverse().forEach(function(run) {
|
|
var runPayload = run && run.payload && typeof run.payload === 'object' ? run.payload : {};
|
|
var userMessage = String(runPayload.message || runPayload.input || '').trim();
|
|
if (userMessage) messages.push({ role: 'user', content: userMessage });
|
|
var runId = String(run && (run.runId || run.run_id) || '').trim();
|
|
var assistantDelta = '';
|
|
var completedOutput = '';
|
|
function flushAssistantDelta() {
|
|
var content = assistantDelta.trim();
|
|
if (content) messages.push({ role: 'assistant', content: content });
|
|
assistantDelta = '';
|
|
}
|
|
pageAiNormalizeArray(eventsByRunId[runId]).forEach(function(event) {
|
|
var message = pageAiMessageFromRuntimeEvent(event);
|
|
if (!message) return;
|
|
if (message.role === 'assistant' && !message.kind) {
|
|
var eventType = String(event && (event.eventType || event.event_type || event.event) || '').trim();
|
|
if (eventType === 'run.completed') {
|
|
completedOutput = String(message.content || '').trim();
|
|
return;
|
|
}
|
|
assistantDelta += String(message.content || '');
|
|
return;
|
|
}
|
|
flushAssistantDelta();
|
|
messages.push(message);
|
|
});
|
|
flushAssistantDelta();
|
|
if (completedOutput && !messages.some(function(message) {
|
|
return message.role === 'assistant' && String(message.content || '').trim() === completedOutput;
|
|
})) {
|
|
messages.push({ role: 'assistant', content: completedOutput });
|
|
}
|
|
});
|
|
}
|
|
if (!messages.length) messages = storedMessages;
|
|
var acpSessionId = '';
|
|
events.forEach(function(event) {
|
|
var eventType = String(event && (event.eventType || event.event_type || event.event) || '').trim();
|
|
var payload = event && event.payload && typeof event.payload === 'object' ? event.payload : event;
|
|
if (eventType === 'session.info.updated') {
|
|
var nextAcpSessionId = String(payload && (payload.acpSessionId || payload.acp_session_id) || '').trim();
|
|
if (nextAcpSessionId) acpSessionId = nextAcpSessionId;
|
|
}
|
|
});
|
|
var current = pageAiCurrentSession();
|
|
if (latest && current) {
|
|
Object.assign(current, latest);
|
|
}
|
|
if (current) {
|
|
current.messages = messages.slice(-300);
|
|
current.updatedAt = Math.max(Number(current.updatedAt || 0), Date.now());
|
|
if (acpSessionId) current.acpSessionId = acpSessionId;
|
|
if (latest && latest.usage) current.usage = latest.usage;
|
|
}
|
|
pageAiApplyRuntimeState(payload && payload.runtime);
|
|
pageUiState.pageAiMessages = messages.slice(-300);
|
|
pageAiPersistSessions();
|
|
}
|
|
|
|
async function pageAiLoadBackendSessionDetail(sessionId) {
|
|
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
|
|
if (!sessionId) return null;
|
|
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '?' + pageAiBackendSessionQuery({ limit: 200 }), {
|
|
headers: { 'accept': 'application/json' }
|
|
});
|
|
var payload = await response.json().catch(function(){ return null; });
|
|
if (!response.ok || !payload || payload.ok !== true) {
|
|
throw new Error(pageAiErrorMessage(payload, 'session_detail_failed_' + response.status));
|
|
}
|
|
pageAiApplyBackendSessionDetail(payload);
|
|
renderPageAiConversation();
|
|
renderPageAiControls();
|
|
return payload;
|
|
}
|
|
|
|
async function pageAiSearchBackendSessions(query) {
|
|
var q = String(query || '').trim();
|
|
pageUiState.pageAiSessionSearchQuery = q;
|
|
if (!q) {
|
|
pageUiState.pageAiSessionSearchResults = [];
|
|
renderPageAiConversation();
|
|
return [];
|
|
}
|
|
var response = await fetch('/api/hermes/client/sessions/search?' + pageAiBackendSessionQuery({ q: q, limit: 20 }), {
|
|
headers: { 'accept': 'application/json' }
|
|
});
|
|
var payload = await response.json().catch(function(){ return null; });
|
|
if (!response.ok || !payload || payload.ok !== true) {
|
|
throw new Error(pageAiErrorMessage(payload, 'session_search_failed_' + response.status));
|
|
}
|
|
pageUiState.pageAiSessionSearchResults = pageAiNormalizeArray(payload.results).map(function(row) {
|
|
var normalized = pageAiNormalizeBackendSessionRow(row) || {};
|
|
normalized.snippet = String(row && row.snippet || normalized.preview || '').trim();
|
|
return normalized;
|
|
}).filter(function(row) { return row.id; });
|
|
renderPageAiConversation();
|
|
return pageUiState.pageAiSessionSearchResults;
|
|
}
|
|
|
|
function pageAiPersistSessions() {
|
|
doc.documentElement.setAttribute('data-mnote-page-ai-session-owner', 'hermes');
|
|
doc.documentElement.setAttribute('data-mnote-page-ai-session-key', pageAiStorageKey());
|
|
try {
|
|
pageAiSyncCurrentSessionMessages();
|
|
win.localStorage.setItem(pageAiStorageKey(), JSON.stringify({
|
|
version: sessionStorageVersion,
|
|
activeSessionId: pageUiState.pageAiActiveSessionId,
|
|
activeProfileName: pageAiCurrentProfile(),
|
|
activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix'
|
|
}));
|
|
} catch (_) {}
|
|
}
|
|
|
|
async function pageAiEnsureHermesSession(forceCreate) {
|
|
pageAiLoadSessions();
|
|
var current = pageAiCurrentSession();
|
|
var runProfile = pageAiRunProfile();
|
|
if (!forceCreate && current && String(current.id || '').startsWith('mnote_') && current.profile === runProfile) return current;
|
|
var response = await fetch('/api/hermes/client/sessions', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({
|
|
workspaceId: resolveWorkspaceId(doc.body),
|
|
documentId: currentDocumentId(),
|
|
sourceKind: currentSourceKind(),
|
|
rootUri: currentRootUri(),
|
|
traceId: 'page-ai-' + Date.now().toString(36),
|
|
profile: runProfile,
|
|
agentId: pageAiCurrentAgentId(),
|
|
profileId: pageUiState.pageAiAcpRuntime === 'hermes' ? runProfile : '',
|
|
title: current && current.title ? current.title : '当前页问答'
|
|
})
|
|
});
|
|
var payload = await response.json().catch(function(){ return null; });
|
|
if (!response.ok || !payload || payload.ok !== true) {
|
|
throw new Error(pageAiErrorMessage(payload, 'hermes_session_failed_' + response.status));
|
|
}
|
|
var session = {
|
|
id: String(payload.sessionId || '').trim(),
|
|
title: String(payload.title || '当前页问答'),
|
|
agentId: pageAiCurrentAgentId(),
|
|
profileId: pageUiState.pageAiAcpRuntime === 'hermes' ? runProfile : '',
|
|
profile: String(payload.profile || runProfile).trim() || 'default',
|
|
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
|
|
persistence: String(payload.persistence || '').trim(),
|
|
sessionStorage: String(payload.sessionStorage || '').trim(),
|
|
permissionLevel: String(payload.permissionLevel || '').trim(),
|
|
shareId: String(payload.shareId || '').trim(),
|
|
acpSessionId: String(payload.acpSessionId || payload.acp_session_id || '').trim(),
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
messages: pageUiState.pageAiMessages.slice()
|
|
};
|
|
var previousSessionId = String(current && current.id || '').trim();
|
|
var retainedSessions = pageAiNormalizeArray(pageUiState.pageAiSessions).filter(function(item) {
|
|
var itemId = String(item && item.id || '').trim();
|
|
return itemId && itemId !== session.id && itemId !== previousSessionId;
|
|
});
|
|
pageUiState.pageAiSessions = pageAiNormalizeSessions([session].concat(retainedSessions));
|
|
pageUiState.pageAiActiveSessionId = session.id;
|
|
pageAiPersistSessions();
|
|
renderPageAiConversation();
|
|
return session;
|
|
}
|
|
|
|
async function pageAiRestoreHermesSession() {
|
|
var current = pageAiCurrentSession();
|
|
if (!current || !String(current.id || '').startsWith('mnote_')) return;
|
|
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(current.id) + '/resume?' + pageAiBackendSessionQuery({}), {
|
|
method: 'POST',
|
|
headers: { 'accept': 'application/json' }
|
|
});
|
|
if (!response.ok) return;
|
|
var payload = await response.json().catch(function(){ return null; });
|
|
if (payload && payload.persistence === 'convex_acp_runtime_store') {
|
|
pageAiApplyBackendSessionDetail(payload);
|
|
renderPageAiConversation();
|
|
renderPageAiControls();
|
|
return;
|
|
}
|
|
var session = payload && (payload.session || (payload.upstream && payload.upstream.session) || payload);
|
|
var messages = session && Array.isArray(session.messages) ? session.messages : [];
|
|
pageAiApplyRuntimeState(payload && payload.runtime);
|
|
if (session && (session.profile || session.profileName)) {
|
|
current.profile = String(session.profile || session.profileName || current.profile || pageAiCurrentProfile());
|
|
}
|
|
if (!messages.length) {
|
|
renderPageAiControls();
|
|
return;
|
|
}
|
|
pageUiState.pageAiMessages = messages.slice(-300).map(function(message) {
|
|
return {
|
|
role: message.role === 'user' ? 'user' : (message.role === 'tool' ? 'tool' : 'assistant'),
|
|
content: String(message.content || '')
|
|
};
|
|
});
|
|
current.messages = pageUiState.pageAiMessages.slice();
|
|
current.updatedAt = Date.now();
|
|
renderPageAiConversation();
|
|
renderPageAiControls();
|
|
}
|
|
|
|
function pageAiCurrentSession() {
|
|
return pageUiState.pageAiSessions.find(function(session) {
|
|
return session.id === pageUiState.pageAiActiveSessionId;
|
|
}) || null;
|
|
}
|
|
|
|
function pageAiSelectedSessionIds() {
|
|
if (!pageUiState.pageAiSelectedSessionIds || typeof pageUiState.pageAiSelectedSessionIds !== 'object') {
|
|
pageUiState.pageAiSelectedSessionIds = {};
|
|
}
|
|
return pageUiState.pageAiSelectedSessionIds;
|
|
}
|
|
|
|
function pageAiSessionSelected(sessionId) {
|
|
sessionId = String(sessionId || '').trim();
|
|
return Boolean(sessionId && pageAiSelectedSessionIds()[sessionId]);
|
|
}
|
|
|
|
function pageAiSelectedSessionList() {
|
|
var selected = pageAiSelectedSessionIds();
|
|
return Object.keys(selected).filter(function(sessionId) {
|
|
return selected[sessionId] === true;
|
|
});
|
|
}
|
|
|
|
function pageAiSelectedSessionCount() {
|
|
return pageAiSelectedSessionList().length;
|
|
}
|
|
|
|
function pageAiToggleSessionSelection(sessionId, selected) {
|
|
sessionId = String(sessionId || '').trim();
|
|
if (!sessionId) return;
|
|
var selectedMap = Object.assign({}, pageAiSelectedSessionIds());
|
|
if (selected === false) {
|
|
delete selectedMap[sessionId];
|
|
} else {
|
|
selectedMap[sessionId] = true;
|
|
}
|
|
pageUiState.pageAiSelectedSessionIds = selectedMap;
|
|
renderPageAiConversation();
|
|
renderPageAiControls();
|
|
}
|
|
|
|
function pageAiClearSessionSelection() {
|
|
pageUiState.pageAiSelectedSessionIds = {};
|
|
renderPageAiConversation();
|
|
renderPageAiControls();
|
|
}
|
|
|
|
function pageAiSyncCurrentSessionMessages() {
|
|
var session = pageAiCurrentSession();
|
|
if (!session) return;
|
|
var runProfile = pageAiRunProfile();
|
|
session.messages = Array.isArray(pageUiState.pageAiMessages) ? pageUiState.pageAiMessages.slice(-300) : [];
|
|
session.agentId = pageAiCurrentAgentId();
|
|
session.profileId = pageUiState.pageAiAcpRuntime === 'hermes' ? runProfile : '';
|
|
session.profile = runProfile;
|
|
session.acpRuntime = pageUiState.pageAiAcpRuntime || 'reasonix';
|
|
session.updatedAt = Date.now();
|
|
}
|
|
|
|
function pageAiSetActiveSession(sessionId) {
|
|
var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
|
|
if (!session) return;
|
|
pageUiState.pageAiActiveSessionId = session.id;
|
|
if (session.agentId) pageUiState.pageAiAgentId = pageAiNormalizeAgentId(session.agentId);
|
|
if (session.acpRuntime) pageUiState.pageAiAcpRuntime = session.acpRuntime;
|
|
if (session.profileId || session.profile) pageAiSetActiveProfile(session.profileId || session.profile);
|
|
pageUiState.pageAiMessages = Array.isArray(session.messages) ? session.messages.slice() : [];
|
|
pageUiState.pageAiPage = 'chat';
|
|
pageAiPersistSessions();
|
|
renderPageAiControls();
|
|
renderPageAiConversation();
|
|
if (session.source === 'acp' || String(session.id || '').startsWith('mnote_')) {
|
|
void pageAiLoadBackendSessionDetail(session.id).catch(function(error) {
|
|
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
|
|
renderPageAiControls();
|
|
});
|
|
}
|
|
}
|
|
|
|
function pageAiStartNewSession() {
|
|
var session = pageAiNewSession();
|
|
pageUiState.pageAiSessions = pageAiNormalizeSessions([session].concat(pageUiState.pageAiSessions));
|
|
pageUiState.pageAiActiveSessionId = session.id;
|
|
pageUiState.pageAiMessages = [];
|
|
pageUiState.pageAiPage = 'chat';
|
|
pageAiPersistSessions();
|
|
renderPageAiControls();
|
|
renderPageAiConversation();
|
|
}
|
|
|
|
async function pageAiRenameBackendSession(sessionId) {
|
|
var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
|
|
if (!session) return;
|
|
var title = win.prompt('重命名 AI 会话', session.title || '当前页问答');
|
|
if (title === null) return;
|
|
title = String(title || '').trim();
|
|
if (!title) return;
|
|
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/rename?' + pageAiBackendSessionQuery({}), {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json', 'accept': 'application/json' },
|
|
body: JSON.stringify({ title: title })
|
|
});
|
|
var payload = await response.json().catch(function(){ return null; });
|
|
if (!response.ok || !payload || payload.ok !== true) {
|
|
throw new Error(pageAiErrorMessage(payload, 'session_rename_failed_' + response.status));
|
|
}
|
|
session.title = String((payload.result && payload.result.title) || title);
|
|
session.updatedAt = Date.now();
|
|
pageAiPersistSessions();
|
|
renderPageAiConversation();
|
|
renderPageAiControls();
|
|
}
|
|
|
|
function pageAiSessionRequiresBackendDelete(session) {
|
|
var id = String(session && session.id || '').trim();
|
|
var source = String(session && session.source || '').trim();
|
|
return Boolean(id && (
|
|
id.startsWith('mnote_')
|
|
|| source === 'acp'
|
|
|| String(session && session.persistence || '').trim()
|
|
|| String(session && (session.sessionStorage || session.session_storage) || '').trim()
|
|
));
|
|
}
|
|
|
|
function pageAiFindSessionById(sessionId) {
|
|
sessionId = String(sessionId || '').trim();
|
|
if (!sessionId) return null;
|
|
return pageAiNormalizeArray(pageUiState.pageAiSessions).find(function(item) {
|
|
return String(item && item.id || '').trim() === sessionId;
|
|
}) || pageAiNormalizeArray(pageUiState.pageAiSessionSearchResults).find(function(item) {
|
|
return String(item && item.id || '').trim() === sessionId;
|
|
}) || null;
|
|
}
|
|
|
|
function pageAiApplyDeletedSessionIds(sessionIds) {
|
|
var deleted = {};
|
|
pageAiNormalizeArray(sessionIds).forEach(function(sessionId) {
|
|
sessionId = String(sessionId || '').trim();
|
|
if (sessionId) deleted[sessionId] = true;
|
|
});
|
|
pageUiState.pageAiSessions = pageAiNormalizeArray(pageUiState.pageAiSessions).filter(function(item) {
|
|
return !deleted[String(item && item.id || '').trim()];
|
|
});
|
|
pageUiState.pageAiSessionSearchResults = pageAiNormalizeArray(pageUiState.pageAiSessionSearchResults).filter(function(item) {
|
|
return !deleted[String(item && item.id || '').trim()];
|
|
});
|
|
var selected = Object.assign({}, pageAiSelectedSessionIds());
|
|
Object.keys(deleted).forEach(function(sessionId) { delete selected[sessionId]; });
|
|
pageUiState.pageAiSelectedSessionIds = selected;
|
|
if (deleted[pageUiState.pageAiActiveSessionId]) {
|
|
var next = pageUiState.pageAiSessions[0] || pageAiNewSession();
|
|
if (!pageUiState.pageAiSessions.length) pageUiState.pageAiSessions = [next];
|
|
pageUiState.pageAiActiveSessionId = next.id;
|
|
pageUiState.pageAiMessages = Array.isArray(next.messages) ? next.messages.slice() : [];
|
|
}
|
|
}
|
|
|
|
async function pageAiDeleteBackendSessions(sessionIds) {
|
|
var ids = pageAiNormalizeArray(sessionIds).map(function(sessionId) {
|
|
return String(sessionId || '').trim();
|
|
}).filter(Boolean);
|
|
if (!ids.length) return;
|
|
var sessions = ids.map(pageAiFindSessionById).filter(Boolean);
|
|
if (!sessions.length) return;
|
|
var confirmText = sessions.length === 1
|
|
? '确定删除 AI 会话“' + (sessions[0].title || sessions[0].id) + '”吗?'
|
|
: '确定删除选中的 ' + String(sessions.length) + ' 个 AI 会话吗?';
|
|
if (!win.confirm(confirmText)) return;
|
|
for (var index = 0; index < sessions.length; index += 1) {
|
|
var session = sessions[index];
|
|
if (!pageAiSessionRequiresBackendDelete(session)) continue;
|
|
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(session.id) + '?' + pageAiBackendSessionQuery({}), {
|
|
method: 'DELETE',
|
|
headers: { 'accept': 'application/json' }
|
|
});
|
|
var payload = await response.json().catch(function(){ return null; });
|
|
if (!response.ok || !payload || payload.ok !== true) {
|
|
throw new Error(pageAiErrorMessage(payload, 'session_delete_failed_' + response.status));
|
|
}
|
|
}
|
|
pageAiApplyDeletedSessionIds(sessions.map(function(session) { return session.id; }));
|
|
pageAiPersistSessions();
|
|
renderPageAiConversation();
|
|
renderPageAiControls();
|
|
}
|
|
|
|
async function pageAiDeleteBackendSession(sessionId) {
|
|
return pageAiDeleteBackendSessions([sessionId]);
|
|
}
|
|
|
|
async function pageAiDeleteSelectedBackendSessions() {
|
|
return pageAiDeleteBackendSessions(pageAiSelectedSessionList());
|
|
}
|
|
|
|
async function pageAiResumeBackendSession(sessionId) {
|
|
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
|
|
if (!sessionId) return;
|
|
pageAiSetActiveSession(sessionId);
|
|
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/resume?' + pageAiBackendSessionQuery({}), {
|
|
method: 'POST',
|
|
headers: { 'accept': 'application/json' }
|
|
});
|
|
var payload = await response.json().catch(function(){ return null; });
|
|
if (!response.ok || !payload || payload.ok !== true) {
|
|
throw new Error(pageAiErrorMessage(payload, 'session_resume_failed_' + response.status));
|
|
}
|
|
pageAiApplyBackendSessionDetail(payload);
|
|
pageUiState.pageAiPage = 'chat';
|
|
renderPageAiConversation();
|
|
renderPageAiControls();
|
|
}
|
|
|
|
async function pageAiCheckActiveRun(sessionId) {
|
|
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
|
|
if (!sessionId) return null;
|
|
var response = await fetch('/api/page-ai/sessions/' + encodeURIComponent(sessionId) + '/active-run?' + pageAiBackendSessionQuery({}), {
|
|
headers: { 'accept': 'application/json' }
|
|
});
|
|
var payload = await response.json().catch(function(){ return null; });
|
|
if (!response.ok || !payload || payload.ok !== true) {
|
|
throw new Error(pageAiErrorMessage(payload, 'active_run_failed_' + response.status));
|
|
}
|
|
if (!payload.active || !payload.run) return null;
|
|
var run = payload.run;
|
|
var hostRunId = String(run.hostRunId || run.runId || '').trim();
|
|
var status = String(run.status || '').trim();
|
|
var current = pageAiCurrentSession();
|
|
if (current && current.id === sessionId) {
|
|
current.runId = hostRunId;
|
|
current.status = status || current.status || 'running';
|
|
current.updatedAt = Date.now();
|
|
}
|
|
pageAiApplyRuntimeState(Object.assign({}, run.runtime || {}, {
|
|
status: status || 'running',
|
|
runId: hostRunId
|
|
}));
|
|
doc.documentElement.setAttribute('data-mnote-page-ai-active-run-checked', 'true');
|
|
if (hostRunId) doc.documentElement.setAttribute('data-mnote-page-ai-active-host-run-id', hostRunId);
|
|
pageAiPersistSessions();
|
|
renderPageAiControls();
|
|
return run;
|
|
}
|
|
|
|
return {
|
|
pageAiStorageKey,
|
|
pageAiBackendSessionQuery,
|
|
pageAiNewSession,
|
|
pageAiNormalizeSessions,
|
|
pageAiNormalizeBackendSessionRow,
|
|
pageAiMergeSessions,
|
|
pageAiSessionStorageLabel,
|
|
pageAiLoadSessions,
|
|
pageAiLoadBackendSessions,
|
|
pageAiMessageFromRuntimeEvent,
|
|
pageAiApplyBackendSessionDetail,
|
|
pageAiLoadBackendSessionDetail,
|
|
pageAiSearchBackendSessions,
|
|
pageAiPersistSessions,
|
|
pageAiEnsureHermesSession,
|
|
pageAiRestoreHermesSession,
|
|
pageAiCurrentSession,
|
|
pageAiSessionSelected,
|
|
pageAiToggleSessionSelection,
|
|
pageAiClearSessionSelection,
|
|
pageAiSelectedSessionCount,
|
|
pageAiSyncCurrentSessionMessages,
|
|
pageAiSetActiveSession,
|
|
pageAiStartNewSession,
|
|
pageAiRenameBackendSession,
|
|
pageAiDeleteBackendSession,
|
|
pageAiDeleteSelectedBackendSessions,
|
|
pageAiResumeBackendSession,
|
|
pageAiCheckActiveRun
|
|
};
|
|
}
|