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