Files
mnote/rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js
T

3457 lines
166 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.
export function createSidebarPageAiRuntime(context) {
const {
buildLocalFileOpenUrl,
currentDocumentId,
currentPageAggregate,
currentPageOptions,
currentRootUri,
currentSourceKind,
escapeHtml,
cssEscape,
openLocalResourceInActiveTab,
pageUiState,
resolveWorkspaceId,
searchText,
} = context;
var PAGE_AI_SESSION_STORAGE_VERSION = 3;
// Page AI 请求合同:agentId 只负责路由;contextRefs 是用户勾选的上下文地址;
// allowedRoots 只展示 SQLite directory_grants 解析结果,服务端仍会按当前用户重算;
// runTargetSnapshot 必须来自 OpenEditorsSnapshot,避免运行中 UI 切换导致目标漂移。
var PAGE_AI_AGENT_REGISTRY = [
{ id: 'hermes', label: 'Hermes', acpRuntime: 'hermes', canWriteFiles: true },
{ id: 'reasonix', label: 'Reasonix', acpRuntime: 'reasonix', canWriteFiles: true },
{ id: 'chat_only', label: 'Chat-only', acpRuntime: 'hermes', canWriteFiles: false }
];
var PAGE_AI_CONTEXT_REF_REGISTRY = [
{ id: 'current_page', label: '当前页' },
{ id: 'selection', label: '选区' },
{ id: 'active_editor', label: '打开资源' },
{ id: 'file', label: '文件' },
{ id: 'folder', label: '文件夹' },
{ id: 'changed_files', label: '最近修改' }
];
function textFromUnknown(value) {
if (value == null) return '';
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value);
if (Array.isArray(value)) return value.map(textFromUnknown).filter(Boolean).join(' ');
if (typeof value !== 'object') return '';
var parts = [];
['text', 'title', 'content', 'children', 'blocks', 'body'].forEach(function(key) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
var text = textFromUnknown(value[key]);
if (text) parts.push(text);
}
});
return parts.join(' ');
}
function readLocalEditorBlocks() {
var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
if (!(editor instanceof HTMLElement)) return [];
return Array.from(editor.children).filter(function(node) {
return node instanceof HTMLElement;
}).map(function(node, index) {
var tag = String(node.tagName || '').toUpperCase();
var headingMatch = tag.match(/^H([1-6])$/);
var type = headingMatch ? 'heading' : 'paragraph';
var text = searchText(node.textContent || '');
var id = searchText(node.getAttribute('data-id') || node.id || 'local-block-' + index);
var props = headingMatch ? { level: Number(headingMatch[1]) } : {};
return { id: id, type: type, props: props, content: text };
}).filter(function(block) {
return block.content || block.type === 'heading';
});
}
function buildPageAiLocalSubtree(blocks, title) {
var documentId = currentDocumentId() || 'current-page';
var rootNodeId = 'page:' + documentId;
var headingCounters = [0, 0, 0, 0, 0, 0];
var headingStack = [];
var nodes = [{
id: rootNodeId,
nodeId: rootNodeId,
nodeType: 'page',
blockId: null,
blockType: 'page',
title: title || '',
parentNodeId: null,
headingLevel: null
}];
var outline = [];
var evidence = [];
blocks.forEach(function(block, index) {
var level = block.type === 'heading' ? Math.max(1, Math.min(6, Number(block.props && block.props.level || 1))) : null;
if (level != null) {
while (headingStack.length && headingStack[headingStack.length - 1].level >= level) headingStack.pop();
}
var parentNodeId = headingStack.length ? headingStack[headingStack.length - 1].nodeId : rootNodeId;
var nodeId = 'local-node:' + String(block.id || index);
var titleText = block.content || (block.type === 'heading' ? '未命名标题' : '');
nodes.push({
id: nodeId,
nodeId: nodeId,
nodeType: 'block',
blockId: block.id,
blockType: block.type,
title: titleText,
parentNodeId: parentNodeId,
headingLevel: level
});
if (level != null) {
headingCounters[level - 1] += 1;
for (var i = level; i < headingCounters.length; i += 1) headingCounters[i] = 0;
var numbering = headingCounters.slice(0, level).filter(function(count) { return count > 0; }).join('.');
outline.push({
id: 'local-outline:' + String(block.id || index),
nodeId: nodeId,
anchorBlockId: block.id,
level: level,
title: titleText,
numbering: numbering
});
headingStack.push({ level: level, nodeId: nodeId });
}
if (titleText) {
evidence.push({
id: 'local-evidence:' + String(block.id || index),
nodeId: nodeId,
anchorBlockId: block.id,
text: titleText,
kind: block.type
});
}
});
return {
projectionId: 'local-editor-dom:' + documentId,
rootNode: {
id: rootNodeId,
documentId: documentId,
title: title || '',
nodeType: 'page'
},
subtree: {
rootNodeId: rootNodeId,
nodes: nodes
},
outline: outline,
evidence: evidence,
stats: {
nodeCount: nodes.length,
headingCount: outline.length,
evidenceCount: evidence.length
},
source: 'local'
};
}
function currentPageAiContextSnapshot() {
var aggregate = currentPageAggregate();
var body = aggregate.body || {};
var serverSubtree = aggregate.tree && aggregate.tree.pageSubtree ? aggregate.tree.pageSubtree : null;
return {
aggregate: aggregate,
body: body,
subtree: serverSubtree,
pageSubtreeSource: serverSubtree ? 'server' : 'none'
};
}
function normalizePageAiOpenEditorEntry(entry) {
if (!entry || typeof entry !== 'object') return null;
return {
objectIdentity: String(entry.objectIdentity || '').trim(),
workspacePath: entry.workspacePath && typeof entry.workspacePath === 'object' ? entry.workspacePath : null,
paneRole: String(entry.paneRole || 'primary').trim() === 'secondary' ? 'secondary' : 'primary',
documentId: String(entry.documentId || '').trim(),
workspaceId: String(entry.workspaceId || '').trim(),
title: String(entry.title || '').trim(),
kind: String(entry.kind || entry.editorKind || '').trim(),
editorKind: String(entry.editorKind || entry.kind || '').trim(),
active: entry.active === true,
dirtyState: String(entry.dirtyState || entry.dirtyGuard || '').trim(),
preview: entry.preview === true,
pinned: entry.pinned === true,
lastActiveAt: Number(entry.lastActiveAt || 0) || 0,
assetId: String(entry.assetId || '').trim(),
path: String(entry.path || '').trim()
};
}
function currentPageAiOpenEditorsSnapshot() {
var snapshot = null;
try {
if (window.__mnoteDocumentPaneRuntime && typeof window.__mnoteDocumentPaneRuntime.getOpenEditorsSnapshot === 'function') {
snapshot = window.__mnoteDocumentPaneRuntime.getOpenEditorsSnapshot();
}
} catch (_) {}
if (!snapshot || typeof snapshot !== 'object') snapshot = window.__mnoteOpenEditorsSnapshot || null;
if (!snapshot || typeof snapshot !== 'object') return null;
var editors = Array.isArray(snapshot.editors)
? snapshot.editors.map(normalizePageAiOpenEditorEntry).filter(Boolean)
: [];
var resources = Array.isArray(snapshot.resourceEditors)
? snapshot.resourceEditors.map(normalizePageAiOpenEditorEntry).filter(Boolean)
: editors.filter(function(entry) { return entry.kind !== 'page'; });
var normalizeGroup = function(group, paneRole) {
var groupEditors = group && Array.isArray(group.editors)
? group.editors.map(normalizePageAiOpenEditorEntry).filter(Boolean)
: editors.filter(function(entry) { return entry.paneRole === paneRole; });
var groupResources = group && Array.isArray(group.resourceEditors)
? group.resourceEditors.map(normalizePageAiOpenEditorEntry).filter(Boolean)
: resources.filter(function(entry) { return entry.paneRole === paneRole; });
var groupActive = groupEditors.find(function(entry) { return entry.active; }) || null;
return {
paneRole: paneRole,
activeObjectIdentity: String(group && group.activeObjectIdentity || (groupActive ? groupActive.objectIdentity : '') || '').trim(),
editors: groupEditors,
resourceEditors: groupResources
};
};
var groups = {
primary: normalizeGroup(snapshot.groups && snapshot.groups.primary, 'primary'),
secondary: normalizeGroup(snapshot.groups && snapshot.groups.secondary, 'secondary')
};
var activeObjectIdentity = String(snapshot.activeObjectIdentity || '').trim();
var activeEditor = editors.find(function(entry) {
return entry.active && (!activeObjectIdentity || entry.objectIdentity === activeObjectIdentity);
}) || groups.primary.editors.find(function(entry) {
return entry.active;
}) || groups.secondary.editors.find(function(entry) {
return entry.active;
}) || null;
return {
schema: String(snapshot.schema || 'mnote.open_editors_snapshot.v1'),
generatedAt: Number(snapshot.generatedAt || 0) || Date.now(),
activeObjectIdentity: activeObjectIdentity || (activeEditor ? activeEditor.objectIdentity : ''),
activeEditor: activeEditor,
editors: editors,
resourceEditors: resources,
groups: groups
};
}
function currentPageAiEditorTarget() {
var snapshot = currentPageAiOpenEditorsSnapshot();
var activeEditor = snapshot && snapshot.activeEditor ? snapshot.activeEditor : null;
if (!activeEditor) {
var fallbackDocumentId = currentDocumentId();
return {
schema: 'mnote.ai_editor_target.v1',
source: 'fallback_current_document',
objectIdentity: fallbackDocumentId ? 'page:primary' : '',
workspacePath: null,
paneRole: 'primary',
documentId: fallbackDocumentId,
workspaceId: resolveWorkspaceId(document.body),
editorKind: 'page',
active: true,
dirtyState: '',
preview: false,
pinned: true,
lastActiveAt: Date.now(),
assetId: '',
path: ''
};
}
return {
schema: 'mnote.ai_editor_target.v1',
source: 'open_editors_snapshot',
objectIdentity: activeEditor.objectIdentity,
workspacePath: activeEditor.workspacePath || null,
paneRole: activeEditor.paneRole,
documentId: activeEditor.documentId,
workspaceId: activeEditor.workspaceId || resolveWorkspaceId(document.body),
editorKind: activeEditor.editorKind || activeEditor.kind,
active: activeEditor.active,
dirtyState: activeEditor.dirtyState || '',
preview: activeEditor.preview === true,
pinned: activeEditor.pinned === true,
lastActiveAt: activeEditor.lastActiveAt || 0,
assetId: activeEditor.assetId || '',
path: activeEditor.path || ''
};
}
function pageAiCloneJson(value) {
if (value == null) return null;
try {
return JSON.parse(JSON.stringify(value));
} catch (_) {
return null;
}
}
function pageAiNormalizeAgentId(agentId) {
var value = String(agentId || '').trim();
return PAGE_AI_AGENT_REGISTRY.some(function(agent) { return agent.id === value; }) ? value : 'reasonix';
}
function pageAiAgentRecord(agentId) {
var normalized = pageAiNormalizeAgentId(agentId);
return PAGE_AI_AGENT_REGISTRY.find(function(agent) { return agent.id === normalized; }) || PAGE_AI_AGENT_REGISTRY[1];
}
function pageAiCurrentAgentId() {
var explicit = String(pageUiState.pageAiAgentId || '').trim();
if (explicit) return pageAiNormalizeAgentId(explicit);
var runtime = String(pageUiState.pageAiAcpRuntime || 'reasonix').trim();
if (runtime === 'hermes') return 'hermes';
return 'reasonix';
}
function pageAiEnsureContextRefState() {
if (!pageUiState.pageAiSelectedContextRefs || typeof pageUiState.pageAiSelectedContextRefs !== 'object') {
pageUiState.pageAiSelectedContextRefs = {
current_page: true,
selection: false,
active_editor: true,
file: false,
folder: false,
changed_files: false
};
}
return pageUiState.pageAiSelectedContextRefs;
}
function pageAiSetAgentId(agentId) {
var next = pageAiNormalizeAgentId(agentId);
var record = pageAiAgentRecord(next);
pageUiState.pageAiAgentId = next;
pageUiState.pageAiAcpRuntime = record.acpRuntime || 'reasonix';
if (next === 'hermes') pageAiSetActiveProfile(pageAiCurrentProfile());
document.documentElement.setAttribute('data-mnote-page-ai-agent-id', next);
pageAiPersistAiPreference('default_agent_id', next);
renderPageAiProviderButtons();
renderPageAiControls();
}
function pageAiToggleContextRef(kind) {
var normalized = String(kind || '').trim();
if (!PAGE_AI_CONTEXT_REF_REGISTRY.some(function(ref) { return ref.id === normalized; })) return;
var selected = pageAiEnsureContextRefState();
selected[normalized] = !selected[normalized];
if (!Object.keys(selected).some(function(key) { return selected[key]; })) selected.current_page = true;
pageAiPersistAiPreference('context_refs.default_selected', selected);
renderPageAiControls();
}
function pageAiPersistAiPreference(key, value) {
try {
var workspaceId = resolveWorkspaceId(document.body);
var body = {
workspaceId: workspaceId,
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
documentId: currentDocumentId(),
updates: {}
};
body.updates['ai.common.' + key] = value;
void fetch('/api/ui/preferences', {
method: 'PUT',
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify(body)
}).catch(function() {});
} catch (_) {}
}
async function pageAiLoadAiPreferences() {
try {
var url = new URL('/api/ui/preferences/effective', window.location.origin);
url.searchParams.set('workspaceId', resolveWorkspaceId(document.body));
url.searchParams.set('sourceKind', currentSourceKind());
url.searchParams.set('rootUri', currentRootUri());
url.searchParams.set('documentId', currentDocumentId());
var response = await fetch(url.toString(), { headers: { accept: 'application/json' }, cache: 'no-store' });
var payload = await response.json().catch(function() { return null; });
if (!response.ok || !payload || !payload.result) return;
var preferences = payload.result.aiPreferences && typeof payload.result.aiPreferences === 'object'
? payload.result.aiPreferences
: {};
var defaultAgent = String(preferences['ai.common.default_agent_id'] || '').trim();
if (defaultAgent) {
var agent = pageAiAgentRecord(defaultAgent);
pageUiState.pageAiAgentId = agent.id;
pageUiState.pageAiAcpRuntime = agent.acpRuntime || pageUiState.pageAiAcpRuntime || 'reasonix';
}
var selected = preferences['ai.common.context_refs.default_selected'];
if (selected && typeof selected === 'object' && !Array.isArray(selected)) {
pageUiState.pageAiSelectedContextRefs = Object.assign({}, pageAiEnsureContextRefState(), selected);
}
} catch (_) {}
renderPageAiControls();
}
function pageAiNormalizeAccessGrant(grant) {
if (!grant || typeof grant !== 'object') return null;
var rootUri = String(grant.rootUri || grant.root_uri || '').trim();
if (!rootUri) return null;
var permission = String(grant.permission || grant.access || '').trim() || 'read';
return {
id: String(grant.id || '').trim(),
rootUri: rootUri,
rootPath: String(grant.rootPath || grant.root_path || '').trim(),
permission: permission === 'read_write' ? 'write' : permission,
recursive: grant.recursive !== false,
source: String(grant.source || 'sqlite_directory_grant').trim() || 'sqlite_directory_grant',
status: String(grant.status || 'active').trim() || 'active'
};
}
async function pageAiLoadAllowedRoots() {
if (currentSourceKind() !== 'local_folder') {
pageUiState.pageAiAllowedRoots = [];
renderPageAiControls();
return [];
}
try {
var response = await fetch('/api/user/access-policy', {
headers: { accept: 'application/json' },
cache: 'no-store'
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'access_policy_failed_' + response.status));
var grants = pageAiNormalizeArray(payload && (payload.grants || payload.policy && payload.policy.grants));
var currentRoot = String(currentRootUri() || '').trim();
pageUiState.pageAiAllowedRoots = grants.map(pageAiNormalizeAccessGrant).filter(Boolean).filter(function(grant) {
return grant.status === 'active' && (!currentRoot || grant.rootUri === currentRoot);
});
pageUiState.pageAiAllowedRootsError = '';
} catch (error) {
pageUiState.pageAiAllowedRoots = [];
pageUiState.pageAiAllowedRootsError = error instanceof Error ? error.message : String(error);
}
renderPageAiControls();
return pageUiState.pageAiAllowedRoots;
}
function pageAiBuildContextRefs(scopedContext, runTargetSnapshot) {
var selected = pageAiEnsureContextRefState();
var refs = [];
var documentId = currentDocumentId();
var rootUri = currentRootUri();
var workspaceId = resolveWorkspaceId(document.body);
var editorTarget = scopedContext && scopedContext.editorTarget ? scopedContext.editorTarget : currentPageAiEditorTarget();
if (selected.current_page) {
refs.push({
kind: 'current_page',
documentId: documentId,
rootUri: rootUri,
workspaceId: workspaceId
});
}
if (selected.selection && scopedContext && scopedContext.selectedText) {
refs.push({
kind: 'selection',
documentId: documentId,
rootUri: rootUri,
selectedBlockId: scopedContext.selectedBlockId || ''
});
}
if (selected.active_editor && editorTarget) {
refs.push({
kind: 'active_editor',
documentId: editorTarget.documentId || documentId,
rootUri: editorTarget.workspacePath && editorTarget.workspacePath.rootUri || rootUri,
relativePath: editorTarget.workspacePath && editorTarget.workspacePath.relativePath || '',
editorKind: editorTarget.editorKind || ''
});
}
if (selected.file) {
refs.push({
kind: 'file',
documentId: documentId,
rootUri: rootUri,
relativePath: localMarkdownRelativePathFromPageAiDocumentId(documentId)
});
}
if (selected.folder) {
refs.push({
kind: 'folder',
rootUri: rootUri,
relativePath: ''
});
}
if (selected.changed_files) {
refs.push({
kind: 'changed_files',
rootUri: rootUri,
sinceRunTargetSnapshot: runTargetSnapshot && runTargetSnapshot.frozenAt || null
});
}
return refs.filter(function(ref) {
return ref && String(ref.kind || '').trim();
});
}
function pageAiBuildAllowedRoots() {
return pageAiNormalizeArray(pageUiState.pageAiAllowedRoots).map(function(root) {
return {
rootUri: root.rootUri,
permission: root.permission === 'write' || root.permission === 'read_write' ? 'write' : 'read',
recursive: root.recursive !== false,
source: root.source === 'auto' || root.source === 'user' || root.source === 'admin'
? 'sqlite_directory_grant'
: (root.source || 'sqlite_directory_grant'),
grantId: root.id || ''
};
});
}
function pageAiBuildRunTargetSnapshot(scopedContext, prompt) {
var aiContext = scopedContext && scopedContext.pageContext && scopedContext.pageContext.aiContext
? scopedContext.pageContext.aiContext
: {};
var editorTarget = scopedContext && scopedContext.editorTarget ? scopedContext.editorTarget : currentPageAiEditorTarget();
return {
schema: 'mnote.page_ai_run_target_snapshot.v1',
source: 'open_editors_snapshot',
frozenAt: Date.now(),
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
contextScope: pageUiState.pageAiContextScope || 'page',
promptPreview: searchText(prompt || '').slice(0, 160),
editorTarget: pageAiCloneJson(editorTarget),
activeEditorTarget: pageAiCloneJson(aiContext.activeEditorTarget || editorTarget),
openEditorsSnapshot: pageAiCloneJson(aiContext.openEditorsSnapshot || currentPageAiOpenEditorsSnapshot())
};
}
function pageAiContextKindsFromRefs(contextRefs) {
var kinds = {};
pageAiNormalizeArray(contextRefs).forEach(function(ref) {
var kind = String(ref && ref.kind || '').trim();
if (kind) kinds[kind] = true;
});
return kinds;
}
function pageAiPageContextForRefs(pageContext, contextRefs) {
var cloned = pageAiCloneJson(pageContext) || {};
var kinds = pageAiContextKindsFromRefs(contextRefs);
var aiContext = cloned.aiContext && typeof cloned.aiContext === 'object' ? cloned.aiContext : {};
if (!kinds.current_page) {
delete cloned.documentBlocks;
delete aiContext.contextBlocks;
delete aiContext.pageText;
delete aiContext.pageXml;
delete aiContext.truncated;
delete aiContext.warnings;
}
if (!kinds.selection) {
delete aiContext.selectedText;
delete aiContext.selectedBlockIds;
delete aiContext.selectedBlocks;
delete aiContext.allowedTargetBlockIds;
}
cloned.aiContext = aiContext;
return cloned;
}
function pageAiSetRunTargetSnapshot(snapshot) {
pageUiState.pageAiCurrentRunTargetSnapshot = snapshot || null;
var target = snapshot && snapshot.editorTarget ? snapshot.editorTarget : null;
var documentId = String(target && target.documentId || snapshot && snapshot.documentId || '').trim();
var workspaceId = String(target && target.workspaceId || snapshot && snapshot.workspaceId || '').trim();
var rootUri = String(target && target.workspacePath && target.workspacePath.rootUri || snapshot && snapshot.rootUri || '').trim();
if (documentId) document.documentElement.setAttribute('data-mnote-page-ai-run-target-document-id', documentId);
if (workspaceId) document.documentElement.setAttribute('data-mnote-page-ai-run-target-workspace-id', workspaceId);
if (rootUri) document.documentElement.setAttribute('data-mnote-page-ai-run-target-root-uri', rootUri);
}
function assertPageAiTargetInCurrentWorkspace(editorTarget) {
var target = editorTarget && typeof editorTarget === 'object' ? editorTarget : {};
var workspacePath = target.workspacePath && typeof target.workspacePath === 'object' ? target.workspacePath : {};
var targetSourceKind = String(workspacePath.sourceKind || '').trim();
var currentKind = String(currentSourceKind() || '').trim();
if (targetSourceKind && currentKind && targetSourceKind !== currentKind) {
var sourceError = new Error('AI target 与当前页面 sourceKind 不一致,请重新选择当前工作区内的目标。');
sourceError.code = 'page_ai_target_workspace_mismatch';
throw sourceError;
}
var targetRootUri = String(workspacePath.rootUri || '').trim();
var currentRoot = String(currentRootUri() || '').trim();
if (targetRootUri && currentRoot && targetRootUri !== currentRoot) {
var rootError = new Error('AI target 与当前本地工作区 rootUri 不一致,请重新选择当前工作区内的目标。');
rootError.code = 'page_ai_target_workspace_mismatch';
throw rootError;
}
var targetWorkspaceId = String(target.workspaceId || workspacePath.workspaceId || '').trim();
var currentWorkspaceId = String(resolveWorkspaceId(document.body) || '').trim();
if (targetWorkspaceId && currentWorkspaceId && targetWorkspaceId !== currentWorkspaceId) {
var workspaceError = new Error('AI target 与当前 workspaceId 不一致,请重新选择当前工作区内的目标。');
workspaceError.code = 'page_ai_target_workspace_mismatch';
throw workspaceError;
}
}
function localMarkdownRelativePathFromPageAiDocumentId(documentId) {
var value = String(documentId || '').trim();
if (!value.startsWith('local-md:')) return '';
return value.slice('local-md:'.length).replace(/~2F/g, '/');
}
function pageAiBlockingDirtyState(dirtyState) {
var state = String(dirtyState || '').trim();
var normalized = state.toLowerCase();
if (normalized === 'dirty') return 'Dirty';
if (normalized === 'stale') return 'Stale';
if (normalized === 'deleted') return 'Deleted';
if (normalized === 'externalmodified' || normalized === 'external-change-conflict' || normalized === 'hasexternalconflict') return 'ExternalModified';
return '';
}
async function fetchPageAiTargetBufferState(editorTarget) {
if (currentSourceKind() !== 'local_folder') return null;
var target = editorTarget && typeof editorTarget === 'object' ? editorTarget : {};
var documentId = String(target.documentId || currentDocumentId() || '').trim();
var rootUri = String(target.workspacePath && target.workspacePath.rootUri || currentRootUri() || '').trim();
if (!documentId || !rootUri) return null;
var relativePath = String(target.workspacePath && target.workspacePath.relativePath || '').trim()
|| localMarkdownRelativePathFromPageAiDocumentId(documentId);
var url = new URL('/api/documents/buffer-state', window.location.origin);
url.searchParams.set('documentId', documentId);
url.searchParams.set('sourceKind', 'local_folder');
url.searchParams.set('rootUri', rootUri);
var workspaceId = String(target.workspaceId || resolveWorkspaceId(document.body) || '').trim();
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
if (relativePath) url.searchParams.set('relativePath', relativePath);
try {
var response = await fetch(url.toString(), { cache: 'no-store', headers: { accept: 'application/json' } });
var payload = await response.json().catch(function() { return null; });
if (!response.ok || !payload || payload.ok !== true) return null;
return payload.result || null;
} catch (_) {
return null;
}
}
async function assertPageAiTargetWritable(editorTarget) {
var snapshotState = pageAiBlockingDirtyState(editorTarget && editorTarget.dirtyState);
var bufferState = await fetchPageAiTargetBufferState(editorTarget);
var bufferDirtyState = pageAiBlockingDirtyState(bufferState && bufferState.dirtyState);
var blockedState = bufferDirtyState || snapshotState;
if (!blockedState) return bufferState;
var documentId = String(editorTarget && editorTarget.documentId || currentDocumentId() || '').trim();
var error = new Error('目标文档存在未保存或外部变更状态(' + blockedState + '),请先保存、解决冲突或刷新后再让 AI 写入。');
error.code = 'page_ai_target_buffer_not_writable';
error.documentId = documentId;
error.dirtyState = blockedState;
throw error;
}
function currentPageAiSelectedText() {
try {
var selection = window.getSelection ? window.getSelection() : null;
return selection ? searchText(selection.toString() || '') : '';
} catch (_) {
return '';
}
}
function pageAiProjectionBlocks(aggregate) {
var blocks = aggregate && aggregate.body && aggregate.body.blockDocument && aggregate.body.blockDocument.blocks;
return Array.isArray(blocks) ? blocks : [];
}
function pageAiBlockText(block) {
return searchText(block && (block.text || block.title || block.content) || '');
}
function pageAiSelectedBlockIdsFromSelection() {
try {
var selection = window.getSelection ? window.getSelection() : null;
if (!selection || selection.rangeCount === 0 || searchText(selection.toString() || '') === '') return [];
var range = selection.getRangeAt(0);
var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
if (!(editor instanceof HTMLElement)) return [];
return Array.from(editor.children).filter(function(node) {
if (!(node instanceof HTMLElement)) return false;
try {
return range.intersectsNode(node);
} catch (_) {
return false;
}
}).map(function(node) {
return searchText(node.getAttribute('data-id') || node.id || '');
}).filter(Boolean);
} catch (_) {
return [];
}
}
function pageAiBlocksToPageXml(blocks, aggregate) {
var revision = aggregate && aggregate.body ? String(aggregate.body.revision || '') : '';
var pageId = currentDocumentId() || 'current-page';
var lines = ['<page id="' + escapeHtml(pageId) + '" revision="' + escapeHtml(revision) + '">'];
blocks.forEach(function(block) {
var blockId = String(block && (block.blockId || block.id) || '');
var type = String(block && block.type || 'paragraph');
var revisionRef = String(block && block.revisionRef || '');
var level = block && block.attrs && block.attrs.level ? ' level="' + escapeHtml(block.attrs.level) + '"' : '';
lines.push(' <block id="' + escapeHtml(blockId) + '" type="' + escapeHtml(type) + '" revisionRef="' + escapeHtml(revisionRef) + '"' + level + '>' + escapeHtml(pageAiBlockText(block)) + '</block>');
});
lines.push('</page>');
return lines.join('\n');
}
function buildPageAiContext(contextSnapshot, scope, selectedText) {
var aggregate = contextSnapshot.aggregate || {};
var body = aggregate.body || {};
var allBlocks = pageAiProjectionBlocks(aggregate);
var selectedBlockIds = scope === 'selection' ? pageAiSelectedBlockIdsFromSelection() : [];
var selectedSet = {};
selectedBlockIds.forEach(function(id) { selectedSet[id] = true; });
var selectedBlocks = selectedBlockIds.length
? allBlocks.filter(function(block) { return selectedSet[String(block.blockId || block.id || '')]; })
: [];
var contextBlocks = selectedBlocks.length ? selectedBlocks : allBlocks.slice(0, 120);
var truncated = !selectedBlocks.length && allBlocks.length > contextBlocks.length;
return {
schema: 'mnote.page_ai_context.v1',
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
activeEditorTarget: currentPageAiEditorTarget(),
openEditorsSnapshot: currentPageAiOpenEditorsSnapshot(),
scope: scope,
revision: body.revision || null,
conflictDetectionKey: body.conflictDetectionKey || null,
selectedText: selectedText || '',
selectedBlockIds: selectedBlockIds,
allowedTargetBlockIds: selectedBlockIds,
selectedBlocks: selectedBlocks,
contextBlocks: contextBlocks,
pageText: contextBlocks.map(pageAiBlockText).filter(Boolean).join('\n'),
pageXml: pageAiBlocksToPageXml(contextBlocks, aggregate),
truncated: truncated,
warnings: truncated ? [{ code: 'page_ai_context_truncated', message: '页面 AI context 已按前 120 个块裁剪' }] : []
};
}
function pageAiScopedPageContext(contextSnapshot) {
var aggregate = contextSnapshot.aggregate || {};
var body = contextSnapshot.body || {};
var subtree = contextSnapshot.subtree || null;
var outline = subtree && subtree.outline ? subtree.outline : null;
var scope = pageUiState.pageAiContextScope || 'page';
var title = aggregate.head && aggregate.head.title ? aggregate.head.title : '';
var selectedText = scope === 'selection' ? currentPageAiSelectedText() : '';
var aiContext = buildPageAiContext(contextSnapshot, scope, selectedText);
var editorTarget = aiContext.activeEditorTarget || currentPageAiEditorTarget();
return {
pageContext: {
contextScope: scope,
documentBlocks: null,
node: {
documentId: currentDocumentId(),
title: title
},
subtree: null,
outline: null,
pageSubtreeSource: scope === 'page' ? (contextSnapshot.pageSubtreeSource || 'none') : 'scope:' + scope,
evidence: selectedText ? [{ kind: 'selection', text: selectedText }] : null,
pageOptions: scope === 'options' || scope === 'page' ? currentPageOptions() : null,
contentAccess: 'mnote.doc.fetch',
aiContext: aiContext
},
editorTarget: editorTarget,
selectedText: selectedText,
selectedBlockId: aiContext.selectedBlockIds[0] || null
};
}
function updatePageAiTriggerState() {
var trigger = document.querySelector('[data-testid="wolai-floating-ai"]');
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('data-state', pageUiState.pageAiOpen ? 'open' : 'closed');
trigger.setAttribute('aria-expanded', pageUiState.pageAiOpen ? 'true' : 'false');
}
function pageAiSuggestions() {
var title = searchText(document.querySelector('[data-page-title-current="true"]')?.textContent) || '当前页面';
return [
'帮我总结《' + title + '》当前内容',
'把当前页面改写得更简洁一些',
'提炼当前页的关键待办和行动项',
'基于当前页内容生成一个三段式摘要'
];
}
function pageAiStorageKey() {
return 'hermes_page_ai_session:' + currentDocumentId();
}
function pageAiTimestamp(value) {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string' && value.trim()) {
var parsed = Date.parse(value);
if (Number.isFinite(parsed)) return parsed;
}
return Date.now();
}
function pageAiBackendSessionQuery(extra) {
var params = new URLSearchParams();
params.set('source', 'acp');
params.set('workspaceId', resolveWorkspaceId(document.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();
return {
id: 'sess_' + now + '_' + Math.random().toString(16).slice(2, 8),
title: title || '新会话',
profile: pageAiCurrentProfile(),
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
createdAt: now,
updatedAt: now,
source: 'local',
usage: null,
status: 'idle',
messages: []
};
}
function pageAiUsageSummary(usage) {
if (!usage || typeof usage !== 'object') return '';
var used = usage.used ?? usage.contextUsed ?? usage.input_tokens ?? usage.inputTokens ?? usage.prompt_tokens ?? usage.promptTokens;
var size = usage.size ?? usage.contextSize ?? usage.total_tokens ?? usage.totalTokens;
var output = usage.output_tokens ?? usage.outputTokens ?? usage.completion_tokens ?? usage.completionTokens;
var parts = [];
if (Number.isFinite(Number(used))) parts.push('ctx ' + Number(used));
if (Number.isFinite(Number(size)) && Number(size) > 0) parts.push('/ ' + Number(size));
if (Number.isFinite(Number(output)) && Number(output) > 0) parts.push('out ' + Number(output));
return parts.join(' ') || '';
}
function pageAiPermissionMessage(payload, eventType) {
payload = payload && typeof payload === 'object' ? payload : {};
var permissionId = String(payload.permissionId || payload.permission_id || payload.id || ('perm_' + Date.now())).trim();
var toolName = String(payload.toolName || payload.tool || payload.name || payload.method || 'session/request_permission').trim();
var args = payload.arguments || payload.args || payload.input || payload.params || payload;
var decision = String(payload.decision || payload.result || '').trim();
if (!decision && eventType === 'permission.denied') decision = 'denied';
if (!decision && eventType === 'permission.allowed') decision = 'allowed';
return {
role: 'tool',
kind: 'permission',
permissionId: permissionId,
toolName: toolName,
argsSummary: pageAiPreviewValue(args),
content: decision === 'denied' ? '已自动拒绝权限请求' : (decision === 'allowed' ? '已自动允许权限请求' : '等待权限确认'),
resolved: decision === 'denied' || decision === 'allowed',
decision: decision
};
}
function pageAiApplyPermissionEvent(eventName, payloadText) {
var payload = null;
try {
payload = JSON.parse(payloadText || 'null');
} catch (_) {
payload = {};
}
var message = pageAiPermissionMessage(payload, eventName);
var existing = pageUiState.pageAiMessages.find(function(item) {
return item.kind === 'permission' && item.permissionId === message.permissionId;
});
if (existing) {
Object.assign(existing, message);
} else {
pageUiState.pageAiMessages.push(message);
}
pageUiState.pageAiPermissionRequests = pageUiState.pageAiPermissionRequests.filter(function(item) {
return item.permissionId !== message.permissionId;
}).concat([message]).slice(-20);
if (!message.resolved) {
pageAiShowPermissionDialog(message);
} else {
pageAiHidePermissionDialog();
}
}
function pageAiResolvePermission(permissionId, decision) {
permissionId = String(permissionId || '').trim();
if (!permissionId) return;
// 调用后端 resolve-permission 端点,让 ACP agent 得到真实响应
var runId = pageUiState.pageAiCurrentRunId;
if (runId) {
fetch('/api/hermes/client/runs/' + encodeURIComponent(runId) + '/resolve-permission', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ permissionId: permissionId, decision: decision })
}).then(function(response) {
if (!response.ok) console.warn('resolve-permission 后端返回异常', response.status);
}).catch(function(err) {
console.warn('resolve-permission 请求失败', err);
});
} else {
console.warn('resolve-permission: 无活跃 runId,只能本地更新');
}
// 本地乐观更新 UI
pageUiState.pageAiMessages.forEach(function(item) {
if (item.kind === 'permission' && item.permissionId === permissionId) {
item.resolved = true;
item.decision = decision;
item.content = decision === 'allow' ? '已允许权限请求' : '已拒绝权限请求';
}
});
var dialog = document.querySelector('[data-page-ai-permission-dialog]');
if (dialog instanceof HTMLElement) dialog.hidden = true;
renderPageAiConversation();
}
function pageAiOpenLocation(loc) {
var path = String(loc || '').trim();
if (!path) return;
openLocalResourceInActiveTab({ path: path }).then(function(opened) {
if (!opened) {
var href = buildLocalFileOpenUrl(path, false);
if (href) window.open(href, '_blank', 'noopener,noreferrer');
}
});
}
function pageAiHidePermissionDialog() {
var dialog = document.querySelector('[data-page-ai-permission-dialog]');
if (dialog instanceof HTMLElement) dialog.hidden = true;
}
function pageAiShowPermissionDialog(message) {
if (!message || message.kind !== 'permission') return;
if (message.resolved) {
pageAiHidePermissionDialog();
return;
}
var dialog = document.querySelector('[data-page-ai-permission-dialog]');
if (!(dialog instanceof HTMLElement)) {
dialog = document.createElement('div');
dialog.className = 'wolai-page-ai-permission-dialog';
dialog.setAttribute('data-page-ai-permission-dialog', 'true');
dialog.innerHTML = '' +
'<div class="wolai-page-ai-permission-panel" role="dialog" aria-modal="false">' +
'<div class="wolai-page-ai-memory-title" data-page-ai-permission-tool></div>' +
'<div class="wolai-page-ai-tool-meta" data-page-ai-permission-args></div>' +
'<div class="wolai-page-ai-message-actions">' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="allow" data-page-ai-permission-dialog-action>允许</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="deny" data-page-ai-permission-dialog-action>拒绝</button>' +
'</div>' +
'</div>';
document.body.appendChild(dialog);
}
var tool = dialog.querySelector('[data-page-ai-permission-tool]');
if (tool instanceof HTMLElement) tool.textContent = message.toolName || 'session/request_permission';
var args = dialog.querySelector('[data-page-ai-permission-args]');
if (args instanceof HTMLElement) args.textContent = message.argsSummary || message.content || '';
dialog.querySelectorAll('[data-page-ai-permission-dialog-action]').forEach(function(button) {
if (button instanceof HTMLButtonElement) {
button.setAttribute('data-page-ai-permission-id', message.permissionId || '');
button.disabled = Boolean(message.resolved);
}
});
dialog.hidden = false;
}
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();
return {
id: String(session && session.id || '').trim() || pageAiNewSession().id,
title: String(session && session.title || '').trim() || '新会话',
profile: String(session && session.profile || pageAiCurrentProfile()).trim() || 'default',
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(),
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();
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 || '当前页问答',
profile: String(row.profile || payload.profile || pageAiRunProfile()).trim() || 'default',
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(),
runId: String(row.runId || row.run_id || '').trim(),
status: String(row.status || (row.runtime && row.runtime.status) || '').trim(),
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 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 === '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 = window.localStorage.getItem(pageAiStorageKey());
var parsed = raw ? JSON.parse(raw) : null;
var activeId = String(parsed && parsed.activeSessionId || '').trim();
var activeProfile = String(parsed && parsed.activeProfileName || '').trim();
var activeAcpRuntime = String(parsed && parsed.activeAcpRuntime || '').trim();
var sessions = pageAiNormalizeSessions(parsed && parsed.sessions);
var storageVersion = Number(parsed && parsed.version || 0);
if (storageVersion >= PAGE_AI_SESSION_STORAGE_VERSION && activeAcpRuntime) pageUiState.pageAiAcpRuntime = activeAcpRuntime;
if (activeProfile) pageAiSetActiveProfile(activeProfile);
if (sessions.length) {
pageUiState.pageAiSessions = sessions;
var activeSession = sessions.find(function(session) { return session.id === activeId; }) || sessions[0];
pageUiState.pageAiActiveSessionId = activeSession.id;
if (activeSession.profile) pageAiSetActiveProfile(activeSession.profile);
if (!pageUiState.pageAiAcpRuntime && activeSession.acpRuntime) pageUiState.pageAiAcpRuntime = activeSession.acpRuntime;
pageUiState.pageAiMessages = Array.isArray(activeSession.messages) ? activeSession.messages.slice() : [];
return;
}
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 = [];
}
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);
if (!backendSessions.length) return [];
pageUiState.pageAiSessions = pageAiMergeSessions(pageUiState.pageAiSessions, backendSessions);
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 || '').trim();
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);
}
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 messages = 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; });
events.forEach(function(event) {
var message = pageAiMessageFromRuntimeEvent(event);
if (message) messages.push(message);
});
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 (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() {
document.documentElement.setAttribute('data-mnote-page-ai-session-owner', 'hermes');
document.documentElement.setAttribute('data-mnote-page-ai-session-key', pageAiStorageKey());
try {
pageAiSyncCurrentSessionMessages();
window.localStorage.setItem(pageAiStorageKey(), JSON.stringify({
version: PAGE_AI_SESSION_STORAGE_VERSION,
activeSessionId: pageUiState.pageAiActiveSessionId,
activeProfileName: pageAiCurrentProfile(),
activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
sessions: pageAiNormalizeSessions(pageUiState.pageAiSessions)
}));
} catch (_) {}
}
async function pageAiEnsureHermesSession(forceCreate) {
pageAiLoadSessions();
var current = pageAiCurrentSession();
if (!forceCreate && current && String(current.id || '').startsWith('mnote_') && current.profile === pageAiCurrentProfile()) 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(),
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
traceId: 'page-ai-' + Date.now().toString(36),
profile: pageAiCurrentProfile(),
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 || '当前页问答'),
profile: String(payload.profile || pageAiCurrentProfile()).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(),
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) + '/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 pageAiSyncCurrentSessionMessages() {
var session = pageAiCurrentSession();
if (!session) return;
session.messages = Array.isArray(pageUiState.pageAiMessages) ? pageUiState.pageAiMessages.slice(-300) : [];
session.profile = pageAiCurrentProfile();
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;
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 = window.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();
}
async function pageAiDeleteBackendSession(sessionId) {
var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
if (!session) return;
if (!window.confirm('确定删除 AI 会话“' + (session.title || sessionId) + '”吗?')) return;
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '?' + 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));
}
pageUiState.pageAiSessions = pageUiState.pageAiSessions.filter(function(item) { return item.id !== sessionId; });
if (pageUiState.pageAiActiveSessionId === sessionId) {
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() : [];
}
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
}
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();
}
function pageAiProviderLabel(provider) {
if (provider === 'codex') return 'Codex';
if (provider === 'claudecode') return 'ClaudeCode';
return 'Hermes';
}
function pageAiNormalizeArray(value) {
return Array.isArray(value) ? value : [];
}
function pageAiDefaultAcpRuntimes() {
return [
{
name: 'reasonix',
title: 'ACP · Reasonix',
description: '通过 ACP 协议直连 ReasonixDeepSeek 缓存优先)',
model: 'deepseek-chat',
preset: 'auto'
},
{
name: 'hermes',
title: 'ACP · Hermes',
description: '通过 ACP 协议直连 Hermes agent runtime'
}
];
}
function pageAiNormalizeAcpRuntimes(runtimes) {
var byName = {};
pageAiDefaultAcpRuntimes().forEach(function(runtime) {
byName[runtime.name] = Object.assign({}, runtime);
});
pageAiNormalizeArray(runtimes).forEach(function(runtime) {
var name = String(runtime && runtime.name || '').trim();
if (name !== 'reasonix' && name !== 'hermes') return;
byName[name] = Object.assign({}, byName[name] || {}, runtime, { name: name });
});
return ['reasonix', 'hermes'].map(function(name) { return byName[name]; }).filter(Boolean);
}
function pageAiUnwrapUpstream(payload) {
if (payload && typeof payload === 'object' && payload.upstream) return payload.upstream;
return payload || null;
}
function pageAiProfileValue(profile) {
if (profile && typeof profile === 'object') {
return String(profile.name || profile.profile || profile.id || '').trim();
}
return String(profile || '').trim();
}
function pageAiCurrentProfile() {
var active = String(pageUiState.pageAiActiveProfileName || '').trim();
if (active) return active;
var selected = pageUiState.pageAiProfiles.find(function(profile) {
return profile && profile.active;
});
return pageAiProfileValue(selected) || 'mnoteai';
}
function pageAiRunProfile() {
return String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix' ? 'reasonix' : pageAiCurrentProfile();
}
function pageAiMnoteToolModel() {
return String(document.documentElement.getAttribute('data-mnote-page-ai-tool-model') || 'deepseek-v4-flash').trim() || 'deepseek-v4-flash';
}
function pageAiCurrentProfileRecord() {
var active = pageAiCurrentProfile();
return pageUiState.pageAiProfiles.find(function(profile) {
return pageAiProfileValue(profile) === active;
}) || null;
}
function pageAiCurrentModelLabel() {
var profile = pageAiCurrentProfileRecord();
var toolModel = pageAiMnoteToolModel();
if (!profile) return 'tool: ' + toolModel;
var model = String(profile.model || '').trim();
var gateway = String(profile.gateway || '').trim();
var profileLabel = [model, gateway].filter(Boolean).join(' · ') || '由 Hermes 决定';
return 'tool: ' + toolModel + ' · profile: ' + profileLabel;
}
function pageAiNormalizeProfiles(payload) {
var upstream = pageAiUnwrapUpstream(payload);
var profiles = pageAiNormalizeArray(upstream && upstream.profiles ? upstream.profiles : upstream);
return profiles.map(function(profile) {
return {
name: pageAiProfileValue(profile) || 'default',
active: Boolean(profile && profile.active),
model: String(profile && profile.model || '').trim(),
gateway: String(profile && profile.gateway || '').trim(),
alias: String(profile && profile.alias || '').trim()
};
});
}
function pageAiNormalizeSkills(payload) {
var upstream = pageAiUnwrapUpstream(payload);
var categories = pageAiNormalizeArray(upstream && upstream.categories ? upstream.categories : []);
var archived = pageAiNormalizeArray(upstream && upstream.archived ? upstream.archived : []);
return {
categories: categories.map(function(category) {
return {
name: String(category && category.name || '').trim() || 'misc',
description: String(category && category.description || '').trim(),
skills: pageAiNormalizeArray(category && category.skills).map(function(skill) {
return {
name: String(skill && skill.name || '').trim(),
description: String(skill && skill.description || '').trim(),
enabled: skill && skill.enabled !== false,
source: String(skill && skill.source || 'local').trim() || 'local',
origin: String(skill && skill.origin || '').trim(),
createdBy: String(skill && skill.createdBy || '').trim(),
patchCount: Number(skill && skill.patchCount || 0),
modified: Boolean(skill && skill.modified)
};
})
};
}),
archived: archived.map(function(skill) {
return {
name: String(skill && skill.name || '').trim(),
description: String(skill && skill.description || '').trim(),
enabled: skill && skill.enabled !== false,
source: String(skill && skill.source || 'local').trim() || 'local',
origin: String(skill && skill.origin || '').trim(),
createdBy: String(skill && skill.createdBy || '').trim(),
patchCount: Number(skill && skill.patchCount || 0),
modified: Boolean(skill && skill.modified)
};
})
};
}
function pageAiSkillListEntries() {
var result = [];
pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
pageAiNormalizeArray(category.skills).forEach(function(skill) {
result.push({
category: category.name,
name: skill.name,
description: skill.description,
enabled: skill.enabled !== false,
source: skill.source || 'local',
origin: skill.origin || '',
createdBy: skill.createdBy || '',
patchCount: Number(skill.patchCount || 0),
modified: Boolean(skill.modified)
});
});
});
return result.concat(pageAiNormalizeArray(pageUiState.pageAiSkills.archived));
}
function pageAiSetActiveProfile(profileName) {
var next = String(profileName || '').trim() || 'mnoteai';
pageUiState.pageAiActiveProfileName = next;
document.documentElement.setAttribute('data-mnote-page-ai-profile', next);
document.documentElement.setAttribute('data-mnote-page-ai-tool-model', pageAiMnoteToolModel());
}
function pageAiSetRunStatus(status, runId) {
pageUiState.pageAiRunStatus = status || 'idle';
pageUiState.pageAiCurrentRunId = runId || pageUiState.pageAiCurrentRunId || '';
document.documentElement.setAttribute('data-mnote-page-ai-run-status', pageUiState.pageAiRunStatus);
if (pageUiState.pageAiCurrentRunId) {
document.documentElement.setAttribute('data-mnote-page-ai-run-id', pageUiState.pageAiCurrentRunId);
}
}
function pageAiApplyRuntimeState(runtime) {
if (!runtime || typeof runtime !== 'object') return;
var status = String(runtime.status || '').trim();
var runId = String(runtime.runId || runtime.run_id || '').trim();
if (status) pageAiSetRunStatus(status, runId);
var queueLength = Number(runtime.queueLength || runtime.queue_length || 0);
if (Number.isFinite(queueLength)) pageUiState.pageAiQueueLength = Math.max(0, queueLength);
var toolName = String(runtime.lastToolName || runtime.last_tool_name || '').trim();
if (toolName) {
pageUiState.pageAiLastToolCall = {
event: String(runtime.lastEvent || runtime.last_event || ''),
name: toolName,
runId: runId,
traceId: String(runtime.traceId || runtime.trace_id || ''),
auditId: String(runtime.lastAuditId || runtime.last_audit_id || '')
};
}
}
function pageAiApplyQueuedRun(payload) {
if (!payload || payload.queued !== true) return false;
var queueId = String(payload.queueId || payload.queue_id || '').trim();
var queueLength = Number(payload.queueLength || payload.queue_length || 0);
pageUiState.pageAiQueueLength = Number.isFinite(queueLength) ? Math.max(0, queueLength) : 1;
if (queueId) {
pageUiState.pageAiQueuedItems = pageUiState.pageAiQueuedItems.filter(function(item) {
return item.queueId !== queueId;
}).concat([{
queueId: queueId,
sessionId: String(payload.sessionId || payload.session_id || pageUiState.pageAiActiveSessionId || ''),
traceId: String(payload.traceId || payload.trace_id || ''),
queuedAt: Number(payload.queuedAt || payload.queued_at || Date.now())
}]);
}
pageAiSetRunStatus('queued', pageUiState.pageAiCurrentRunId);
return true;
}
function pageAiPreviewValue(value) {
if (value == null || value === '') return '';
if (typeof value === 'string') return value.length > 120 ? value.slice(0, 120) + '…' : value;
try {
var text = JSON.stringify(value);
return text.length > 120 ? text.slice(0, 120) + '…' : text;
} catch (_) {
return String(value);
}
}
function pageAiNormalizeToolName(name) {
return String(name || '').trim().replace(/_/g, '.');
}
function pageAiFormatChangedFiles(files) {
return pageAiNormalizeArray(files).map(function(file) {
var path = String(file && file.path || '').trim();
var changeType = String(file && file.changeType || file.change_type || 'modified').trim();
var summary = String(file && file.summary || '').trim();
var version = String(file && (file.version || file.revision || '') || '').trim();
var hashBefore = String(file && file.hashBefore || '').trim();
var hashAfter = String(file && file.hashAfter || '').trim();
if (!version && (hashBefore || hashAfter)) version = 'hash ' + [hashBefore || '0', hashAfter || '0'].join('→');
var modifiedBefore = Number(file && file.modifiedBeforeMs || 0) || 0;
var modifiedAfter = Number(file && file.modifiedAfterMs || 0) || 0;
if (!version && (modifiedBefore || modifiedAfter)) version = 'mtime ' + [String(modifiedBefore), String(modifiedAfter)].join('→');
var actor = [file && file.agentKind, file && file.actorType, file && file.actorId].map(function(value) {
return String(value || '').trim();
}).filter(Boolean).join('/');
return [changeType, path, version, summary, actor].filter(Boolean).join(' · ');
}).filter(Boolean).join('\n');
}
function pageAiToolEventDeepFindString(value, keys, depth) {
if (!value || typeof value !== 'object' || depth > 5) return '';
for (var index = 0; index < keys.length; index += 1) {
var key = keys[index];
if (Object.prototype.hasOwnProperty.call(value, key) && typeof value[key] === 'string' && value[key].trim()) {
return value[key].trim();
}
}
if (Array.isArray(value)) {
for (var arrayIndex = 0; arrayIndex < value.length; arrayIndex += 1) {
var fromArray = pageAiToolEventDeepFindString(value[arrayIndex], keys, depth + 1);
if (fromArray) return fromArray;
}
return '';
}
var preferred = ['audit', 'args', 'arguments', 'input', 'result', 'summary', 'output', 'upstream'];
for (var prefIndex = 0; prefIndex < preferred.length; prefIndex += 1) {
var child = value[preferred[prefIndex]];
var fromPreferred = pageAiToolEventDeepFindString(child, keys, depth + 1);
if (fromPreferred) return fromPreferred;
}
var objectKeys = Object.keys(value);
for (var objectIndex = 0; objectIndex < objectKeys.length; objectIndex += 1) {
var fromObject = pageAiToolEventDeepFindString(value[objectKeys[objectIndex]], keys, depth + 1);
if (fromObject) return fromObject;
}
return '';
}
function pageAiNotifyToolWriteCompleted(toolName, toolEvent, runId, runTraceId) {
var normalizedTool = pageAiNormalizeToolName(toolName);
var writesCurrentPage = [
'mnote.page.save',
'mnote.page.update.title',
'mnote.page.update.options',
'mnote.doc.apply.block.ops',
'mnote.block.replace',
'mnote.block.insert.after',
'mnote.block.delete',
'mnote.block.move.after'
].indexOf(normalizedTool) >= 0 || [
'mnote.page.update_title',
'mnote.page.update_options',
'mnote.doc.apply_block_ops',
'mnote.block.replace',
'mnote.block.insert_after',
'mnote.block.delete',
'mnote.block.move_after'
].indexOf(String(toolName || '').trim()) >= 0;
if (!writesCurrentPage) return;
var documentId = pageAiToolEventDeepFindString(toolEvent, ['documentId', 'document_id'], 0) || currentDocumentId();
var workspaceId = pageAiToolEventDeepFindString(toolEvent, ['workspaceId', 'workspace_id'], 0) || resolveWorkspaceId(document.body);
try {
window.dispatchEvent(new CustomEvent('mnote:page-ai-tool-write-completed', {
detail: {
toolName: toolName,
normalizedToolName: normalizedTool,
documentId: documentId,
workspaceId: workspaceId,
runId: String(toolEvent && (toolEvent.run_id || toolEvent.runId) || runId || ''),
traceId: String(toolEvent && (toolEvent.trace_id || toolEvent.traceId) || runTraceId || ''),
toolCallId: String(toolEvent && (toolEvent.tool_call_id || toolEvent.toolCallId || toolEvent.id) || '')
}
}));
} catch (error) {
console.warn('mnote 页面 AI 写入刷新事件派发失败', error);
}
}
function pageAiApplyToolEvent(eventName, payloadText, runId, runTraceId) {
var toolEvent = null;
try {
toolEvent = JSON.parse(payloadText || 'null');
} catch (_) {
toolEvent = {};
}
var rawToolName = toolEvent && (toolEvent.name || toolEvent.tool || toolEvent.toolName || '');
var toolName = String(rawToolName || eventName);
var toolCallId = String(toolEvent && (toolEvent.tool_call_id || toolEvent.toolCallId || toolEvent.id) || (runId + ':' + toolName));
var eventStatus = String(toolEvent && toolEvent.status || '').trim();
var status = eventName === 'tool.completed'
? (toolEvent && toolEvent.error ? 'failed' : 'completed')
: (eventName === 'tool.failed' ? 'failed' : (eventStatus || 'running'));
if (status === 'in_progress' || status === 'pending') status = 'running';
var traceId = String(toolEvent && (toolEvent.trace_id || toolEvent.traceId) || runTraceId || '');
var auditId = String(toolEvent && (toolEvent.audit_id || toolEvent.auditId) || '');
var argsSummary = pageAiPreviewValue(toolEvent && (toolEvent.arguments || toolEvent.args || toolEvent.input));
var resultSource = toolEvent && (toolEvent.summary || toolEvent.result || toolEvent.output);
if (!resultSource && status === 'failed') {
resultSource = [toolEvent && toolEvent.code, toolEvent && toolEvent.error].filter(Boolean).join(' ');
}
var resultSummary = pageAiPreviewValue(resultSource);
pageUiState.pageAiLastToolCall = {
event: eventName,
name: toolName,
runId: String(toolEvent && (toolEvent.run_id || toolEvent.runId) || runId),
traceId: traceId,
auditId: auditId
};
var rawLocations = toolEvent && toolEvent.locations;
var 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; }) : [];
var existing = pageUiState.pageAiMessages.find(function(item) {
return item.role === 'tool' && item.toolCallId === toolCallId;
});
if (!rawToolName && existing && existing.toolName) toolName = existing.toolName;
if (!existing) {
existing = {
role: 'tool',
content: toolName,
toolCallId: toolCallId,
toolName: toolName,
toolKind: String(toolEvent && toolEvent.kind || ''),
status: status,
argsSummary: '',
resultSummary: '',
locations: locations,
traceId: traceId,
auditId: auditId
};
pageUiState.pageAiMessages.push(existing);
}
existing.content = toolName;
existing.toolName = toolName;
existing.toolKind = String(toolEvent && toolEvent.kind || existing.toolKind || '');
existing.status = status;
existing.traceId = traceId || existing.traceId || '';
existing.auditId = auditId || existing.auditId || '';
if (argsSummary) existing.argsSummary = argsSummary;
if (resultSummary) existing.resultSummary = resultSummary;
if (locations.length) existing.locations = locations;
if (status === 'completed') {
pageAiNotifyToolWriteCompleted(toolName, toolEvent, runId, runTraceId);
}
}
async function pageAiCancelQueuedRun(queueId) {
queueId = String(queueId || '').trim();
if (!queueId) return;
var item = pageUiState.pageAiQueuedItems.find(function(entry) {
return entry.queueId === queueId;
});
var sessionId = String(item && item.sessionId || pageUiState.pageAiActiveSessionId || '').trim();
if (!sessionId) return;
try {
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/queue/' + encodeURIComponent(queueId), {
method: 'DELETE',
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'queue_cancel_failed_' + response.status));
pageUiState.pageAiQueuedItems = pageUiState.pageAiQueuedItems.filter(function(entry) {
return entry.queueId !== queueId;
});
var queueLength = Number(payload && (payload.queueLength || payload.queue_length || 0));
pageUiState.pageAiQueueLength = Number.isFinite(queueLength) ? Math.max(0, queueLength) : pageUiState.pageAiQueuedItems.length;
pageUiState.pageAiMessages.push({ role: 'assistant', content: '已取消一条 AI 队列项。' });
} catch (error) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: '取消 AI 队列项失败:' + (error instanceof Error ? error.message : String(error))
});
}
renderPageAiConversation();
renderPageAiControls();
}
function pageAiSetContextScope(scope) {
var next = String(scope || '').trim() || 'page';
pageUiState.pageAiContextScope = next;
document.documentElement.setAttribute('data-mnote-page-ai-context-scope', next);
}
function pageAiRunStatusLabel(status) {
if (status === 'queued') return '排队中';
if (status === 'running') return '运行中';
if (status === 'tool_calling') return '调用工具';
if (status === 'completed') return '已完成';
if (status === 'failed') return '失败';
if (status === 'aborted') return '已停止';
return '空闲';
}
function pageAiMemoryFileLabel(section) {
if (section === 'soul') return 'SOUL.md';
if (section === 'user') return 'USER.md';
return 'MEMORY.md';
}
function pageAiContextScopeLabel(scope) {
if (scope === 'selection') return '当前选区';
if (scope === 'block') return '当前块';
if (scope === 'options') return '页面设置';
return '当前页';
}
function pageAiHermesSettingsUrl() {
var configured = String(window.__mnoteHermesSettingsUrl || '').trim();
return configured || '';
}
function pageAiOpenHermesSettings() {
var url = pageAiHermesSettingsUrl();
if (url) {
window.open(url, '_blank', 'noopener,noreferrer');
return;
}
pageUiState.pageAiProfileError = '未配置 Hermes 设置入口:请设置 MNOTE_WEB_HERMES_UPSTREAM_URL。';
renderPageAiControls();
}
function pageAiNormalizeTools(payload) {
var upstream = pageAiUnwrapUpstream(payload) || {};
var tools = pageAiNormalizeArray(upstream.tools || upstream);
return tools.map(function(tool) {
var name = String(tool && (tool.name || tool.toolName || tool.tool) || '').trim();
if (!name) return null;
return {
name: name,
description: String(tool && tool.description || '').trim(),
scope: String(tool && (tool.scope || tool.requiredScope || '') || '').trim(),
kind: String(tool && (tool.kind || tool.mode || '') || '').trim(),
status: String(tool && (tool.status || tool.permission || 'available') || '').trim(),
enabled: tool && tool.enabled !== false,
unavailableReason: String(tool && (tool.unavailableReason || tool.reason || '') || '').trim()
};
}).filter(Boolean);
}
function pageAiErrorMessage(payload, fallback) {
if (!payload || typeof payload !== 'object') return fallback;
return String(payload.message || payload.error || payload.code || fallback || '').trim() || fallback;
}
function pageAiNormalizeGatewayHealth(payload) {
var upstream = pageAiUnwrapUpstream(payload) || payload || {};
return {
ok: Boolean(upstream.ok),
profile: upstream.profile || null,
gateway: upstream.gateway || null,
suggestions: pageAiNormalizeArray(upstream.suggestions).map(function(item) {
return String(item || '').trim();
}).filter(Boolean)
};
}
function pageAiSetDraftForSection(section, value) {
pageUiState.pageAiProfileMemoryDrafts[section] = String(value == null ? '' : value);
}
function pageAiApplyProfileMemory(payload) {
var upstream = pageAiUnwrapUpstream(payload) || {};
pageUiState.pageAiProfileMemory = {
memory: String(upstream.memory || ''),
user: String(upstream.user || ''),
soul: String(upstream.soul || '')
};
pageUiState.pageAiProfileMemoryDrafts = {
memory: pageUiState.pageAiProfileMemory.memory,
user: pageUiState.pageAiProfileMemory.user,
soul: pageUiState.pageAiProfileMemory.soul
};
}
async function pageAiLoadTools() {
try {
var response = await fetch('/api/hermes/client/tools?scope=mnote&profile=' + encodeURIComponent(pageAiCurrentProfile()), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'tools_failed_' + response.status));
pageUiState.pageAiTools = pageAiNormalizeTools(payload);
pageUiState.pageAiToolsError = '';
} catch (error) {
pageUiState.pageAiToolsError = error instanceof Error ? error.message : String(error);
}
renderPageAiControls();
}
async function pageAiLoadGatewayHealth() {
try {
var response = await fetch('/api/hermes/client/gateway/health?profile=' + encodeURIComponent(pageAiCurrentProfile()), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'gateway_health_failed_' + response.status));
pageUiState.pageAiGatewayHealth = pageAiNormalizeGatewayHealth(payload);
pageUiState.pageAiGatewayHealthError = '';
} catch (error) {
pageUiState.pageAiGatewayHealthError = error instanceof Error ? error.message : String(error);
}
renderPageAiControls();
}
async function pageAiStopRun() {
var runId = String(pageUiState.pageAiCurrentRunId || '').trim();
if (!runId || pageUiState.pageAiRunStatus === 'idle' || pageUiState.pageAiRunStatus === 'completed') return;
pageUiState.pageAiStoppedRunIds[runId] = true;
try {
var response = await fetch('/api/hermes/client/runs/' + encodeURIComponent(runId) + '/abort', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
sessionId: pageUiState.pageAiActiveSessionId,
profile: pageAiRunProfile(),
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
reason: 'page_ai_user_stop'
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'run_abort_failed_' + response.status));
pageAiApplyRuntimeState(payload && payload.runtime);
pageAiSetRunStatus('aborted', runId);
pageUiState.pageAiMessages.push({ role: 'assistant', content: '已请求停止当前 AI run。' });
} catch (error) {
pageAiSetRunStatus('failed', runId);
pageUiState.pageAiMessages.push({
role: 'assistant',
content: '停止 AI run 失败:' + (error instanceof Error ? error.message : String(error))
});
}
renderPageAiControls();
renderPageAiConversation();
}
function pageAiFilteredSkillEntries() {
var query = String(pageUiState.pageAiSkillQuery || '').trim().toLowerCase();
return pageAiSkillListEntries().filter(function(skill) {
if (!query) return true;
return String(skill.name || '').toLowerCase().indexOf(query) >= 0
|| String(skill.description || '').toLowerCase().indexOf(query) >= 0
|| String(skill.category || '').toLowerCase().indexOf(query) >= 0
|| pageAiSkillOriginLabel(skill).toLowerCase().indexOf(query) >= 0;
});
}
function pageAiSkillOriginLabel(skill) {
var origin = String(skill && skill.origin || '').trim();
if (origin === 'generated' || String(skill && skill.createdBy || '') === 'agent') return '生成';
if (origin === 'installed') return '安装';
if (origin === 'builtin') return '内置';
if (origin === 'copied') return '本地';
var source = String(skill && skill.source || '').trim();
if (source === 'hub') return '安装';
if (source === 'builtin') return '内置';
if (source === 'reasonix') {
if (origin === 'project') return 'Reasonix 项目';
if (origin === 'global') return 'Reasonix 全局';
return 'Reasonix';
}
return '本地';
}
async function pageAiLoadProfiles() {
try {
var response = await fetch('/api/hermes/client/profiles', {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'profiles_failed_' + response.status));
var profiles = pageAiNormalizeProfiles(payload);
pageUiState.pageAiProfiles = profiles.length ? profiles : [{ name: 'mnoteai', active: true }];
var acpRuntimes = Array.isArray(payload.acpRuntimes) ? payload.acpRuntimes : [];
pageUiState.pageAiAcpRuntimes = pageAiNormalizeAcpRuntimes(acpRuntimes);
var current = pageAiCurrentProfile();
var hasCurrent = pageUiState.pageAiProfiles.some(function(profile) { return pageAiProfileValue(profile) === current; });
var hasMnoteAi = pageUiState.pageAiProfiles.some(function(profile) { return pageAiProfileValue(profile) === 'mnoteai'; });
var active = pageAiProfileValue(pageUiState.pageAiProfiles.find(function(profile) { return profile.active; }));
pageAiSetActiveProfile(hasCurrent && current !== 'default' ? current : (hasMnoteAi ? 'mnoteai' : (active || current)));
pageUiState.pageAiProfileError = '';
void pageAiLoadGatewayHealth();
} catch (error) {
pageUiState.pageAiProfileError = error instanceof Error ? error.message : String(error);
if (!pageUiState.pageAiProfiles.length) pageUiState.pageAiProfiles = [{ name: 'mnoteai', active: true }];
pageUiState.pageAiAcpRuntimes = pageAiNormalizeAcpRuntimes(pageUiState.pageAiAcpRuntimes);
pageAiSetActiveProfile(pageAiCurrentProfile());
}
renderPageAiProviderButtons();
renderPageAiControls();
}
async function pageAiSwitchProfile(profileName) {
var next = String(profileName || '').trim();
if (!next) return;
pageAiSetActiveProfile(next);
renderPageAiProviderButtons();
renderPageAiControls();
try {
var response = await fetch('/api/hermes/client/profiles/active', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: next })
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'profile_switch_failed_' + response.status));
pageUiState.pageAiSessions = pageUiState.pageAiSessions.filter(function(session) {
return session && session.profile === next;
});
pageUiState.pageAiActiveSessionId = '';
pageUiState.pageAiMessages = [];
pageAiPersistSessions();
await pageAiEnsureHermesSession(true);
await pageAiLoadProfileMemory();
await pageAiLoadSkills();
await pageAiLoadTools();
await pageAiLoadGatewayHealth();
renderPageAiControls();
} catch (error) {
pageUiState.pageAiProfileError = error instanceof Error ? error.message : String(error);
}
renderPageAiConversation();
}
async function pageAiLoadProfileMemory() {
try {
var response = await fetch('/api/hermes/client/profile-memory?profile=' + encodeURIComponent(pageAiCurrentProfile()), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'profile_memory_failed_' + response.status));
pageAiApplyProfileMemory(payload);
pageUiState.pageAiProfileMemoryError = '';
} catch (error) {
pageUiState.pageAiProfileMemoryError = error instanceof Error ? error.message : String(error);
}
renderPageAiControls();
renderPageAiConversation();
}
async function pageAiSaveProfileMemory(section) {
var normalized = String(section || '').trim();
if (['memory', 'user', 'soul'].indexOf(normalized) < 0) return;
var content = String(pageUiState.pageAiProfileMemoryDrafts[normalized] || '');
try {
var response = await fetch('/api/hermes/client/profile-memory', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
profile: pageAiCurrentProfile(),
section: normalized,
content: content
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'profile_memory_save_failed_' + response.status));
pageUiState.pageAiProfileMemory[normalized] = content;
pageUiState.pageAiProfileMemoryError = '';
document.documentElement.setAttribute('data-mnote-page-ai-memory-saved', normalized);
} catch (error) {
pageUiState.pageAiProfileMemoryError = error instanceof Error ? error.message : String(error);
}
renderPageAiControls();
renderPageAiConversation();
}
async function pageAiLoadSkills() {
try {
var runtime = String(pageUiState.pageAiAcpRuntime || 'reasonix').trim();
var params = runtime === 'reasonix'
? 'runtime=reasonix'
: 'profile=' + encodeURIComponent(pageAiCurrentProfile());
var response = await fetch('/api/hermes/client/skills?' + params, {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'skills_failed_' + response.status));
pageUiState.pageAiSkills = pageAiNormalizeSkills(payload);
pageUiState.pageAiSkillError = '';
} catch (error) {
pageUiState.pageAiSkillError = error instanceof Error ? error.message : String(error);
pageUiState.pageAiSkills = { categories: [], archived: [] };
}
renderPageAiControls();
renderPageAiConversation();
}
async function pageAiToggleSkill(skillName, enabled) {
var name = String(skillName || '').trim();
if (!name) return;
if (String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix') return;
var previous = null;
pageAiSkillListEntries().forEach(function(skill) {
if (skill.name === name && previous == null) previous = skill.enabled !== false;
});
try {
var response = await fetch('/api/hermes/client/skills/toggle', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
profile: pageAiCurrentProfile(),
name: name,
enabled: Boolean(enabled)
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'skill_toggle_failed_' + response.status));
pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
pageAiNormalizeArray(category.skills).forEach(function(skill) {
if (skill.name === name) skill.enabled = Boolean(enabled);
});
});
document.documentElement.setAttribute('data-mnote-page-ai-skill-toggled', name);
pageUiState.pageAiSkillError = '';
} catch (error) {
pageUiState.pageAiSkillError = error instanceof Error ? error.message : String(error);
if (previous != null) {
pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
pageAiNormalizeArray(category.skills).forEach(function(skill) {
if (skill.name === name) skill.enabled = previous;
});
});
}
}
renderPageAiControls();
renderPageAiConversation();
}
async function pageAiToggleTool(toolName, enabled) {
var name = String(toolName || '').trim();
if (!name) return;
var previous = null;
pageAiNormalizeArray(pageUiState.pageAiTools).forEach(function(tool) {
if (tool.name === name && previous == null) previous = tool.enabled !== false;
});
try {
var response = await fetch('/api/hermes/client/tools/toggle', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
profile: pageAiCurrentProfile(),
name: name,
enabled: Boolean(enabled)
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'tool_toggle_failed_' + response.status));
pageAiNormalizeArray(pageUiState.pageAiTools).forEach(function(tool) {
if (tool.name === name) {
tool.enabled = Boolean(enabled);
tool.status = Boolean(enabled) ? 'available' : 'disabled';
tool.unavailableReason = Boolean(enabled) ? '' : '当前 Hermes profile 已关闭该 mnote tool';
}
});
document.documentElement.setAttribute('data-mnote-page-ai-tool-toggled', name);
pageUiState.pageAiToolsError = '';
} catch (error) {
pageUiState.pageAiToolsError = error instanceof Error ? error.message : String(error);
if (previous != null) {
pageAiNormalizeArray(pageUiState.pageAiTools).forEach(function(tool) {
if (tool.name === name) tool.enabled = previous;
});
}
}
renderPageAiControls();
}
function renderPageAiProviderButtons() {
var drawer = ensurePageAiDrawer();
var isAcp = pageUiState.pageAiAcpRuntime !== '';
drawer.querySelectorAll('[data-page-ai-provider]').forEach(function(button) {
var provider = button.getAttribute('data-page-ai-provider') || 'hermes';
var active = provider === pageUiState.pageAiProvider;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', active ? 'true' : 'false');
});
var providerNode = drawer.querySelector('.wolai-page-ai-subtitle span:first-child');
if (providerNode instanceof HTMLElement) {
providerNode.textContent = pageAiAgentRecord(pageAiCurrentAgentId()).label;
}
}
function renderPageAiControls() {
var drawer = ensurePageAiDrawer();
var isAcp = pageUiState.pageAiAcpRuntime !== '';
var activeAgentId = pageAiCurrentAgentId();
var activeProfile = pageAiRunProfile();
drawer.setAttribute('data-page-ai-page', pageUiState.pageAiPage || 'chat');
drawer.setAttribute('data-page-ai-active-agent-id', activeAgentId);
drawer.setAttribute('data-mnote-acp-runtime', pageUiState.pageAiAcpRuntime || 'reasonix');
document.documentElement.setAttribute('data-mnote-page-ai-agent-id', activeAgentId);
var agentSelector = drawer.querySelector('[data-page-ai-agent-selector]');
if (agentSelector instanceof HTMLElement) {
agentSelector.innerHTML = PAGE_AI_AGENT_REGISTRY.map(function(agent) {
var active = agent.id === activeAgentId;
return '<button type="button" class="wolai-page-ai-tab' + (active ? ' is-active' : '') + '" data-page-ai-agent-id="' + escapeHtml(agent.id) + '" aria-pressed="' + (active ? 'true' : 'false') + '">' + escapeHtml(agent.label) + '</button>';
}).join('');
}
var contextRefs = drawer.querySelector('[data-page-ai-context-refs]');
if (contextRefs instanceof HTMLElement) {
var selectedRefs = pageAiEnsureContextRefState();
contextRefs.innerHTML = PAGE_AI_CONTEXT_REF_REGISTRY.map(function(ref) {
var active = selectedRefs[ref.id] !== false && selectedRefs[ref.id] === true;
return '<button type="button" class="wolai-page-ai-ghost' + (active ? ' is-active' : '') + '" data-page-ai-context-ref="' + escapeHtml(ref.id) + '" aria-pressed="' + (active ? 'true' : 'false') + '">' + escapeHtml(ref.label) + '</button>';
}).join('');
}
var allowedRootsNode = drawer.querySelector('[data-page-ai-allowed-roots]');
if (allowedRootsNode instanceof HTMLElement) {
var allowedRoots = pageAiBuildAllowedRoots();
if (!allowedRoots.length) {
var errorText = String(pageUiState.pageAiAllowedRootsError || '').trim();
allowedRootsNode.innerHTML = '<span class="wolai-page-ai-tool-meta">' + escapeHtml(errorText || '未选择授权区域') + '</span>';
} else {
allowedRootsNode.innerHTML = allowedRoots.map(function(root) {
var label = root.rootUri.replace(/^file:\/\//, '') || root.rootUri;
return '<span class="wolai-page-ai-model-chip is-active" data-page-ai-allowed-root="' + escapeHtml(root.rootUri) + '" data-page-ai-allowed-root-permission="' + escapeHtml(root.permission) + '">' + escapeHtml(root.permission + ' · ' + label) + '</span>';
}).join('');
}
}
// Populate ACP runtime dropdown
var acpSelect = drawer.querySelector('[data-page-ai-acp-runtime]');
if (acpSelect instanceof HTMLSelectElement) {
var runtimes = pageUiState.pageAiAcpRuntimes.length ? pageUiState.pageAiAcpRuntimes : pageAiNormalizeAcpRuntimes([]);
acpSelect.innerHTML = runtimes.map(function(rt) {
return '<option value="' + escapeHtml(rt.name || '') + '"' + ((rt.name || '') === pageUiState.pageAiAcpRuntime ? ' selected' : '') + '>' + escapeHtml(rt.title || rt.name) + '</option>';
}).join('');
acpSelect.value = pageUiState.pageAiAcpRuntime || 'reasonix';
}
// Show/hide Hermes-specific profile select
var profileLabel = drawer.querySelector('[data-page-ai-hermes-profile]');
if (profileLabel instanceof HTMLElement) {
profileLabel.style.display = pageUiState.pageAiAcpRuntime === 'reasonix' ? 'none' : '';
}
// When ACP is selected, populate agent panel with ACP runtime info
var agentPanel = drawer.querySelector('[data-page-ai-agent-panel]');
if (agentPanel instanceof HTMLElement) {
agentPanel.innerHTML = '' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">授权区域</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(pageAiBuildAllowedRoots().length ? 'SQLite directory_grants' : '需要授权') + '</div></div>' +
'</section>' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">默认上下文</div><div class="wolai-page-ai-memory-scope">ContextRefs</div></div>' +
'</section>' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">审计</div><div class="wolai-page-ai-memory-scope">changed files 默认摘要</div></div>' +
'</section>';
}
var profileSummary = drawer.querySelector('[data-page-ai-profile-summary]');
if (profileSummary instanceof HTMLElement) profileSummary.textContent = '上下文可选';
var profileSummary = drawer.querySelector('[data-page-ai-profile-summary]');
if (profileSummary instanceof HTMLElement) profileSummary.textContent = '上下文可选';
drawer.querySelectorAll('[data-page-ai-profile-select]').forEach(function(profileSelect) {
if (!(profileSelect instanceof HTMLSelectElement)) return;
var profiles = pageUiState.pageAiProfiles.length ? pageUiState.pageAiProfiles : [{ name: activeProfile, active: true }];
profileSelect.innerHTML = profiles.map(function(profile) {
var name = pageAiProfileValue(profile) || 'default';
var model = [profile.model, profile.gateway].filter(Boolean).join(' / ');
var label = [name, profile.alias, model].filter(Boolean).join(' · ');
return '<option value="' + escapeHtml(name) + '"' + (name === activeProfile ? ' selected' : '') + '>' + escapeHtml(label) + '</option>';
}).join('');
profileSelect.value = activeProfile;
});
var runStatus = drawer.querySelector('[data-page-ai-run-status]');
if (runStatus instanceof HTMLElement) {
var queueSuffix = pageUiState.pageAiQueueLength > 0 ? ' · 队列 ' + pageUiState.pageAiQueueLength : '';
runStatus.textContent = pageAiRunStatusLabel(pageUiState.pageAiRunStatus) + queueSuffix;
}
var stopButton = drawer.querySelector('[data-page-ai-action="stop-run"]');
if (stopButton instanceof HTMLButtonElement) {
var canStop = ['queued', 'running', 'tool_calling'].indexOf(pageUiState.pageAiRunStatus) >= 0 && pageUiState.pageAiCurrentRunId;
stopButton.disabled = !canStop;
stopButton.setAttribute('aria-disabled', canStop ? 'false' : 'true');
}
var settingsLink = drawer.querySelector('[data-page-ai-action="open-hermes-settings"]');
if (settingsLink instanceof HTMLButtonElement) {
settingsLink.disabled = !pageAiHermesSettingsUrl();
settingsLink.title = pageAiHermesSettingsUrl() ? '打开 Hermes 设置' : '未配置 MNOTE_WEB_HERMES_UPSTREAM_URL';
}
var queueList = drawer.querySelector('[data-page-ai-queue-list]');
if (queueList instanceof HTMLElement) {
if (!pageUiState.pageAiQueuedItems.length) {
queueList.innerHTML = '<div class="wolai-page-ai-empty">暂无排队项。</div>';
} else {
queueList.innerHTML = pageUiState.pageAiQueuedItems.map(function(item, index) {
var label = '队列 ' + (index + 1);
return '' +
'<div class="wolai-page-ai-tool-row" data-page-ai-queue-item="' + escapeHtml(item.queueId) + '">' +
'<div><strong>' + escapeHtml(label) + '</strong><br /><span>' + escapeHtml(item.queueId) + '</span></div>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="cancel-queued-run" data-page-ai-queue-id="' + escapeHtml(item.queueId) + '">取消</button>' +
'</div>';
}).join('');
}
}
var sessionNode = drawer.querySelector('[data-page-ai-session-status]');
if (sessionNode instanceof HTMLElement) {
var session = pageAiCurrentSession();
var usageText = session && session.usage ? pageAiUsageSummary(session.usage) : '';
sessionNode.textContent = session && session.id
? [String(session.title || '当前页问答'), pageAiSessionStorageLabel(session), usageText].filter(Boolean).join(' · ')
: '等待 AI session';
}
var modelNode = drawer.querySelector('[data-page-ai-model-status]');
if (modelNode instanceof HTMLElement) modelNode.textContent = pageAiBuildAllowedRoots().length ? '已授权' : '需授权';
var scopeSelect = drawer.querySelector('[data-page-ai-context-scope]');
if (scopeSelect instanceof HTMLSelectElement) scopeSelect.value = pageUiState.pageAiContextScope;
var scopeLabel = drawer.querySelector('[data-page-ai-context-scope-label]');
if (scopeLabel instanceof HTMLElement) scopeLabel.textContent = pageAiContextScopeLabel(pageUiState.pageAiContextScope);
drawer.querySelectorAll('[data-page-ai-tab]').forEach(function(button) {
var target = button.getAttribute('data-page-ai-tab') || 'chat';
var active = target === pageUiState.pageAiPage;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', active ? 'true' : 'false');
});
drawer.querySelectorAll('[data-page-ai-panel]').forEach(function(panel) {
if (panel instanceof HTMLElement) {
panel.hidden = panel.getAttribute('data-page-ai-panel') !== pageUiState.pageAiPage;
}
});
var profileError = drawer.querySelector('[data-page-ai-profile-error]');
if (profileError instanceof HTMLElement) {
profileError.textContent = pageUiState.pageAiProfileError || '';
profileError.hidden = !pageUiState.pageAiProfileError;
}
var memoryError = drawer.querySelector('[data-page-ai-memory-error]');
if (memoryError instanceof HTMLElement) {
var memoryErrorText = pageUiState.pageAiProfileMemoryError || '';
if (memoryErrorText && memoryErrorText === pageUiState.pageAiProfileError) memoryErrorText = '';
memoryError.textContent = memoryErrorText;
memoryError.hidden = !memoryErrorText;
}
var hermesMemoryPanel = drawer.querySelector('[data-page-ai-hermes-panel]');
if (hermesMemoryPanel instanceof HTMLElement) {
hermesMemoryPanel.innerHTML = ['soul', 'user', 'memory'].map(function(section) {
var label = pageAiMemoryFileLabel(section);
var value = pageUiState.pageAiProfileMemoryDrafts[section] || '';
return '' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head">' +
'<div>' +
'<div class="wolai-page-ai-memory-title">' + escapeHtml(label) + '</div>' +
'<div class="wolai-page-ai-memory-scope">保存到 Hermes profile: ' + escapeHtml(activeProfile) + '</div>' +
'</div>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-memory-save="' + escapeHtml(section) + '">保存</button>' +
'</div>' +
'<textarea class="wolai-page-ai-memory-editor" data-page-ai-memory-editor="' + escapeHtml(section) + '" spellcheck="false">' + escapeHtml(value) + '</textarea>' +
'</section>';
}).join('');
}
var skillError = drawer.querySelector('[data-page-ai-skill-error]');
if (skillError instanceof HTMLElement) {
skillError.textContent = pageUiState.pageAiSkillError || '';
skillError.hidden = !pageUiState.pageAiSkillError;
}
var sessionError = drawer.querySelector('[data-page-ai-session-error]');
if (sessionError instanceof HTMLElement) {
sessionError.textContent = pageUiState.pageAiSessionError || '';
sessionError.hidden = !pageUiState.pageAiSessionError;
}
var sessionSearch = drawer.querySelector('[data-page-ai-session-search]');
if (sessionSearch instanceof HTMLInputElement && document.activeElement !== sessionSearch) {
sessionSearch.value = pageUiState.pageAiSessionSearchQuery || '';
}
var skillSearch = drawer.querySelector('[data-page-ai-skill-search]');
if (skillSearch instanceof HTMLInputElement && document.activeElement !== skillSearch) {
skillSearch.value = pageUiState.pageAiSkillQuery;
}
var skillList = drawer.querySelector('[data-page-ai-skill-list]');
if (skillList instanceof HTMLElement) {
var skills = pageAiFilteredSkillEntries();
if (!skills.length) {
var emptyText = String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix'
? '没有匹配的 Reasonix skill。'
: '没有匹配的 Hermes skill。';
skillList.innerHTML = '<div class="wolai-page-ai-empty">' + escapeHtml(emptyText) + '</div>';
} else {
skillList.innerHTML = skills.map(function(skill) {
var sourceText = pageAiSkillOriginLabel(skill) + (skill.modified ? ' · modified' : '');
var description = String(skill.description || '').trim();
var hasDescription = description && description !== '---' && description !== '无描述';
var canToggle = skill.toggleable !== false && String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() !== 'reasonix';
return '' +
'<div class="wolai-page-ai-skill-row">' +
'<div class="wolai-page-ai-skill-copy">' +
'<div class="wolai-page-ai-skill-main">' +
'<div class="wolai-page-ai-skill-name">' + escapeHtml(skill.name) + '</div>' +
'<div class="wolai-page-ai-skill-source">' + escapeHtml(sourceText) + '</div>' +
'</div>' +
(hasDescription ? '<div class="wolai-page-ai-skill-desc">' + escapeHtml(description) + '</div>' : '') +
'</div>' +
'<button type="button" class="wolai-page-ai-skill-switch' + (skill.enabled !== false ? ' is-on' : '') + '" data-page-ai-skill-toggle="' + escapeHtml(skill.name) + '" aria-pressed="' + (skill.enabled !== false ? 'true' : 'false') + '"' + (canToggle ? '' : ' disabled title="Reasonix skills 当前为只读展示"') + '>' +
'<span></span>' +
'</button>' +
'</div>';
}).join('');
}
}
var toolsList = drawer.querySelector('[data-page-ai-tool-list]');
if (toolsList instanceof HTMLElement) {
var tools = pageAiNormalizeArray(pageUiState.pageAiTools);
if (!tools.length) {
toolsList.innerHTML = '<div class="wolai-page-ai-empty">尚未读取到 mnote tool manifest。</div>';
} else {
toolsList.innerHTML = tools.map(function(tool) {
return '' +
'<div class="wolai-page-ai-tool-row">' +
'<div class="wolai-page-ai-skill-copy">' +
'<div class="wolai-page-ai-tool-name">' + escapeHtml(tool.name) + '</div>' +
'<div class="wolai-page-ai-tool-meta">' + escapeHtml(tool.scope || tool.kind || 'mnote') + ' · ' + escapeHtml(tool.status || 'available') + '</div>' +
(tool.unavailableReason ? '<div class="wolai-page-ai-tool-meta">' + escapeHtml(tool.unavailableReason) + '</div>' : '') +
'</div>' +
'<button type="button" class="wolai-page-ai-skill-switch' + (tool.enabled !== false ? ' is-on' : '') + '" data-page-ai-tool-toggle="' + escapeHtml(tool.name) + '" aria-pressed="' + (tool.enabled !== false ? 'true' : 'false') + '">' +
'<span></span>' +
'</button>' +
'</div>';
}).join('');
}
}
var gatewayError = drawer.querySelector('[data-page-ai-gateway-error]');
if (gatewayError instanceof HTMLElement) {
gatewayError.textContent = pageUiState.pageAiGatewayHealthError || '';
gatewayError.hidden = !pageUiState.pageAiGatewayHealthError;
}
var gatewayStatusNode = drawer.querySelector('[data-page-ai-gateway-status]');
var gatewayDetail = drawer.querySelector('[data-page-ai-gateway-detail]');
var gatewayHealth = pageUiState.pageAiGatewayHealth;
if (gatewayStatusNode instanceof HTMLElement) {
if (!gatewayHealth) {
gatewayStatusNode.textContent = '未检查';
} else {
var gateway = gatewayHealth.gateway || {};
var profile = gatewayHealth.profile || {};
gatewayStatusNode.textContent = gatewayHealth.ok ? '可用' : '需要设置';
if (gateway.status) gatewayStatusNode.textContent += ' · ' + gateway.status;
if (profile.name) gatewayStatusNode.textContent += ' · ' + profile.name;
}
}
if (gatewayDetail instanceof HTMLElement) {
if (!gatewayHealth) {
gatewayDetail.innerHTML = '<div class="wolai-page-ai-empty">打开高级后会检查 agent runtime 与当前 profile。</div>';
} else {
var gatewayInfo = gatewayHealth.gateway || {};
var profileInfo = gatewayHealth.profile || {};
var suggestionText = pageAiNormalizeArray(gatewayHealth.suggestions).join('');
gatewayDetail.innerHTML = '' +
'<div class="wolai-page-ai-tool-row">' +
'<div class="wolai-page-ai-tool-name">' + escapeHtml(profileInfo.name || activeProfile) + '</div>' +
'<div class="wolai-page-ai-tool-meta">model.default: ' + escapeHtml(profileInfo.modelDefault || '未设置') + '</div>' +
'<div class="wolai-page-ai-tool-meta">provider: ' + escapeHtml(profileInfo.provider || '未设置') + ' · API key: ' + escapeHtml(profileInfo.apiKeyConfigured ? '已配置' : '未检测到') + '</div>' +
'</div>' +
'<div class="wolai-page-ai-tool-row">' +
'<div class="wolai-page-ai-tool-name">' + escapeHtml(gatewayInfo.upstream || '未配置 upstream') + '</div>' +
'<div class="wolai-page-ai-tool-meta">gateway: ' + escapeHtml(gatewayInfo.status || 'unknown') + (gatewayInfo.httpStatus ? ' · HTTP ' + escapeHtml(gatewayInfo.httpStatus) : '') + '</div>' +
(suggestionText ? '<div class="wolai-page-ai-tool-meta">' + escapeHtml(suggestionText) + '</div>' : '') +
'</div>';
}
}
var toolError = drawer.querySelector('[data-page-ai-tool-error]');
if (toolError instanceof HTMLElement) {
toolError.textContent = pageUiState.pageAiToolsError || '';
toolError.hidden = !pageUiState.pageAiToolsError;
}
var lastTool = drawer.querySelector('[data-page-ai-last-tool]');
if (lastTool instanceof HTMLElement) {
var call = pageUiState.pageAiLastToolCall;
lastTool.textContent = call
? [call.event, call.name, call.runId, call.traceId || call.auditId].filter(Boolean).join(' · ')
: '暂无 tool call';
}
var reasonixPanel = drawer.querySelector('[data-page-ai-reasonix-panel]');
if (reasonixPanel instanceof HTMLElement) {
reasonixPanel.innerHTML = '<section class="wolai-page-ai-memory-card"><div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">Reasonix 专属设置</div><div class="wolai-page-ai-memory-scope">ACP runtime / skills</div></div></section>';
}
var chatOnlyPanel = drawer.querySelector('[data-page-ai-chat-only-panel]');
if (chatOnlyPanel instanceof HTMLElement) {
chatOnlyPanel.innerHTML = '<section class="wolai-page-ai-memory-card"><div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">Chat-only</div><div class="wolai-page-ai-memory-scope">默认不申请文件写权限</div></div></section>';
}
}
function humanizePageAiResponse(rawText, promptText) {
var providerLabel = pageAiProviderLabel(pageUiState.pageAiProvider);
var text = String(rawText || '').trim();
if (!text) {
return providerLabel + ' 已收到你的问题“' + searchText(promptText) + '”,但这次没有返回可读内容。';
}
if (text.startsWith('{')) {
try {
var payload = JSON.parse(text);
var operation = payload && payload.operation ? payload.operation : {};
var normalized = operation && operation.normalized_input ? operation.normalized_input : {};
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) + '”。当前已通过 Hermes 调用 mnote plugin 工具,目标页面是 ' + (documentId || '当前页面') + ',本次选择的工具是 ' + toolName + '。如果你继续追问,我会沿当前页面上下文继续回复。';
} catch (_) {
return providerLabel + ' 已返回结果,但当前结果是结构化文本,已为你保留原始内容。';
}
}
return text;
}
function ensurePageAiDrawer() {
var existing = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
if (existing instanceof HTMLElement) return existing;
var drawer = document.createElement('aside');
drawer.className = 'wolai-page-ai-drawer';
drawer.setAttribute('data-testid', 'wolai-page-ai-drawer');
drawer.setAttribute('data-mnote-surface', 'page-ai');
drawer.hidden = true;
drawer.innerHTML = '' +
'<div class="wolai-page-ai-panel" role="dialog" aria-modal="false">' +
'<div class="wolai-page-ai-header">' +
'<div class="wolai-page-ai-header-copy">' +
'<h2 class="wolai-page-ai-title" data-title="page-ai-title">页面 AI</h2>' +
'<div class="wolai-page-ai-subtitle">' +
'<span>Hermes</span>' +
'<span data-page-ai-profile-summary>default</span>' +
'<span data-page-ai-model-status>由 Hermes 决定</span>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-header-actions">' +
'<button type="button" class="wolai-page-ai-icon" data-page-ai-tab="agent" aria-label="打开页面 AI 设置">⚙</button>' +
'<button type="button" class="wolai-page-ai-icon" data-page-ai-action="close" aria-label="关闭页面 AI">×</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-body">' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="chat">' +
'<div class="wolai-page-ai-chat-meta">' +
'<button type="button" class="wolai-page-ai-session-button" data-page-ai-action="history">' +
'<span data-page-ai-session-status>等待 AI session</span>' +
'</button>' +
'<span data-page-ai-context-scope-label>当前页</span>' +
'<span data-page-ai-run-status>空闲</span>' +
'</div>' +
'<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
'<div class="wolai-page-ai-suggestions">' +
'<div class="wolai-page-ai-suggestions-header">' +
'<span>推荐问题</span>' +
'<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>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="agent" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="agent" role="tab" aria-selected="true">Common</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="hermes-settings" role="tab" aria-selected="false">Hermes</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="reasonix-settings" role="tab" aria-selected="false">Reasonix</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="chat-only-settings" role="tab" aria-selected="false">Chat-only</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="runtime" role="tab" aria-selected="false">高级</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-profile-error hidden></div>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-memory-error hidden></div>' +
'<div class="wolai-page-ai-memory-grid" data-page-ai-agent-panel></div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="hermes-settings" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="agent" role="tab" aria-selected="false">Common</button>' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="hermes-settings" role="tab" aria-selected="true">Hermes</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="reasonix-settings" role="tab" aria-selected="false">Reasonix</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="chat-only-settings" role="tab" aria-selected="false">Chat-only</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="runtime" role="tab" aria-selected="false">高级</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-settings-grid">' +
'<label class="wolai-page-ai-profile-select" data-page-ai-hermes-profile>' +
'<span>Hermes profile</span>' +
'<select data-page-ai-profile-select></select>' +
'</label>' +
'</div>' +
'<button type="button" class="wolai-page-ai-settings-link" data-page-ai-action="open-hermes-settings">打开 Hermes 设置</button>' +
'<div class="wolai-page-ai-memory-grid" data-page-ai-hermes-panel></div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="reasonix-settings" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="agent" role="tab" aria-selected="false">Common</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="hermes-settings" role="tab" aria-selected="false">Hermes</button>' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="reasonix-settings" role="tab" aria-selected="true">Reasonix</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="chat-only-settings" role="tab" aria-selected="false">Chat-only</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="runtime" role="tab" aria-selected="false">高级</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-settings-grid">' +
'<label class="wolai-page-ai-profile-select">' +
'<span>ACP runtime</span>' +
'<select data-page-ai-acp-runtime></select>' +
'</label>' +
'</div>' +
'<div class="wolai-page-ai-memory-grid" data-page-ai-reasonix-panel></div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="chat-only-settings" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="agent" role="tab" aria-selected="false">Common</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="hermes-settings" role="tab" aria-selected="false">Hermes</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="reasonix-settings" role="tab" aria-selected="false">Reasonix</button>' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="chat-only-settings" role="tab" aria-selected="true">Chat-only</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="runtime" role="tab" aria-selected="false">高级</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-memory-grid" data-page-ai-chat-only-panel></div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="skills" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="agent" role="tab" aria-selected="false">Agent</button>' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="skills" role="tab" aria-selected="true">Skills</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="runtime" role="tab" aria-selected="false">Runtime</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-skills-toolbar">' +
'<label class="wolai-page-ai-skill-search">' +
'<span>搜索技能</span>' +
'<input type="search" data-page-ai-skill-search placeholder="搜索技能…" />' +
'</label>' +
'</div>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-skill-error hidden></div>' +
'<div class="wolai-page-ai-skill-list" data-page-ai-skill-list></div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="runtime" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="agent" role="tab" aria-selected="false">Agent</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="skills" role="tab" aria-selected="false">Skills</button>' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="runtime" role="tab" aria-selected="true">Runtime</button>' +
'</div>' +
'</div>' +
'<button type="button" class="wolai-page-ai-settings-link" data-page-ai-action="open-hermes-settings">打开 Hermes 设置</button>' +
'<label class="wolai-page-ai-context-select">' +
'<span>兼容上下文 scope</span>' +
'<select data-page-ai-context-scope>' +
'<option value="page">当前页</option>' +
'<option value="selection">当前选区</option>' +
'<option value="block">当前块</option>' +
'<option value="options">页面设置</option>' +
'</select>' +
'</label>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-gateway-error hidden></div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>gateway</span>' +
'<span data-page-ai-gateway-status>未检查</span>' +
'</div>' +
'<div class="wolai-page-ai-tool-list" data-page-ai-gateway-detail></div>' +
'</div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>queue</span>' +
'<span data-page-ai-queue-status>由 mnote-web 管理</span>' +
'</div>' +
'<div class="wolai-page-ai-tool-list" data-page-ai-queue-list></div>' +
'</div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>mnote tools</span>' +
'<span data-page-ai-last-tool>暂无 tool call</span>' +
'</div>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-tool-error hidden></div>' +
'<div class="wolai-page-ai-tool-list" data-page-ai-tool-list></div>' +
'</div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="history" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'</div>' +
'<label class="wolai-page-ai-skill-search">' +
'<span>搜索会话</span>' +
'<input type="search" data-page-ai-session-search placeholder="搜索 AI 会话…" />' +
'</label>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-session-error hidden></div>' +
'<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
'</section>' +
'</div>' +
'<div class="wolai-page-ai-footer">' +
'<div class="wolai-page-ai-composer">' +
'<div class="wolai-page-ai-agent-selector" data-page-ai-agent-selector></div>' +
'<div class="wolai-page-ai-context-refs" data-page-ai-context-refs></div>' +
'<div class="wolai-page-ai-allowed-roots" data-page-ai-allowed-roots></div>' +
'<textarea class="wolai-page-ai-input" data-page-ai-input rows="3" placeholder="输入消息…"></textarea>' +
'<div class="wolai-page-ai-composer-bar">' +
'<button type="button" class="wolai-page-ai-tool-button" data-page-ai-action="new-session" title="新会话"></button>' +
'<button type="button" class="wolai-page-ai-tool-button" data-page-ai-action="history" title="历史会话">⌕</button>' +
'<span class="wolai-page-ai-composer-spacer"></span>' +
'<button type="button" class="wolai-page-ai-stop" data-page-ai-action="stop-run" disabled>停止</button>' +
'<button type="button" class="wolai-page-ai-send" data-page-ai-action="send" aria-label="发送">发送</button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
document.body.appendChild(drawer);
return drawer;
}
function renderPageAiSuggestions() {
var drawer = ensurePageAiDrawer();
var list = drawer.querySelector('[data-page-ai-panel="chat"] [data-page-ai-suggestion-list]');
if (!(list instanceof HTMLElement)) return;
var container = list.closest('.wolai-page-ai-suggestions');
if (container instanceof HTMLElement) {
container.hidden = pageUiState.pageAiPage !== 'chat' || pageUiState.pageAiMessages.length > 0;
}
var suggestions = pageAiSuggestions();
var offset = pageUiState.pageAiSuggestionIndex % suggestions.length;
var ordered = suggestions.slice(offset).concat(suggestions.slice(0, offset)).slice(0, 3);
list.innerHTML = ordered.map(function(text) {
return '<button type="button" class="wolai-page-ai-suggestion" data-page-ai-suggestion="' + escapeHtml(text) + '">' + escapeHtml(text) + '</button>';
}).join('');
}
function renderPageAiConversation() {
var drawer = ensurePageAiDrawer();
renderPageAiSuggestions();
var conversation = drawer.querySelector('[data-page-ai-panel="' + cssEscape(pageUiState.pageAiPage || 'chat') + '"] [data-page-ai-conversation]');
if (!(conversation instanceof HTMLElement)) return;
if (pageUiState.pageAiPage === 'history') {
var historyRows = pageUiState.pageAiSessionSearchResults.length
? pageUiState.pageAiSessionSearchResults
: pageUiState.pageAiSessions;
if (!historyRows.length) {
conversation.innerHTML = '<div class="wolai-page-ai-empty">尚未产生历史会话。</div>';
return;
}
conversation.innerHTML = historyRows.map(function(session) {
var preview = Array.isArray(session.messages) && session.messages.length
? session.messages.slice(-1)[0].content
: (session.snippet || session.preview || '暂无消息');
var active = session.id === pageUiState.pageAiActiveSessionId;
var usage = pageAiUsageSummary(session.usage);
var meta = [pageAiSessionStorageLabel(session), session.permissionLevel, session.shareId, session.status, usage].filter(Boolean).join(' · ');
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--history' + (active ? ' is-active' : '') + '" data-page-ai-session-row="' + escapeHtml(session.id) + '">' +
'<button type="button" class="wolai-page-ai-message-text" data-page-ai-session="' + escapeHtml(session.id) + '">' +
'<strong>' + escapeHtml(session.title || '新会话') + '</strong><br />' +
'<span>' + escapeHtml(preview) + '</span>' +
(meta ? '<br /><span class="wolai-page-ai-tool-meta">' + escapeHtml(meta) + '</span>' : '') +
'</button>' +
'<div class="wolai-page-ai-message-actions">' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-resume="' + escapeHtml(session.id) + '">恢复</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-rename="' + escapeHtml(session.id) + '">重命名</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-delete="' + escapeHtml(session.id) + '">删除</button>' +
'</div>' +
'</div>';
}).join('');
return;
}
if (!pageUiState.pageAiMessages.length) {
conversation.innerHTML = '<div class="wolai-page-ai-empty">围绕当前页面提问,我会优先使用当前页内容、页面结构和已保存设置。</div>';
return;
}
conversation.innerHTML = pageUiState.pageAiMessages.map(function(item) {
var roleLabel = item.role === 'user' ? '你' : (item.role === 'tool' ? '工具' : 'AI');
if (item.role === 'tool') {
var statusLabel = item.status === 'completed' ? '完成' : (item.status === 'failed' ? '失败' : '运行中');
var locationRows = Array.isArray(item.locations) && item.locations.length
? '<div class="wolai-page-ai-tool-meta">位置:' + item.locations.map(function(loc, idx) {
return '<span style="display:inline-flex;align-items:center;gap:4px;margin-right:8px">' +
'<span>' + escapeHtml(loc) + '</span>' +
'<button type="button" class="wolai-page-ai-ghost" style="font-size:0.85em;padding:0 4px;min-width:unset" data-page-ai-open-location="' + escapeHtml(loc) + '" data-page-ai-tool-call-id="' + escapeHtml(item.toolCallId || '') + '" title="打开文件">打开</button>' +
'</span>';
}).join('') + '</div>'
: '';
var detailRows = [
locationRows,
item.argsSummary ? '<div class="wolai-page-ai-tool-meta">参数 ' + escapeHtml(item.argsSummary) + '</div>' : '',
item.resultSummary ? '<div class="wolai-page-ai-tool-meta">结果 ' + escapeHtml(item.resultSummary) + '</div>' : '',
item.changedFiles && item.changedFiles.length ? '<pre class="wolai-page-ai-tool-meta wolai-page-ai-changed-files">' + escapeHtml(pageAiFormatChangedFiles(item.changedFiles)) + '</pre>' : '',
(item.traceId || item.auditId) ? '<div class="wolai-page-ai-tool-meta">' + escapeHtml([item.traceId, item.auditId].filter(Boolean).join(' · ')) + '</div>' : ''
].filter(Boolean).join('');
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--tool" data-page-ai-tool-card data-page-ai-tool-call-id="' + escapeHtml(item.toolCallId || '') + '" data-page-ai-tool-status="' + escapeHtml(item.status || '') + '">' +
'<div class="wolai-page-ai-message-role">' + escapeHtml(roleLabel) + '</div>' +
'<div class="wolai-page-ai-message-text">' +
'<details class="wolai-page-ai-tool-details">' +
'<summary>' +
'<strong>' + escapeHtml(item.toolName || item.content || 'tool') + '</strong>' +
'<span>' + escapeHtml([statusLabel, item.toolKind, item.toolCallId].filter(Boolean).join(' · ')) + '</span>' +
'</summary>' +
(detailRows || '<div class="wolai-page-ai-tool-meta">暂无参数或结果详情。</div>') +
'</details>' +
'</div>' +
'</div>';
}
if (item.kind === 'thought') {
return '' +
'<details class="wolai-page-ai-message wolai-page-ai-message--assistant" data-page-ai-thought-delta="true">' +
'<summary class="wolai-page-ai-message-role" style="cursor:pointer;user-select:none;opacity:0.7;font-size:0.85em">思考过程</summary>' +
'<div class="wolai-page-ai-message-text" style="margin-top:4px;opacity:0.6;font-size:0.9em;border-left:2px solid var(--wolai-border-color, #ddd);padding-left:8px">' + escapeHtml(item.content || '') + '</div>' +
'</details>';
}
if (item.kind === 'permission') {
var permissionActions = item.resolved ? '' : (
'<div class="wolai-page-ai-message-actions">' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="allow" data-page-ai-permission-id="' + escapeHtml(item.permissionId || '') + '">允许</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="deny" data-page-ai-permission-id="' + escapeHtml(item.permissionId || '') + '">拒绝</button>' +
'</div>'
);
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--tool" data-page-ai-permission-request="' + escapeHtml(item.permissionId || '') + '">' +
'<div class="wolai-page-ai-message-role">权限</div>' +
'<div class="wolai-page-ai-message-text">' +
'<strong>' + escapeHtml(item.toolName || 'session/request_permission') + '</strong><br />' +
'<span>' + escapeHtml(item.argsSummary || item.content || '') + '</span>' +
permissionActions +
'</div>' +
'</div>';
}
if (item.kind === 'plan') {
var planEntries = Array.isArray(item.entries) ? item.entries : [];
var listHtml = planEntries.map(function(entry, idx) {
return '<li style="margin:2px 0">' + escapeHtml(String(entry || '')) + '</li>';
}).join('');
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--assistant" data-page-ai-plan="true">' +
'<details class="wolai-page-ai-plan-details" open>' +
'<summary class="wolai-page-ai-message-role" style="cursor:pointer;user-select:none;opacity:0.8;font-size:0.85em">执行计划 · ' + planEntries.length + ' 步</summary>' +
'<div class="wolai-page-ai-message-text" style="margin-top:4px;opacity:0.75;font-size:0.9em;border-left:2px solid var(--wolai-border-color, #ddd);padding-left:8px">' +
'<ol style="margin:4px 0;padding-left:20px">' + listHtml + '</ol>' +
'</div>' +
'</details>' +
'</div>';
}
var streamingAttr = item.streaming ? ' data-page-ai-streaming="true"' : '';
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--' + escapeHtml(item.role || 'assistant') + '"' + streamingAttr + '>' +
'<div class="wolai-page-ai-message-role">' + escapeHtml(roleLabel) + '</div>' +
'<div class="wolai-page-ai-message-text">' + escapeHtml(item.content || '') + '</div>' +
'</div>';
}).join('');
conversation.scrollTop = conversation.scrollHeight;
}
function openPageAiDrawer() {
pageUiState.pageAiAgentId = pageAiCurrentAgentId();
pageAiEnsureContextRefState();
pageAiLoadSessions();
renderPageAiSuggestions();
renderPageAiConversation();
renderPageAiProviderButtons();
renderPageAiControls();
var drawer = ensurePageAiDrawer();
drawer.hidden = false;
pageUiState.pageAiOpen = true;
updatePageAiTriggerState();
Promise.all([
pageAiLoadProfiles(),
pageAiLoadProfileMemory(),
pageAiLoadSkills(),
pageAiLoadTools(),
pageAiLoadGatewayHealth(),
pageAiLoadAiPreferences(),
pageAiLoadAllowedRoots(),
pageAiLoadBackendSessions()
]).then(function() {
renderPageAiControls();
}).catch(function() {}).then(function() {
return pageAiEnsureHermesSession();
}).then(function() {
return pageAiRestoreHermesSession();
}).then(function() {
renderPageAiControls();
}).catch(function(error) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: '当前 AI 不可用:' + (error instanceof Error ? error.message : String(error))
});
renderPageAiConversation();
renderPageAiControls();
});
}
function closePageAiDrawer() {
var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
if (drawer instanceof HTMLElement) drawer.hidden = true;
pageUiState.pageAiOpen = false;
updatePageAiTriggerState();
}
function isPageAiDrawerOpen() {
var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
return drawer instanceof HTMLElement && !drawer.hidden;
}
async function streamPageAiResponse(response, onEvent) {
if (!response.body || typeof response.body.getReader !== 'function') return;
var reader = response.body.getReader();
var decoder = new TextDecoder();
var buffer = '';
while (true) {
var chunk = await reader.read();
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
var frames = buffer.split('\n\n');
buffer = frames.pop() || '';
frames.forEach(function(frame) {
var eventName = '';
var dataLines = [];
frame.split('\n').forEach(function(line) {
if (line.startsWith('event:')) eventName = line.slice(6).trim();
if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
});
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);
});
}
}
function pageAiDecodeDeltaText(payloadText) {
try {
var payload = JSON.parse(payloadText || 'null');
return String((payload && (payload.text || payload.delta)) || '');
} catch (_) {
return String(payloadText || '');
}
}
function pageAiEnsureStreamingAssistantMessage(runId) {
var id = String(runId || pageUiState.pageAiCurrentRunId || '');
var existing = pageUiState.pageAiMessages.find(function(item) {
return item.role === 'assistant' && item.streaming === true && item.runId === id;
});
if (existing) return existing;
existing = {
role: 'assistant',
content: '',
runId: id,
streaming: true
};
pageUiState.pageAiMessages.push(existing);
return existing;
}
function pageAiAppendStreamingAssistantDelta(runId, deltaText) {
var delta = String(deltaText || '');
if (!delta) return '';
var message = pageAiEnsureStreamingAssistantMessage(runId);
message.content = String(message.content || '') + delta;
pageAiSyncCurrentSessionMessages();
renderPageAiConversation();
return message.content;
}
function pageAiFinishStreamingAssistantMessage(runId, finalText, promptText) {
var id = String(runId || pageUiState.pageAiCurrentRunId || '');
var message = pageUiState.pageAiMessages.find(function(item) {
return item.role === 'assistant' && item.streaming === true && item.runId === id;
});
var text = String(finalText || (message && message.content) || '');
var content = humanizePageAiResponse(text, promptText);
if (message) {
message.content = content;
message.streaming = false;
} else if (content) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: content,
runId: id
});
}
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
}
function pageAiLooksLikeBlockEdit(prompt) {
var text = searchText(prompt);
return ['新增', '添加', '插入', '删除', '删掉', '修改', '替换', '改成', '移动', '移到', 'move', 'replace', 'delete', 'insert'].some(function(word) {
return text.indexOf(word) >= 0;
});
}
async function pageAiTryBlockEditWorkflow(prompt, scopedContext, runTargetSnapshot) {
if (currentSourceKind() === 'local_folder') return false;
if (!pageAiLooksLikeBlockEdit(prompt)) return false;
var runId = 'page-ai-fast-' + Date.now().toString(36);
var traceId = 'page-ai-fast-' + Date.now().toString(36);
pageAiSetRunStatus('running', runId);
renderPageAiControls();
var started = Date.now();
var response = await fetch('/api/page-ai/block-edit-workflow', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
sessionId: pageUiState.pageAiActiveSessionId,
runId: runId,
profile: pageAiCurrentProfile(),
model: pageAiMnoteToolModel(),
message: prompt,
pageContext: scopedContext.pageContext,
editorTarget: scopedContext.editorTarget,
runTargetSnapshot: runTargetSnapshot || null,
selectedBlockId: scopedContext.selectedBlockId,
selectedText: scopedContext.selectedText,
traceId: traceId
})
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok || !payload || payload.ok !== true) {
var code = payload && payload.code ? String(payload.code) : '';
if (code === 'page_ai_workflow_not_block_edit') return false;
pageUiState.pageAiMessages.push({
role: 'tool',
toolName: 'mnote.page_ai.block_edit_workflow',
status: 'failed',
toolCallId: runId,
resultSummary: pageAiErrorMessage(payload, 'fast_path_failed_' + response.status)
});
renderPageAiConversation();
pageAiSetRunStatus('failed', runId);
renderPageAiControls();
return true;
}
pageUiState.pageAiMessages.push({
role: 'tool',
toolName: 'mnote.page_ai.block_edit_workflow',
status: 'completed',
toolCallId: runId,
resultSummary: 'operations=' + String(Array.isArray(payload.operations) ? payload.operations.length : 0) + ' · ' + String(Date.now() - started) + 'ms'
});
pageUiState.pageAiMessages.push({
role: 'assistant',
content: payload.message || '已通过页面块编辑快路径完成写入。'
});
pageAiSetRunStatus('completed', runId);
try {
window.dispatchEvent(new CustomEvent('mnote:page-ai-tool-write-completed', {
detail: {
toolName: 'mnote.doc.apply_block_ops',
normalizedToolName: 'mnote.doc.apply.block.ops',
documentId: currentDocumentId(),
workspaceId: resolveWorkspaceId(document.body),
runId: runId,
traceId: traceId,
toolCallId: runId
}
}));
} catch (_) {}
var currentSession = pageAiCurrentSession();
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
return true;
}
async function sendPageAiMessage(text) {
var allowQueue = ['running', 'tool_calling'].indexOf(pageUiState.pageAiRunStatus) >= 0;
if (pageUiState.pageAiBusy && !allowQueue) return;
var prompt = searchText(text);
if (!prompt) return;
if (!allowQueue) pageUiState.pageAiBusy = true;
var currentSession = null;
try {
await pageAiEnsureHermesSession();
if (!allowQueue) pageAiSetRunStatus('queued');
renderPageAiControls();
var contextSnapshot = currentPageAiContextSnapshot();
var scopedContext = pageAiScopedPageContext(contextSnapshot);
assertPageAiTargetInCurrentWorkspace(scopedContext.editorTarget);
await assertPageAiTargetWritable(scopedContext.editorTarget);
var runTargetSnapshot = pageAiBuildRunTargetSnapshot(scopedContext, prompt);
if (currentSourceKind() === 'local_folder' && !pageAiNormalizeArray(pageUiState.pageAiAllowedRoots).length) {
await pageAiLoadAllowedRoots();
}
var contextRefs = pageAiBuildContextRefs(scopedContext, runTargetSnapshot);
var allowedRoots = pageAiBuildAllowedRoots();
if (scopedContext.pageContext && scopedContext.pageContext.aiContext) {
scopedContext.pageContext.aiContext.runTargetSnapshot = runTargetSnapshot;
}
var requestPageContext = pageAiPageContextForRefs(scopedContext.pageContext, contextRefs);
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();
}
pageAiPersistSessions();
renderPageAiConversation();
if (!allowQueue && await pageAiTryBlockEditWorkflow(prompt, scopedContext, runTargetSnapshot)) {
return;
}
var response = await fetch('/api/hermes/client/runs', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
agentId: pageAiCurrentAgentId(),
sessionId: pageUiState.pageAiActiveSessionId,
profile: pageAiRunProfile(),
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
contextScope: pageUiState.pageAiContextScope,
contextRefs: contextRefs,
allowedRoots: allowedRoots,
message: prompt,
model: pageAiMnoteToolModel(),
pageContext: requestPageContext,
editorTarget: scopedContext.editorTarget,
runTargetSnapshot: runTargetSnapshot,
selectedBlockId: scopedContext.selectedBlockId,
selectedText: pageAiContextKindsFromRefs(contextRefs).selection ? scopedContext.selectedText : '',
traceId: 'page-ai-run-' + Date.now().toString(36)
})
});
if (!response.ok) {
var errorPayload = await response.json().catch(function(){ return null; });
throw new Error(pageAiErrorMessage(errorPayload, 'page_ai_failed_' + response.status));
}
var runPayload = await response.json().catch(function(){ return null; });
if (pageAiApplyQueuedRun(runPayload)) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: '已加入 AI 队列,前一条 run 完成后继续处理。'
});
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
return;
}
if (allowQueue) {
throw new Error('hermes_queue_expected_queued_response');
}
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 runTraceId = String((upstream && (upstream.trace_id || upstream.traceId)) || (runPayload && (runPayload.trace_id || runPayload.traceId)) || '');
pageAiSetRunStatus('running', runId);
pageAiSetRunTargetSnapshot(Object.assign({}, runTargetSnapshot, {
runId: runId,
sessionId: pageUiState.pageAiActiveSessionId,
traceId: runTraceId
}));
renderPageAiControls();
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(pageAiErrorMessage(eventError, 'hermes_events_failed_' + eventResponse.status));
}
var assistantText = '';
await streamPageAiResponse(eventResponse, function(eventName, payloadText) {
if (eventName === 'message.delta') {
if (pageUiState.pageAiStoppedRunIds[runId]) return;
assistantText = pageAiAppendStreamingAssistantDelta(runId, pageAiDecodeDeltaText(payloadText));
}
if (eventName === 'thought.delta') {
try {
var thoughtPayload = JSON.parse(payloadText || 'null');
var thoughtText = String((thoughtPayload && (thoughtPayload.delta || thoughtPayload.text)) || '');
if (thoughtText) {
var msgs = pageUiState.pageAiMessages;
var lastThought = msgs.length > 0 && msgs[msgs.length - 1].kind === 'thought' ? msgs[msgs.length - 1] : null;
if (lastThought) {
lastThought.content += thoughtText;
} else {
msgs.push({ role: 'assistant', kind: 'thought', content: thoughtText });
}
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
}
} catch (_) {}
}
if (eventName === 'usage.updated') {
try {
var usagePayload = JSON.parse(payloadText || 'null') || {};
var sessionForUsage = pageAiCurrentSession();
if (sessionForUsage) {
sessionForUsage.usage = {
source: 'usage_update',
used: Number(usagePayload.used ?? usagePayload.contextUsed ?? 0),
size: Number(usagePayload.size ?? usagePayload.contextSize ?? 0)
};
pageAiPersistSessions();
}
} catch (_) {}
renderPageAiControls();
}
if (eventName === 'permission.requested' || eventName === 'permission.denied' || eventName === 'permission.allowed') {
pageAiApplyPermissionEvent(eventName, payloadText);
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
}
if (eventName === 'run.completed') {
try {
var completed = JSON.parse(payloadText || 'null');
if (completed && completed.output) assistantText = String(completed.output || '');
if (completed && completed.usage) {
var completedSession = pageAiCurrentSession();
if (completedSession) completedSession.usage = completed.usage;
}
var agentAudit = completed && completed.agentAudit && typeof completed.agentAudit === 'object' ? completed.agentAudit : null;
var changedFiles = pageAiNormalizeArray(agentAudit && agentAudit.changedFiles).map(function(file) {
if (!file || typeof file !== 'object') return file;
return Object.assign({
actorId: String(agentAudit && agentAudit.actorId || ''),
actorType: String(agentAudit && agentAudit.actorType || ''),
agentKind: String(agentAudit && agentAudit.agentKind || ''),
runId: runId
}, file);
});
if (changedFiles.length) {
pageUiState.pageAiMessages.push({
role: 'tool',
content: 'agent.changed_files',
toolCallId: runId + ':agent.changed_files',
toolName: 'agent.changed_files',
toolKind: 'audit',
status: 'completed',
argsSummary: [agentAudit.rootUri, agentAudit.actorId, agentAudit.agentKind].map(function(value) {
return String(value || '').trim();
}).filter(Boolean).join(' · '),
resultSummary: String(agentAudit.diffSummary || changedFiles.length + ' changed file(s)'),
changedFiles: changedFiles,
traceId: runTraceId,
auditId: String(agentAudit.eventId || '')
});
try {
var currentId = currentDocumentId();
var currentPath = String(currentId || '').replace(/^local-md:/, '').replace(/~2F/g, '/');
var touchesCurrent = changedFiles.some(function(file) {
var path = String(file && (file.documentId || file.path || file.filePath || '') || '');
return path === currentId || (currentPath && path.indexOf(currentPath) >= 0);
});
if (touchesCurrent) {
window.dispatchEvent(new CustomEvent('mnote:page-ai-tool-write-completed', {
detail: {
toolName: 'agent.changed_files',
normalizedToolName: 'agent.changed_files',
documentId: currentId,
workspaceId: resolveWorkspaceId(document.body),
runId: runId,
traceId: runTraceId,
toolCallId: runId + ':agent.changed_files'
}
}));
}
} catch (_) {}
}
} catch (_) {}
pageAiSetRunStatus('completed', runId);
}
if (eventName === 'run.failed') {
try {
var failed = JSON.parse(payloadText || 'null');
assistantText = String((failed && (failed.message || failed.code || failed.error)) || 'AI run failed');
} catch (_) {}
pageAiSetRunStatus('failed', runId);
}
if (eventName === 'run.aborted' || eventName === 'run.cancelled' || eventName === 'run.canceled') {
pageUiState.pageAiStoppedRunIds[runId] = true;
pageAiSetRunStatus('aborted', runId);
}
if (eventName === 'session.info.updated') {
try {
var infoPayload = JSON.parse(payloadText || 'null') || {};
var newTitle = String(infoPayload.title || '').trim();
if (newTitle) {
var sessionForTitle = pageAiCurrentSession();
if (sessionForTitle) {
sessionForTitle.title = newTitle;
sessionForTitle.updatedAt = Date.now();
pageAiPersistSessions();
renderPageAiControls();
}
}
} catch (_) {}
}
if (eventName === 'plan.updated') {
try {
var planPayload = JSON.parse(payloadText || 'null') || {};
var planEntries = Array.isArray(planPayload.entries) ? planPayload.entries : [];
if (planEntries.length) {
var planMsgs = pageUiState.pageAiMessages;
var existingPlan = planMsgs.length > 0 && planMsgs[planMsgs.length - 1].kind === 'plan' ? planMsgs[planMsgs.length - 1] : null;
if (existingPlan) {
existingPlan.entries = planEntries;
existingPlan.updatedAt = Date.now();
} else {
planMsgs.push({ role: 'system', kind: 'plan', entries: planEntries, createdAt: Date.now(), updatedAt: Date.now() });
}
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
}
} catch (_) {}
}
if (eventName === 'tool.started' || eventName === 'tool.completed' || eventName === 'tool.failed') {
pageAiApplyToolEvent(eventName, payloadText, runId, runTraceId);
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
pageAiSetRunStatus('tool_calling', runId);
renderPageAiControls();
}
});
if (!pageUiState.pageAiStoppedRunIds[runId]) {
pageAiFinishStreamingAssistantMessage(runId, assistantText, prompt);
}
currentSession = pageAiCurrentSession();
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
} catch (error) {
pageAiSetRunStatus('failed');
pageUiState.pageAiMessages.push({
role: 'assistant',
content: 'AI 当前请求失败:' + (error instanceof Error ? error.message : String(error))
});
currentSession = pageAiCurrentSession();
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
} finally {
if (!allowQueue) pageUiState.pageAiBusy = false;
renderPageAiConversation();
renderPageAiControls();
}
}
return {
openPageAiDrawer: (...args) => openPageAiDrawer(...args),
closePageAiDrawer: (...args) => closePageAiDrawer(...args),
isPageAiDrawerOpen: (...args) => isPageAiDrawerOpen(...args),
ensurePageAiDrawer: (...args) => ensurePageAiDrawer(...args),
sendPageAiMessage: (...args) => sendPageAiMessage(...args),
pageAiOpenHermesSettings: (...args) => pageAiOpenHermesSettings(...args),
pageAiStopRun: (...args) => pageAiStopRun(...args),
pageAiLoadGatewayHealth: (...args) => pageAiLoadGatewayHealth(...args),
renderPageAiControls: (...args) => renderPageAiControls(...args),
renderPageAiConversation: (...args) => renderPageAiConversation(...args),
renderPageAiProviderButtons: (...args) => renderPageAiProviderButtons(...args),
renderPageAiSuggestions: (...args) => renderPageAiSuggestions(...args),
pageAiSaveProfileMemory: (...args) => pageAiSaveProfileMemory(...args),
pageAiToggleSkill: (...args) => pageAiToggleSkill(...args),
pageAiToggleTool: (...args) => pageAiToggleTool(...args),
pageAiResumeBackendSession: (...args) => pageAiResumeBackendSession(...args),
pageAiRenameBackendSession: (...args) => pageAiRenameBackendSession(...args),
pageAiDeleteBackendSession: (...args) => pageAiDeleteBackendSession(...args),
pageAiResolvePermission: (...args) => pageAiResolvePermission(...args),
pageAiOpenLocation: (...args) => pageAiOpenLocation(...args),
pageAiSetActiveSession: (...args) => pageAiSetActiveSession(...args),
pageAiStartNewSession: (...args) => pageAiStartNewSession(...args),
pageAiLoadSessions: (...args) => pageAiLoadSessions(...args),
pageAiLoadBackendSessions: (...args) => pageAiLoadBackendSessions(...args),
pageAiCancelQueuedRun: (...args) => pageAiCancelQueuedRun(...args),
pageAiSearchBackendSessions: (...args) => pageAiSearchBackendSessions(...args),
pageAiPersistSessions: (...args) => pageAiPersistSessions(...args),
pageAiLoadProfiles: (...args) => pageAiLoadProfiles(...args),
pageAiLoadProfileMemory: (...args) => pageAiLoadProfileMemory(...args),
pageAiLoadSkills: (...args) => pageAiLoadSkills(...args),
pageAiSwitchProfile: (...args) => pageAiSwitchProfile(...args),
pageAiSetContextScope: (...args) => pageAiSetContextScope(...args),
pageAiSetAgentId: (...args) => pageAiSetAgentId(...args),
pageAiToggleContextRef: (...args) => pageAiToggleContextRef(...args),
pageAiLoadAllowedRoots: (...args) => pageAiLoadAllowedRoots(...args),
updatePageAiTriggerState: (...args) => updatePageAiTriggerState(...args)
};
}