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 = ['']; 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(' ' + escapeHtml(pageAiBlockText(block)) + ''); }); lines.push(''); 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 = '' + ''; 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 协议直连 Reasonix(DeepSeek 缓存优先)', 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 ''; }).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 ''; }).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 = '' + escapeHtml(errorText || '未选择授权区域') + ''; } else { allowedRootsNode.innerHTML = allowedRoots.map(function(root) { var label = root.rootUri.replace(/^file:\/\//, '') || root.rootUri; return '' + escapeHtml(root.permission + ' · ' + label) + ''; }).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 ''; }).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 = '' + '
' + '
授权区域
' + escapeHtml(pageAiBuildAllowedRoots().length ? 'SQLite directory_grants' : '需要授权') + '
' + '
' + '
' + '
默认上下文
ContextRefs
' + '
' + '
' + '
审计
changed files 默认摘要
' + '
'; } 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 ''; }).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 = '
暂无排队项。
'; } else { queueList.innerHTML = pageUiState.pageAiQueuedItems.map(function(item, index) { var label = '队列 ' + (index + 1); return '' + '
' + '
' + escapeHtml(label) + '
' + escapeHtml(item.queueId) + '
' + '' + '
'; }).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 '' + '
' + '
' + '
' + '
' + escapeHtml(label) + '
' + '
保存到 Hermes profile: ' + escapeHtml(activeProfile) + '
' + '
' + '' + '
' + '' + '
'; }).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 = '
' + escapeHtml(emptyText) + '
'; } 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 '' + '
' + '
' + '
' + '
' + escapeHtml(skill.name) + '
' + '
' + escapeHtml(sourceText) + '
' + '
' + (hasDescription ? '
' + escapeHtml(description) + '
' : '') + '
' + '' + '
'; }).join(''); } } var toolsList = drawer.querySelector('[data-page-ai-tool-list]'); if (toolsList instanceof HTMLElement) { var tools = pageAiNormalizeArray(pageUiState.pageAiTools); if (!tools.length) { toolsList.innerHTML = '
尚未读取到 mnote tool manifest。
'; } else { toolsList.innerHTML = tools.map(function(tool) { return '' + '
' + '
' + '
' + escapeHtml(tool.name) + '
' + '
' + escapeHtml(tool.scope || tool.kind || 'mnote') + ' · ' + escapeHtml(tool.status || 'available') + '
' + (tool.unavailableReason ? '
' + escapeHtml(tool.unavailableReason) + '
' : '') + '
' + '' + '
'; }).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 = '
打开高级后会检查 agent runtime 与当前 profile。
'; } else { var gatewayInfo = gatewayHealth.gateway || {}; var profileInfo = gatewayHealth.profile || {}; var suggestionText = pageAiNormalizeArray(gatewayHealth.suggestions).join(';'); gatewayDetail.innerHTML = '' + '
' + '
' + escapeHtml(profileInfo.name || activeProfile) + '
' + '
model.default: ' + escapeHtml(profileInfo.modelDefault || '未设置') + '
' + '
provider: ' + escapeHtml(profileInfo.provider || '未设置') + ' · API key: ' + escapeHtml(profileInfo.apiKeyConfigured ? '已配置' : '未检测到') + '
' + '
' + '
' + '
' + escapeHtml(gatewayInfo.upstream || '未配置 upstream') + '
' + '
gateway: ' + escapeHtml(gatewayInfo.status || 'unknown') + (gatewayInfo.httpStatus ? ' · HTTP ' + escapeHtml(gatewayInfo.httpStatus) : '') + '
' + (suggestionText ? '
' + escapeHtml(suggestionText) + '
' : '') + '
'; } } 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 = '
Reasonix 专属设置
ACP runtime / skills
'; } var chatOnlyPanel = drawer.querySelector('[data-page-ai-chat-only-panel]'); if (chatOnlyPanel instanceof HTMLElement) { chatOnlyPanel.innerHTML = '
Chat-only
默认不申请文件写权限
'; } } 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 = '' + ''; 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 ''; }).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 = '
尚未产生历史会话。
'; 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 '' + '
' + '' + '
' + '' + '' + '' + '
' + '
'; }).join(''); return; } if (!pageUiState.pageAiMessages.length) { conversation.innerHTML = '
围绕当前页面提问,我会优先使用当前页内容、页面结构和已保存设置。
'; 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 ? '
位置:' + item.locations.map(function(loc, idx) { return '' + '' + escapeHtml(loc) + '' + '' + ''; }).join('') + '
' : ''; var detailRows = [ locationRows, item.argsSummary ? '
参数 ' + escapeHtml(item.argsSummary) + '
' : '', item.resultSummary ? '
结果 ' + escapeHtml(item.resultSummary) + '
' : '', item.changedFiles && item.changedFiles.length ? '
' + escapeHtml(pageAiFormatChangedFiles(item.changedFiles)) + '
' : '', (item.traceId || item.auditId) ? '
' + escapeHtml([item.traceId, item.auditId].filter(Boolean).join(' · ')) + '
' : '' ].filter(Boolean).join(''); return '' + '
' + '
' + escapeHtml(roleLabel) + '
' + '
' + '
' + '' + '' + escapeHtml(item.toolName || item.content || 'tool') + '' + '' + escapeHtml([statusLabel, item.toolKind, item.toolCallId].filter(Boolean).join(' · ')) + '' + '' + (detailRows || '
暂无参数或结果详情。
') + '
' + '
' + '
'; } if (item.kind === 'thought') { return '' + '
' + '思考过程' + '
' + escapeHtml(item.content || '') + '
' + '
'; } if (item.kind === 'permission') { var permissionActions = item.resolved ? '' : ( '
' + '' + '' + '
' ); return '' + '
' + '
权限
' + '
' + '' + escapeHtml(item.toolName || 'session/request_permission') + '
' + '' + escapeHtml(item.argsSummary || item.content || '') + '' + permissionActions + '
' + '
'; } if (item.kind === 'plan') { var planEntries = Array.isArray(item.entries) ? item.entries : []; var listHtml = planEntries.map(function(entry, idx) { return '
  • ' + escapeHtml(String(entry || '')) + '
  • '; }).join(''); return '' + '
    ' + '
    ' + '执行计划 · ' + planEntries.length + ' 步' + '
    ' + '
      ' + listHtml + '
    ' + '
    ' + '
    ' + '
    '; } var streamingAttr = item.streaming ? ' data-page-ai-streaming="true"' : ''; return '' + '
    ' + '
    ' + escapeHtml(roleLabel) + '
    ' + '
    ' + escapeHtml(item.content || '') + '
    ' + '
    '; }).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) }; }