diff --git a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md
index 6f465e32..d59caf80 100644
--- a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md
+++ b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md
@@ -244,7 +244,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::tree -- --
拆分项:
- [x] E1. `style.rs`:迁出 `SPIKE_STYLE`,根 `lib.rs` 只引用常量。
-- [ ] E2. `mount.rs`:迁出 mount context、mounted handles、`mount_app_into` 周边可独立部分;`#[wasm_bindgen]` wrapper 可暂留根文件。
+- [x] E2. `mount.rs`:迁出 mount context、mounted handles、`mount_app_into` 周边可独立部分;`#[wasm_bindgen]` wrapper 可暂留根文件。
- [x] E3. `runtime_bridge.rs`:迁出 `dispatch_runtime_event`、`dispatch_*_to_target`、host command listener install。
- [x] E4. `block_transform.rs`:迁出 `run_block_turn_into_action`、`top_level_block_matches_action`、page/block transform helpers。
- [ ] E5. `slash_menu_view.rs`:迁出 slash menu Leptos view 和 click handling。
@@ -272,8 +272,8 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::web_shell
拆分项:
-- [ ] F1. 审计 `sidebar-tree-runtime.js` 中所有 Page AI / ACP / Hermes / Reasonix 字符串和函数。
-- [ ] F2. 纯 UI panel host 可以迁入 `browser/page-ai-panel-runtime.js`。
+- [x] F1. 审计 `sidebar-tree-runtime.js` 中所有 Page AI / ACP / Hermes / Reasonix 字符串和函数。
+- [x] F2. 纯 UI panel host 已迁入 `browser/sidebar-page-ai-runtime.js`。
- [ ] F3. ACP/Hermes session 行为不得迁入 tree runtime;需要新建或更新 `design/07-ai/process/*` checklist 后再实施。
验收命令:
diff --git a/rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js b/rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js
new file mode 100644
index 00000000..fb81db4d
--- /dev/null
+++ b/rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js
@@ -0,0 +1,2824 @@
+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;
+
+ 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 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(),
+ 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);
+ 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
+ },
+ 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();
+ return [changeType, path, summary].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: '已取消一条 Hermes 队列项。' });
+ } catch (error) {
+ pageUiState.pageAiMessages.push({
+ role: 'assistant',
+ content: '取消 Hermes 队列项失败:' + (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: '已请求 Hermes 停止当前 run。' });
+ } catch (error) {
+ pageAiSetRunStatus('failed', runId);
+ pageUiState.pageAiMessages.push({
+ role: 'assistant',
+ content: '停止 Hermes 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 = isAcp ? 'ACP · ' + (pageUiState.pageAiAcpRuntime === 'reasonix' ? 'Reasonix' : 'Hermes') : pageAiProviderLabel(pageUiState.pageAiProvider);
+ }
+ }
+
+ function renderPageAiControls() {
+ var drawer = ensurePageAiDrawer();
+ var isAcp = pageUiState.pageAiAcpRuntime !== '';
+ var activeProfile = pageAiRunProfile();
+ drawer.setAttribute('data-page-ai-page', pageUiState.pageAiPage || 'chat');
+ drawer.setAttribute('data-mnote-acp-runtime', pageUiState.pageAiAcpRuntime || 'reasonix');
+ // 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) {
+ if (pageUiState.pageAiAcpRuntime === 'reasonix') {
+ var rt = pageUiState.pageAiAcpRuntimes.find(function(r) { return r.name === pageUiState.pageAiAcpRuntime; }) || {};
+ var keyStatus = rt.apiKeyConfigured ? '已配置' : '未检测到 DEEPSEEK_API_KEY';
+ agentPanel.innerHTML = '' +
+ '' +
+ '' + escapeHtml(rt.title || 'ACP Runtime') + '
' +
+ '' +
+ '' +
+ '模型
' + escapeHtml(rt.model || 'deepseek-chat') + '
' +
+ '' +
+ '' +
+ 'Preset
' + escapeHtml(rt.preset || 'auto') + '
' +
+ '' +
+ '' +
+ 'API Key
' + escapeHtml(keyStatus) + '
' +
+ '' +
+ '' +
+ '描述
' + escapeHtml(rt.description || '') + '
' +
+ '';
+ }
+ }
+ var profileSummary = drawer.querySelector('[data-page-ai-profile-summary]');
+ if (profileSummary instanceof HTMLElement) profileSummary.textContent = activeProfile;
+ var profileSummary = drawer.querySelector('[data-page-ai-profile-summary]');
+ if (profileSummary instanceof HTMLElement) profileSummary.textContent = activeProfile;
+ var profileSelect = drawer.querySelector('[data-page-ai-profile-select]');
+ if (profileSelect instanceof HTMLSelectElement) {
+ 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(' · ')
+ : '等待 Hermes session';
+ }
+ var modelNode = drawer.querySelector('[data-page-ai-model-status]');
+ if (modelNode instanceof HTMLElement) modelNode.textContent = pageAiCurrentModelLabel();
+ 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 agentPanel = drawer.querySelector('[data-page-ai-agent-panel]');
+ if (agentPanel instanceof HTMLElement && pageUiState.pageAiAcpRuntime !== 'reasonix') {
+ agentPanel.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 = '打开 Runtime 后会检查 Hermes gateway 与当前 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';
+ }
+ }
+
+ 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 = '' +
+ '' +
+ '' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '当前页' +
+ '空闲' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ 'gateway' +
+ '未检查' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ 'queue' +
+ '由 mnote-web 管理' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ 'mnote tools' +
+ '暂无 tool call' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '
';
+ 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() {
+ pageAiLoadSessions();
+ renderPageAiSuggestions();
+ renderPageAiConversation();
+ renderPageAiProviderButtons();
+ renderPageAiControls();
+ var drawer = ensurePageAiDrawer();
+ drawer.hidden = false;
+ pageUiState.pageAiOpen = true;
+ updatePageAiTriggerState();
+ Promise.all([
+ pageAiLoadProfiles(),
+ pageAiLoadProfileMemory(),
+ pageAiLoadSkills(),
+ pageAiLoadTools(),
+ pageAiLoadGatewayHealth(),
+ 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: 'Hermes 当前不可用:' + (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) {
+ 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,
+ 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);
+ 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)) {
+ 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(),
+ sessionId: pageUiState.pageAiActiveSessionId,
+ profile: pageAiRunProfile(),
+ acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
+ contextScope: pageUiState.pageAiContextScope,
+ message: prompt,
+ model: pageAiMnoteToolModel(),
+ pageContext: scopedContext.pageContext,
+ selectedBlockId: scopedContext.selectedBlockId,
+ selectedText: 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: '已加入 Hermes 队列,前一条 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);
+ 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);
+ 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: String(agentAudit.rootUri || ''),
+ 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)) || 'Hermes 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: 'Hermes 当前请求失败:' + (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),
+ updatePageAiTriggerState: (...args) => updatePageAiTriggerState(...args)
+ };
+}
diff --git a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js
index f652df03..6d322941 100644
--- a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js
+++ b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js
@@ -5,12 +5,12 @@ import { createSidebarFileTreeOpenRuntime } from './sidebar-filetree-open-runtim
import { createSidebarAttachmentOpenRuntime } from './sidebar-attachment-open-runtime.js';
import { createSidebarFileTreeCommandRuntime } from './sidebar-filetree-command-runtime.js';
import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-runtime.js';
+import { createSidebarPageAiRuntime } from './sidebar-page-ai-runtime.js';
(function(){
if (window.__mnoteSidebarTreeRuntimeStarted) return;
window.__mnoteSidebarTreeRuntimeStarted = true;
- var PAGE_AI_SESSION_STORAGE_VERSION = 3;
var FILETREE_DRAG_MIME = 'application/x-mnote-filetree-row-ids';
var MNOTE_SIDEBAR_TREE_MODE_KEY = 'mnote.sidebar.tree.mode';
var MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX = 'mnote.localFolder.recentRoots:';
@@ -191,249 +191,6 @@ import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-ru
return parseJsonScript('__MNOTE_PAGE_AGGREGATE__') || {};
}
- 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 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(),
- 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);
- 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
- },
- selectedText: selectedText,
- selectedBlockId: aiContext.selectedBlockIds[0] || null
- };
- }
-
function defaultPageOptions() {
return {
wideLayout: false,
@@ -2244,13 +2001,6 @@ import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-ru
trigger.setAttribute('aria-expanded', pageUiState.pageSettingsOpen ? 'true' : 'false');
}
- 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 createPageOptionRow(key, type) {
var inputType = type || 'checkbox';
var supported = pageOptionIsSupported(key);
@@ -2418,2512 +2168,53 @@ import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-ru
document.documentElement.removeAttribute('data-mnote-page-share-open');
}
- 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 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();
- return [changeType, path, summary].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: '已取消一条 Hermes 队列项。' });
- } catch (error) {
- pageUiState.pageAiMessages.push({
- role: 'assistant',
- content: '取消 Hermes 队列项失败:' + (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: '已请求 Hermes 停止当前 run。' });
- } catch (error) {
- pageAiSetRunStatus('failed', runId);
- pageUiState.pageAiMessages.push({
- role: 'assistant',
- content: '停止 Hermes 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 = isAcp ? 'ACP · ' + (pageUiState.pageAiAcpRuntime === 'reasonix' ? 'Reasonix' : 'Hermes') : pageAiProviderLabel(pageUiState.pageAiProvider);
- }
- }
-
- function renderPageAiControls() {
- var drawer = ensurePageAiDrawer();
- var isAcp = pageUiState.pageAiAcpRuntime !== '';
- var activeProfile = pageAiRunProfile();
- drawer.setAttribute('data-page-ai-page', pageUiState.pageAiPage || 'chat');
- drawer.setAttribute('data-mnote-acp-runtime', pageUiState.pageAiAcpRuntime || 'reasonix');
- // 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) {
- if (pageUiState.pageAiAcpRuntime === 'reasonix') {
- var rt = pageUiState.pageAiAcpRuntimes.find(function(r) { return r.name === pageUiState.pageAiAcpRuntime; }) || {};
- var keyStatus = rt.apiKeyConfigured ? '已配置' : '未检测到 DEEPSEEK_API_KEY';
- agentPanel.innerHTML = '' +
- '' +
- '' + escapeHtml(rt.title || 'ACP Runtime') + '
' +
- '' +
- '' +
- '模型
' + escapeHtml(rt.model || 'deepseek-chat') + '
' +
- '' +
- '' +
- 'Preset
' + escapeHtml(rt.preset || 'auto') + '
' +
- '' +
- '' +
- 'API Key
' + escapeHtml(keyStatus) + '
' +
- '' +
- '' +
- '描述
' + escapeHtml(rt.description || '') + '
' +
- '';
- }
- }
- var profileSummary = drawer.querySelector('[data-page-ai-profile-summary]');
- if (profileSummary instanceof HTMLElement) profileSummary.textContent = activeProfile;
- var profileSummary = drawer.querySelector('[data-page-ai-profile-summary]');
- if (profileSummary instanceof HTMLElement) profileSummary.textContent = activeProfile;
- var profileSelect = drawer.querySelector('[data-page-ai-profile-select]');
- if (profileSelect instanceof HTMLSelectElement) {
- 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(' · ')
- : '等待 Hermes session';
- }
- var modelNode = drawer.querySelector('[data-page-ai-model-status]');
- if (modelNode instanceof HTMLElement) modelNode.textContent = pageAiCurrentModelLabel();
- 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 agentPanel = drawer.querySelector('[data-page-ai-agent-panel]');
- if (agentPanel instanceof HTMLElement && pageUiState.pageAiAcpRuntime !== 'reasonix') {
- agentPanel.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 = '打开 Runtime 后会检查 Hermes gateway 与当前 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';
- }
- }
-
- 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 = '' +
- '' +
- '' +
- '
' +
- '
' +
- '' +
- '' +
- '当前页' +
- '空闲' +
- '
' +
- '' +
- '' +
- '' +
- '
' +
- '' +
- '
' +
- '
' +
- '' +
- '' +
- '' +
- '
' +
- '
' +
- '' +
- '' +
- '' +
- '' +
- '
' +
- '' +
- '' +
- '' +
- '' +
- '
' +
- '' +
- '
' +
- '
' +
- '' +
- '' +
- '' +
- '
' +
- '
' +
- '' +
- '' +
- '
' +
- '' +
- '' +
- '' +
- '
' +
- '' +
- '
' +
- '
' +
- '' +
- '' +
- '' +
- '
' +
- '
' +
- '' +
- '' +
- '' +
- '
' +
- 'gateway' +
- '未检查' +
- '
' +
- '
' +
- '
' +
- '' +
- '
' +
- 'queue' +
- '由 mnote-web 管理' +
- '
' +
- '
' +
- '
' +
- '' +
- '
' +
- 'mnote tools' +
- '暂无 tool call' +
- '
' +
- '
' +
- '
' +
- '
' +
- '' +
- '
' +
- '' +
- '' +
- '
' +
- '' +
- '' +
- '' +
- '' +
- '
' +
- '' +
- '
';
- 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() {
- pageAiLoadSessions();
- renderPageAiSuggestions();
- renderPageAiConversation();
- renderPageAiProviderButtons();
- renderPageAiControls();
- var drawer = ensurePageAiDrawer();
- drawer.hidden = false;
- pageUiState.pageAiOpen = true;
- updatePageAiTriggerState();
- Promise.all([
- pageAiLoadProfiles(),
- pageAiLoadProfileMemory(),
- pageAiLoadSkills(),
- pageAiLoadTools(),
- pageAiLoadGatewayHealth(),
- 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: 'Hermes 当前不可用:' + (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) {
- 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,
- 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);
- 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)) {
- 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(),
- sessionId: pageUiState.pageAiActiveSessionId,
- profile: pageAiRunProfile(),
- acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
- contextScope: pageUiState.pageAiContextScope,
- message: prompt,
- model: pageAiMnoteToolModel(),
- pageContext: scopedContext.pageContext,
- selectedBlockId: scopedContext.selectedBlockId,
- selectedText: 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: '已加入 Hermes 队列,前一条 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);
- 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);
- 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: String(agentAudit.rootUri || ''),
- 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)) || 'Hermes 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: 'Hermes 当前请求失败:' + (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();
- }
- }
+ const sidebarPageAi = createSidebarPageAiRuntime({
+ buildLocalFileOpenUrl,
+ currentDocumentId,
+ currentPageAggregate,
+ currentPageOptions,
+ currentRootUri,
+ currentSourceKind,
+ escapeHtml,
+ cssEscape,
+ openLocalResourceInActiveTab,
+ pageUiState,
+ resolveWorkspaceId,
+ searchText,
+ });
+ const openPageAiDrawer = (...args) => sidebarPageAi.openPageAiDrawer(...args);
+ const closePageAiDrawer = (...args) => sidebarPageAi.closePageAiDrawer(...args);
+ const isPageAiDrawerOpen = (...args) => sidebarPageAi.isPageAiDrawerOpen(...args);
+ const ensurePageAiDrawer = (...args) => sidebarPageAi.ensurePageAiDrawer(...args);
+ const sendPageAiMessage = (...args) => sidebarPageAi.sendPageAiMessage(...args);
+ const pageAiOpenHermesSettings = (...args) => sidebarPageAi.pageAiOpenHermesSettings(...args);
+ const pageAiStopRun = (...args) => sidebarPageAi.pageAiStopRun(...args);
+ const pageAiLoadGatewayHealth = (...args) => sidebarPageAi.pageAiLoadGatewayHealth(...args);
+ const renderPageAiControls = (...args) => sidebarPageAi.renderPageAiControls(...args);
+ const renderPageAiConversation = (...args) => sidebarPageAi.renderPageAiConversation(...args);
+ const renderPageAiProviderButtons = (...args) => sidebarPageAi.renderPageAiProviderButtons(...args);
+ const renderPageAiSuggestions = (...args) => sidebarPageAi.renderPageAiSuggestions(...args);
+ const pageAiSaveProfileMemory = (...args) => sidebarPageAi.pageAiSaveProfileMemory(...args);
+ const pageAiToggleSkill = (...args) => sidebarPageAi.pageAiToggleSkill(...args);
+ const pageAiToggleTool = (...args) => sidebarPageAi.pageAiToggleTool(...args);
+ const pageAiResumeBackendSession = (...args) => sidebarPageAi.pageAiResumeBackendSession(...args);
+ const pageAiRenameBackendSession = (...args) => sidebarPageAi.pageAiRenameBackendSession(...args);
+ const pageAiDeleteBackendSession = (...args) => sidebarPageAi.pageAiDeleteBackendSession(...args);
+ const pageAiResolvePermission = (...args) => sidebarPageAi.pageAiResolvePermission(...args);
+ const pageAiOpenLocation = (...args) => sidebarPageAi.pageAiOpenLocation(...args);
+ const pageAiSetActiveSession = (...args) => sidebarPageAi.pageAiSetActiveSession(...args);
+ const pageAiStartNewSession = (...args) => sidebarPageAi.pageAiStartNewSession(...args);
+ const pageAiLoadSessions = (...args) => sidebarPageAi.pageAiLoadSessions(...args);
+ const pageAiLoadBackendSessions = (...args) => sidebarPageAi.pageAiLoadBackendSessions(...args);
+ const pageAiCancelQueuedRun = (...args) => sidebarPageAi.pageAiCancelQueuedRun(...args);
+ const pageAiSearchBackendSessions = (...args) => sidebarPageAi.pageAiSearchBackendSessions(...args);
+ const pageAiPersistSessions = (...args) => sidebarPageAi.pageAiPersistSessions(...args);
+ const pageAiLoadProfiles = (...args) => sidebarPageAi.pageAiLoadProfiles(...args);
+ const pageAiLoadProfileMemory = (...args) => sidebarPageAi.pageAiLoadProfileMemory(...args);
+ const pageAiLoadSkills = (...args) => sidebarPageAi.pageAiLoadSkills(...args);
+ const pageAiSwitchProfile = (...args) => sidebarPageAi.pageAiSwitchProfile(...args);
+ const pageAiSetContextScope = (...args) => sidebarPageAi.pageAiSetContextScope(...args);
+ const updatePageAiTriggerState = (...args) => sidebarPageAi.updatePageAiTriggerState(...args);
function ensurePageSettingsPopover() {
var existing = document.querySelector('[data-testid="wolai-page-settings-popover"]');
@@ -5488,18 +2779,11 @@ import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-ru
return;
}
- var pageAiOpenLocation = closestAction(e.target, '[data-page-ai-open-location]');
- if (pageAiOpenLocation) {
+ var pageAiOpenLocationAction = closestAction(e.target, '[data-page-ai-open-location]');
+ if (pageAiOpenLocationAction) {
e.preventDefault();
- var loc = String(pageAiOpenLocation.getAttribute('data-page-ai-open-location') || '').trim();
- if (loc) {
- openLocalResourceInActiveTab({ path: loc }).then(function(opened) {
- if (!opened) {
- var href = buildLocalFileOpenUrl(loc, false);
- if (href) window.open(href, '_blank', 'noopener,noreferrer');
- }
- });
- }
+ var loc = String(pageAiOpenLocationAction.getAttribute('data-page-ai-open-location') || '').trim();
+ if (loc) pageAiOpenLocation(loc);
return;
}
diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs
index 6b875a83..5c393d98 100644
--- a/rust/crates/mnote-web/src/routes/mod.rs
+++ b/rust/crates/mnote-web/src/routes/mod.rs
@@ -154,6 +154,10 @@ pub fn build_router(state: AppState) -> Router {
"/api/mnote-browser-runtime/sidebar-attachment-open-runtime.js",
get(web_shell::sidebar_attachment_open_runtime_asset),
)
+ .route(
+ "/api/mnote-browser-runtime/sidebar-page-ai-runtime.js",
+ get(web_shell::sidebar_page_ai_runtime_asset),
+ )
.route(
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
get(web_shell::sidebar_tree_runtime_asset),
@@ -650,6 +654,7 @@ mod tests {
"/api/mnote-browser-runtime/sidebar-filetree-command-runtime.js",
"/api/mnote-browser-runtime/sidebar-filetree-upload-runtime.js",
"/api/mnote-browser-runtime/sidebar-attachment-open-runtime.js",
+ "/api/mnote-browser-runtime/sidebar-page-ai-runtime.js",
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
"/api/mnote-browser-runtime/tree-live-controller.js",
"/api/mnote-browser-runtime/tree-shell-runtime.js",
diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs
index a6271c02..71ec971d 100644
--- a/rust/crates/mnote-web/src/routes/web_shell.rs
+++ b/rust/crates/mnote-web/src/routes/web_shell.rs
@@ -728,6 +728,20 @@ pub async fn sidebar_tree_runtime_asset() -> Response {
.unwrap_or_else(|_| Response::new(Body::empty()))
}
+pub async fn sidebar_page_ai_runtime_asset() -> Response {
+ const JS: &str = include_str!("../../browser/sidebar-page-ai-runtime.js");
+ Response::builder()
+ .status(StatusCode::OK)
+ .header(
+ header::CONTENT_TYPE,
+ "application/javascript; charset=utf-8",
+ )
+ .header(header::CACHE_CONTROL, "no-store")
+ .header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
+ .body(Body::from(JS))
+ .unwrap_or_else(|_| Response::new(Body::empty()))
+}
+
pub async fn sidebar_shell_runtime_asset() -> Response {
const JS: &str = include_str!("../../browser/sidebar-shell-runtime.js");
Response::builder()
diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs
index d9658832..10524ae9 100644
--- a/rust/crates/mnote-web/src/ssr/pages/layout.rs
+++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs
@@ -215,6 +215,8 @@ mod tests {
include_str!("../../../browser/sidebar-workspace-runtime.js");
const SIDEBAR_PAGE_TREE_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-tree-runtime.js");
+ const SIDEBAR_PAGE_AI_RUNTIME_JS: &str =
+ include_str!("../../../browser/sidebar-page-ai-runtime.js");
const FILETREE_CONTEXT_MENU_RUNTIME_JS: &str =
include_str!("../../../browser/filetree-context-menu-runtime.js");
const FILETREE_DND_RUNTIME_JS: &str = include_str!("../../../browser/filetree-dnd-runtime.js");
@@ -352,7 +354,7 @@ mod tests {
#[test]
fn page_ai_fast_path_is_not_local_first_main_path() {
assert!(
- SIDEBAR_TREE_RUNTIME_JS
+ SIDEBAR_PAGE_AI_RUNTIME_JS
.contains("if (currentSourceKind() === 'local_folder') return false;"),
"local-first 页面 AI 不应继续加厚 page-ai fast-path;本地编辑应走受控文件工具"
);
@@ -361,15 +363,15 @@ mod tests {
#[test]
fn page_ai_local_source_passes_file_reference_fields_to_agent_run() {
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function currentRootUri()"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sourceKind: currentSourceKind()"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("rootUri: currentRootUri()"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("sourceKind: currentSourceKind()"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("rootUri: currentRootUri()"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS
.contains("if (sourceKind && !payload.sourceKind) payload.sourceKind = sourceKind"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS
.contains("if (rootUri && !payload.rootUri) payload.rootUri = rootUri"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageContext: scopedContext.pageContext"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageContext: scopedContext.pageContext"));
assert!(
- SIDEBAR_TREE_RUNTIME_JS
+ SIDEBAR_PAGE_AI_RUNTIME_JS
.contains("if (currentSourceKind() === 'local_folder') return false;"),
"local source 不应进入 page-ai fast-path,后端会把 run 收敛为文件引用 scope"
);
@@ -377,70 +379,70 @@ mod tests {
#[test]
fn page_ai_uses_backend_acp_session_runtime_store() {
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiLoadBackendSessions"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/hermes/client/sessions?"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("params.set('source', 'acp')"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiLoadBackendSessionDetail"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiSearchBackendSessions"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-session-rename"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-session-delete"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-session-resume"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-session-search"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiUsageSummary"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("usage.updated"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("thought.delta"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("permission.requested"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-permission-action=\"allow\""));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-permission-action=\"deny\""));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiHidePermissionDialog"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("if (!message.resolved)"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiLoadBackendSessions"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("/api/hermes/client/sessions?"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("params.set('source', 'acp')"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiLoadBackendSessionDetail"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiSearchBackendSessions"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-rename"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-delete"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-resume"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-search"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiUsageSummary"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("usage.updated"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("thought.delta"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("permission.requested"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-permission-action=\"allow\""));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-permission-action=\"deny\""));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiHidePermissionDialog"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("if (!message.resolved)"));
assert!(
- !SIDEBAR_TREE_RUNTIME_JS.contains("(item.resolved ? ' disabled' : '')"),
+ !SIDEBAR_PAGE_AI_RUNTIME_JS.contains("(item.resolved ? ' disabled' : '')"),
"已决 ACP permission 事件不能继续展示假审批按钮"
);
assert!(
- SIDEBAR_TREE_RUNTIME_JS.contains("session.info.updated"),
+ SIDEBAR_PAGE_AI_RUNTIME_JS.contains("session.info.updated"),
"ACP SessionInfoUpdate 事件应通过 session.info.updated SSE 转发到前端"
);
assert!(
- SIDEBAR_TREE_RUNTIME_JS.contains("plan.updated"),
+ SIDEBAR_PAGE_AI_RUNTIME_JS.contains("plan.updated"),
"ACP PlanUpdate 事件应通过 plan.updated SSE 转发到前端"
);
assert!(
- SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-plan"),
+ SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-plan"),
"plan 消息应渲染为 data-page-ai-plan 标记的轻量系统状态面板"
);
assert!(
- SIDEBAR_TREE_RUNTIME_JS.contains("执行计划 · "),
+ SIDEBAR_PAGE_AI_RUNTIME_JS.contains("执行计划 · "),
"plan 面板标题应显示执行计划和步数"
);
}
#[test]
fn page_ai_session_ui_labels_local_shared_and_cloud_storage() {
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiSessionStorageLabel"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("local_private"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("local_shared"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("convex_acp_runtime_store"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("本地私有"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("共享会话"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("云端会话"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sessionStorage:"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("permissionLevel:"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("shareId:"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiSessionStorageLabel"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("local_private"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("local_shared"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("convex_acp_runtime_store"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("本地私有"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("共享会话"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("云端会话"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("sessionStorage:"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("permissionLevel:"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("shareId:"));
}
#[test]
fn page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch() {
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("var PAGE_AI_SESSION_STORAGE_VERSION = 3"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("var PAGE_AI_SESSION_STORAGE_VERSION = 3"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiAcpRuntime: 'reasonix'"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiNormalizeAcpRuntimes"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("return ['reasonix', 'hermes']"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiNormalizeAcpRuntimes(acpRuntimes)"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiNormalizeAcpRuntimes"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("return ['reasonix', 'hermes']"));
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiNormalizeAcpRuntimes(acpRuntimes)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiAcpRuntimeSelect.value || 'reasonix'"));
- assert!(SIDEBAR_TREE_RUNTIME_JS
+ assert!(SIDEBAR_PAGE_AI_RUNTIME_JS
.contains("activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix'"));
- assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("默认 (Hermes HTTP)"));
+ assert!(!SIDEBAR_PAGE_AI_RUNTIME_JS.contains("默认 (Hermes HTTP)"));
}
#[test]
diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mindmap_node_view.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mindmap_node_view.rs
index cf8dc34f..34250df5 100644
--- a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mindmap_node_view.rs
+++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mindmap_node_view.rs
@@ -20,7 +20,7 @@ use std::collections::HashMap;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{CustomEvent, CustomEventInit, EventTarget, HtmlElement, HtmlInputElement};
-use crate::next_mount_id;
+use crate::editor_runtime::mount::next_mount_id;
thread_local! {
static MOUNTED_MINDMAP_SHELLS: std::cell::RefCell> = std::cell::RefCell::new(HashMap::new());
diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs
index f644afe9..5948425d 100644
--- a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs
+++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs
@@ -14,6 +14,7 @@ pub(crate) mod dom_selection;
pub(crate) mod editor_focus;
pub(crate) mod history_safe_commands;
pub(crate) mod mindmap_node_view;
+pub(crate) mod mount;
pub(crate) mod overlays;
pub(crate) mod persistence;
pub(crate) mod runtime_bridge;
diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mount.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mount.rs
new file mode 100644
index 00000000..ffc97b37
--- /dev/null
+++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mount.rs
@@ -0,0 +1,165 @@
+//! Leptos Tiptap island 挂载上下文与 embedded/standalone delivery mode。
+
+use std::cell::Cell;
+
+use leptos::mount::mount_to;
+use leptos::prelude::*;
+use leptos_tiptap::TiptapContent;
+use serde::Deserialize;
+use serde_json::Value;
+use wasm_bindgen::JsCast;
+use web_sys::{window, EventTarget, HtmlElement};
+
+use crate::editor_runtime::bridge_events::{RuntimePageOptions, COMMAND_EVENT};
+use crate::editor_runtime::runtime_bridge::{build_command_listener, register_unmount_handle};
+use crate::App;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub(crate) enum RuntimeDeliveryMode {
+ Standalone,
+ Embedded,
+}
+
+#[derive(Clone, Debug, Default, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct MountOptions {
+ pub(crate) document_id: Option,
+ pub(crate) workspace_id: Option,
+ pub(crate) title: Option,
+ pub(crate) content: Option,
+ pub(crate) html: Option,
+ pub(crate) editable: Option,
+ pub(crate) read_only: Option,
+ pub(crate) revision: Option,
+ pub(crate) conflict_detection_key: Option,
+ pub(crate) page_options: Option,
+ #[serde(default)]
+ pub(crate) standalone_object: Option,
+}
+
+#[derive(Clone, Debug, Default, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct RuntimeStandaloneObject {
+ pub(crate) kind: Option,
+ pub(crate) document_id: Option,
+ pub(crate) mindmap_id: Option,
+}
+
+struct RuntimeMountContext {
+ id: u32,
+ target: EventTarget,
+ mode: RuntimeDeliveryMode,
+}
+
+#[derive(Clone, Debug)]
+#[allow(dead_code)]
+struct RuntimeMountOptions {
+ options: MountOptions,
+ mode: RuntimeDeliveryMode,
+}
+
+thread_local! {
+ static RUNTIME_MOUNT_CONTEXT: std::cell::RefCell