feat(ai): switch page ai to hermes panel

This commit is contained in:
lix-2026
2026-05-14 15:10:33 +08:00
parent e9188716e6
commit 9816035491
48 changed files with 6353 additions and 412 deletions
+176 -74
View File
@@ -3014,7 +3014,7 @@ const SIDEBAR_TREE_JS: &str = r##"
}
function pageAiStorageKey() {
return 'doc_ai_sessions:' + currentDocumentId();
return 'hermes_page_ai_session:' + currentDocumentId();
}
function pageAiNewSession(title) {
@@ -3046,46 +3046,90 @@ const SIDEBAR_TREE_JS: &str = r##"
}
function pageAiLoadSessions() {
if (pageUiState.pageAiSessions.length && pageUiState.pageAiActiveSessionId) return;
try {
var raw = window.localStorage.getItem(pageAiStorageKey());
if (!raw) {
var fresh = pageAiNewSession();
pageUiState.pageAiSessions = [fresh];
pageUiState.pageAiActiveSessionId = fresh.id;
pageUiState.pageAiMessages = [];
return;
}
var parsed = JSON.parse(raw);
var sessions = pageAiNormalizeSessions(parsed && parsed.sessions ? parsed.sessions : []);
if (!sessions.length) {
var fallback = pageAiNewSession();
pageUiState.pageAiSessions = [fallback];
pageUiState.pageAiActiveSessionId = fallback.id;
pageUiState.pageAiMessages = [];
return;
}
pageUiState.pageAiSessions = sessions;
var parsed = raw ? JSON.parse(raw) : null;
var activeId = String(parsed && parsed.activeSessionId || '').trim();
var active = sessions.find(function(session) { return session.id === activeId; }) || sessions[0];
pageUiState.pageAiActiveSessionId = active.id;
pageUiState.pageAiMessages = Array.isArray(active.messages) ? active.messages.slice() : [];
} catch (_) {
var reset = pageAiNewSession();
pageUiState.pageAiSessions = [reset];
pageUiState.pageAiActiveSessionId = reset.id;
pageUiState.pageAiMessages = [];
}
if (activeId) {
pageUiState.pageAiSessions = [pageAiNewSession('当前页问答')];
pageUiState.pageAiSessions[0].id = activeId;
pageUiState.pageAiActiveSessionId = activeId;
pageUiState.pageAiMessages = [];
return;
}
} catch (_) {}
var fresh = pageAiNewSession();
pageUiState.pageAiSessions = [fresh];
pageUiState.pageAiActiveSessionId = fresh.id;
pageUiState.pageAiMessages = [];
}
function pageAiPersistSessions() {
document.documentElement.setAttribute('data-mnote-page-ai-session-owner', 'hermes');
document.documentElement.setAttribute('data-mnote-page-ai-session-key', pageAiStorageKey());
try {
window.localStorage.setItem(pageAiStorageKey(), JSON.stringify({
activeSessionId: pageUiState.pageAiActiveSessionId,
sessions: pageAiNormalizeSessions(pageUiState.pageAiSessions)
activeSessionId: pageUiState.pageAiActiveSessionId
}));
} catch (_) {}
}
async function pageAiEnsureHermesSession() {
pageAiLoadSessions();
var current = pageAiCurrentSession();
if (current && String(current.id || '').startsWith('mnote_')) return current;
var response = await fetch('/api/hermes/client/sessions', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
traceId: 'page-ai-' + Date.now().toString(36),
title: current && current.title ? current.title : '当前页问答'
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
var code = payload && payload.code ? payload.code : 'hermes_session_failed_' + response.status;
throw new Error(code);
}
var session = {
id: String(payload.sessionId || '').trim(),
title: String(payload.title || '当前页问答'),
createdAt: Date.now(),
updatedAt: Date.now(),
messages: pageUiState.pageAiMessages.slice()
};
pageUiState.pageAiSessions = pageAiNormalizeSessions([session]);
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), {
headers: { 'accept': 'application/json' }
});
if (!response.ok) return;
var payload = await response.json().catch(function(){ return null; });
var session = payload && (payload.session || (payload.upstream && payload.upstream.session) || payload);
var messages = session && Array.isArray(session.messages) ? session.messages : [];
if (!messages.length) return;
pageUiState.pageAiMessages = messages.slice(-40).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();
}
function pageAiCurrentSession() {
return pageUiState.pageAiSessions.find(function(session) {
return session.id === pageUiState.pageAiActiveSessionId;
@@ -3146,7 +3190,7 @@ const SIDEBAR_TREE_JS: &str = r##"
var args = normalized && normalized.args ? normalized.args : {};
var documentId = searchText(args.documentId || args.pageId || currentDocumentId());
var toolName = searchText(operation.tool_name || normalized.toolName || 'doc_get');
return providerLabel + ' 已收到你的问题“' + searchText(promptText) + '”。当前已通过 mnote-cli 建立页面级工具调用,目标页面是 ' + (documentId || '当前页面') + ',本次选择的工具是 ' + toolName + '。如果你继续追问,我会沿当前页面上下文继续回复。';
return providerLabel + ' 已收到你的问题“' + searchText(promptText) + '”。当前已通过 Hermes 调用 mnote plugin 工具,目标页面是 ' + (documentId || '当前页面') + ',本次选择的工具是 ' + toolName + '。如果你继续追问,我会沿当前页面上下文继续回复。';
} catch (_) {
return providerLabel + ' 已返回结果,但当前结果是结构化文本,已为你保留原始内容。';
}
@@ -3175,7 +3219,11 @@ const SIDEBAR_TREE_JS: &str = r##"
'<div class="wolai-page-ai-suggestions">' +
'<div class="wolai-page-ai-suggestions-header">' +
'<span>推荐问题</span>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="rotate">换一换</button>' +
'<div class="wolai-page-ai-intents">' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-intent="create-summary">创建 Summary</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-intent="create-ai-note">创建 AI Note</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="rotate">换一换</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-suggestion-list" data-page-ai-suggestion-list></div>' +
'</div>' +
@@ -3187,8 +3235,6 @@ const SIDEBAR_TREE_JS: &str = r##"
'<button type="button" class="wolai-page-ai-tab" data-page-ai-action="history">历史会话</button>' +
'<div class="wolai-page-ai-provider-group">' +
'<button type="button" class="wolai-page-ai-model-chip is-active" data-page-ai-provider="hermes" aria-pressed="true">Hermes</button>' +
'<button type="button" class="wolai-page-ai-model-chip" data-page-ai-provider="codex" aria-pressed="false">Codex</button>' +
'<button type="button" class="wolai-page-ai-model-chip" data-page-ai-provider="claudecode" aria-pressed="false">ClaudeCode</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-input-row">' +
@@ -3240,9 +3286,10 @@ const SIDEBAR_TREE_JS: &str = r##"
return;
}
conversation.innerHTML = pageUiState.pageAiMessages.map(function(item) {
var roleLabel = item.role === 'user' ? '你' : (item.role === 'tool' ? '工具' : 'AI');
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--' + escapeHtml(item.role || 'assistant') + '">' +
'<div class="wolai-page-ai-message-role">' + escapeHtml(item.role === 'user' ? '你' : 'AI') + '</div>' +
'<div class="wolai-page-ai-message-role">' + escapeHtml(roleLabel) + '</div>' +
'<div class="wolai-page-ai-message-text">' + escapeHtml(item.content || '') + '</div>' +
'</div>';
}).join('');
@@ -3258,6 +3305,15 @@ const SIDEBAR_TREE_JS: &str = r##"
drawer.hidden = false;
pageUiState.pageAiOpen = true;
updatePageAiTriggerState();
pageAiEnsureHermesSession().then(function() {
return pageAiRestoreHermesSession();
}).catch(function(error) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: 'Hermes 当前不可用:' + (error instanceof Error ? error.message : String(error))
});
renderPageAiConversation();
});
}
function closePageAiDrawer() {
@@ -3290,7 +3346,14 @@ const SIDEBAR_TREE_JS: &str = r##"
if (line.startsWith('event:')) eventName = line.slice(6).trim();
if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
});
if (eventName) onEvent(eventName, dataLines.join('\n'));
var payloadText = dataLines.join('\n');
if (!eventName && payloadText) {
try {
var parsed = JSON.parse(payloadText);
eventName = parsed && parsed.event ? String(parsed.event) : '';
} catch (_) {}
}
if (eventName) onEvent(eventName, payloadText);
});
}
}
@@ -3299,39 +3362,35 @@ const SIDEBAR_TREE_JS: &str = r##"
if (pageUiState.pageAiBusy) return;
var prompt = searchText(text);
if (!prompt) return;
pageAiLoadSessions();
var contextSnapshot = currentPageAiContextSnapshot();
var aggregate = contextSnapshot.aggregate || {};
var body = contextSnapshot.body || {};
var subtree = contextSnapshot.subtree || null;
var outline = subtree && subtree.outline ? subtree.outline : null;
pageUiState.pageAiBusy = true;
pageUiState.pageAiMessages.push({ role: 'user', content: prompt });
var currentSession = pageAiCurrentSession();
if (currentSession) {
if (currentSession.title === '新会话') {
currentSession.title = prompt.length > 18 ? prompt.slice(0, 18) + '…' : prompt;
}
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
renderPageAiConversation();
var currentSession = null;
try {
var response = await fetch('/api/ai-agent/run', {
await pageAiEnsureHermesSession();
var contextSnapshot = currentPageAiContextSnapshot();
var aggregate = contextSnapshot.aggregate || {};
var body = contextSnapshot.body || {};
var subtree = contextSnapshot.subtree || null;
var outline = subtree && subtree.outline ? subtree.outline : null;
pageUiState.pageAiMessages.push({ role: 'user', content: prompt });
currentSession = pageAiCurrentSession();
if (currentSession) {
if (currentSession.title === '新会话') {
currentSession.title = prompt.length > 18 ? prompt.slice(0, 18) + '…' : prompt;
}
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
renderPageAiConversation();
var response = await fetch('/api/hermes/client/runs', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
stream: true,
maxSteps: 8,
scope: 'document',
messages: [{ role: 'user', content: prompt }],
toolChoice: {
mode: 'auto',
toolSets: ['toolset.readonly', 'toolset.rag_read', 'toolset.docs_read', 'toolset.media_read', 'toolset.doc_read', 'toolset.doc_write', 'toolset.slash_write']
},
context: {
documentId: currentDocumentId(),
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sessionId: pageUiState.pageAiActiveSessionId,
message: prompt,
model: 'hermes-agent',
pageContext: {
documentBlocks: body.content || null,
node: {
documentId: currentDocumentId(),
@@ -3343,23 +3402,54 @@ const SIDEBAR_TREE_JS: &str = r##"
evidence: null,
pageOptions: currentPageOptions()
},
options: {
searxng: true,
ai: { provider: pageUiState.pageAiProvider }
}
selectedBlockId: null,
selectedText: null,
traceId: 'page-ai-run-' + Date.now().toString(36)
})
});
if (!response.ok) {
throw new Error('page_ai_failed_' + response.status);
var errorPayload = await response.json().catch(function(){ return null; });
throw new Error(errorPayload && errorPayload.code ? errorPayload.code : 'page_ai_failed_' + response.status);
}
var runPayload = await response.json().catch(function(){ return null; });
var upstream = runPayload && runPayload.upstream ? runPayload.upstream : runPayload;
var runId = upstream && (upstream.run_id || upstream.runId);
if (!runId) throw new Error('hermes_run_missing_run_id');
var eventResponse = await fetch('/api/hermes/client/events/' + encodeURIComponent(runId), {
headers: { 'accept': 'text/event-stream' }
});
if (!eventResponse.ok) {
var eventError = await eventResponse.json().catch(function(){ return null; });
throw new Error(eventError && eventError.code ? eventError.code : 'hermes_events_failed_' + eventResponse.status);
}
var assistantText = '';
await streamPageAiResponse(response, function(eventName, payloadText) {
if (eventName === 'assistant_message') {
await streamPageAiResponse(eventResponse, function(eventName, payloadText) {
if (eventName === 'assistant_message' || eventName === 'message.delta') {
try {
var payload = JSON.parse(payloadText || 'null');
assistantText = searchText(payload && payload.text);
assistantText += searchText((payload && (payload.text || payload.delta)) || '');
} catch (_) {
assistantText += searchText(payloadText);
}
}
if (eventName === 'run.completed') {
try {
var completed = JSON.parse(payloadText || 'null');
if (completed && completed.output) assistantText = searchText(completed.output);
} catch (_) {}
}
if (eventName === 'tool.started' || eventName === 'tool.completed' || eventName === 'tool.failed') {
try {
var toolEvent = JSON.parse(payloadText || 'null');
pageUiState.pageAiMessages.push({
role: 'tool',
content: String(toolEvent && (toolEvent.name || toolEvent.tool || eventName) || eventName)
});
} catch (_) {
pageUiState.pageAiMessages.push({ role: 'tool', content: eventName });
}
renderPageAiConversation();
}
});
pageUiState.pageAiMessages.push({
role: 'assistant',
@@ -3370,18 +3460,16 @@ const SIDEBAR_TREE_JS: &str = r##"
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
} catch (error) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: pageAiProviderLabel(pageUiState.pageAiProvider) + ' 当前请求失败:' + (error instanceof Error ? error.message : String(error))
content: 'Hermes 当前请求失败:' + (error instanceof Error ? error.message : String(error))
});
currentSession = pageAiCurrentSession();
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
} finally {
pageUiState.pageAiBusy = false;
renderPageAiConversation();
@@ -3937,6 +4025,20 @@ const SIDEBAR_TREE_JS: &str = r##"
return;
}
var pageAiIntent = closestAction(e.target, '[data-page-ai-intent]');
if (pageAiIntent) {
e.preventDefault();
var intentName = pageAiIntent.getAttribute('data-page-ai-intent') || '';
if (intentName === 'create-summary') {
void sendPageAiMessage('请通过 Hermes 调用 mnote.artifact.create_summary,为当前页面创建或更新 AI Summary。');
return;
}
if (intentName === 'create-ai-note') {
void sendPageAiMessage('请通过 Hermes 调用 mnote.artifact.create_ai_note,基于当前页面创建一篇新的 AI Note。');
return;
}
}
var pageAiProvider = closestAction(e.target, '[data-page-ai-provider]');
if (pageAiProvider) {
e.preventDefault();
+7
View File
@@ -2690,6 +2690,13 @@ body {
gap: 8px;
}
.wolai-page-ai-intents {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
}
.wolai-page-ai-suggestion-list,
.wolai-page-ai-toolbar {
display: flex;