feat: Page AI Reasonix desktop session alignment, settings IA cleanup, knowledge RAG hardening

- ACP client/session manager: Reasonix desktop live session context
- Hermes tools: knowledge_rag tool manifest and skill updates
- Browser runtime: sidebar page AI permission/profile/render/session/tree modules
- Routes: hermes_client, hermes_tools, knowledge_rag, web_shell
- Scripts: reasonix ACP wrapper, LightRAG MCP, smoke tasks 159/558/559/561/562
- Skills: mnote-knowledge-rag and mnote-lightrag-bridge SKILL.md updates
This commit is contained in:
lix-2026
2026-06-13 22:20:01 +08:00
parent 2236a053c0
commit 4a7efd4a30
30 changed files with 5321 additions and 291 deletions
@@ -356,11 +356,12 @@ function openEditorUploadFilePicker(detail, deps) {
async function fetchWithTimeout(input, init, timeoutMs, label) {
var controller = typeof AbortController === 'function' ? new AbortController() : null;
var timer = 0;
var timeout = Math.max(1000, Number(timeoutMs) || 15000);
try {
if (controller) {
timer = window.setTimeout(function() {
controller.abort();
}, Math.max(1000, Number(timeoutMs) || 15000));
}, timeout);
}
var nextInit = Object.assign({}, init || {});
if (controller) nextInit.signal = controller.signal;
@@ -375,6 +376,14 @@ async function fetchWithTimeout(input, init, timeoutMs, label) {
}
}
function uploadTimeoutMsForFile(file, requestedMs) {
var requested = Number(requestedMs) || 0;
var size = Number(file && file.size || 0);
if (!Number.isFinite(size) || size <= 0) return Math.max(requested, 15000);
var sizeBased = 15000 + Math.ceil(size / (1024 * 1024)) * 12000;
return Math.min(10 * 60 * 1000, Math.max(requested, 60000, sizeBased));
}
async function uploadLocalFolderAsset(file, plan, context) {
var rootUri = String(context && context.rootUri || '').trim() || currentRootUri();
var documentId = String(
@@ -401,7 +410,7 @@ async function uploadLocalFolderAsset(file, plan, context) {
method: 'POST',
credentials: 'include',
body: localForm
}, Number(context && context.timeoutMs) || 15000, '本地上传');
}, uploadTimeoutMsForFile(file, context && context.timeoutMs), '本地上传');
var localPayload = await localResponse.json().catch(function() { return null; });
if (!localResponse.ok || !localPayload || !localPayload.asset) {
throw new Error(localPayload && localPayload.error ? localPayload.error : '上传失败');
@@ -552,7 +561,7 @@ async function uploadFileToMediaAsset(file, plan, options, deps) {
var localResult = await uploadLocalFolderAsset(file, plan, {
rootUri: rootUri,
documentId: documentId,
timeoutMs: 15000
timeoutMs: uploadTimeoutMsForFile(file, 15000)
});
var localAsset = localResult && localResult.asset ? localResult.asset : null;
if (!localAsset) {
@@ -753,6 +762,7 @@ window.__mnoteLocalUploadRuntime = {
editorRootFromUploadOptions: editorRootFromUploadOptions,
openEditorUploadFilePicker: openEditorUploadFilePicker,
fetchWithTimeout: fetchWithTimeout,
uploadTimeoutMsForFile: uploadTimeoutMsForFile,
uploadLocalFolderAsset: uploadLocalFolderAsset,
uploadMediaAsset: uploadMediaAsset,
insertUploadedAssetIntoEditor: insertUploadedAssetIntoEditor,
@@ -7,26 +7,133 @@ export function createSidebarPageAiPermissionRuntime(context) {
} = context;
const doc = documentRef || document;
function pageAiPermissionStorageKey() {
var sessionId = String(pageUiState.pageAiActiveSessionId || 'no-session').trim() || 'no-session';
var path = doc && doc.location ? String(doc.location.pathname || '') : '';
return 'mnote.page_ai.permission_queue.v1:' + path + ':' + sessionId;
}
function pageAiPersistPermissionRequests() {
try {
var pending = pageUiState.pageAiPermissionRequests.filter(function(item) {
return item && item.kind === 'permission' && !item.resolved;
}).slice(-20);
if (window.localStorage) window.localStorage.setItem(pageAiPermissionStorageKey(), JSON.stringify(pending));
} catch (_) {}
}
function pageAiRestorePermissionRequests() {
try {
var raw = window.localStorage ? window.localStorage.getItem(pageAiPermissionStorageKey()) : '';
var items = raw ? JSON.parse(raw) : [];
if (!Array.isArray(items)) items = [];
pageUiState.pageAiPermissionRequests = items.filter(function(item) {
return item && item.kind === 'permission' && !item.resolved;
}).slice(-20);
pageUiState.pageAiPermissionRequests.forEach(function(item) {
var exists = pageUiState.pageAiMessages.some(function(message) {
return message.kind === 'permission' && message.permissionId === item.permissionId;
});
if (!exists) pageUiState.pageAiMessages.push(item);
});
var pending = pageUiState.pageAiPermissionRequests.find(function(item) { return !item.resolved; });
if (pending) {
if (!pageAiApplyPermissionMode(pending)) pageAiShowPermissionDialog(pending);
}
return pageUiState.pageAiPermissionRequests;
} catch (_) {
return [];
}
}
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 detail = pageAiPermissionDetail(payload, permissionId);
var toolName = detail.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';
var options = pageAiPermissionOptions(payload);
return {
role: 'tool',
kind: 'permission',
permissionId: permissionId,
runId: String(payload.runId || payload.run_id || pageUiState.pageAiCurrentRunId || '').trim(),
toolName: toolName,
argsSummary: pageAiPreviewValue(args),
argsSummary: detail.summary || pageAiPreviewValue(args),
content: decision === 'denied' ? '已自动拒绝权限请求' : (decision === 'allowed' ? '已自动允许权限请求' : '等待权限确认'),
resolved: decision === 'denied' || decision === 'allowed',
decision: decision
decision: decision,
options: options,
permissionDetails: detail
};
}
function pageAiPermissionDetail(payload, permissionId) {
var params = payload && payload.params && typeof payload.params === 'object' ? payload.params : {};
var toolCall = params.toolCall && typeof params.toolCall === 'object' ? params.toolCall : {};
var rawInput = toolCall.rawInput && typeof toolCall.rawInput === 'object' ? toolCall.rawInput : {};
var title = String(toolCall.title || payload.toolName || payload.tool || payload.name || '').trim();
var kind = String(toolCall.kind || params.kind || '').trim();
var command = String(rawInput.command || rawInput.cmd || '').trim();
var path = String(rawInput.path || rawInput.file || rawInput.target || '').trim();
var toolName = title || String(payload.toolName || payload.tool || payload.name || 'session/request_permission').trim();
var primary = command || path || String(rawInput.pattern || rawInput.query || '').trim() || title || '权限请求';
var rows = [];
rows.push({ label: '操作', value: toolName });
if (kind) rows.push({ label: '类型', value: pageAiPermissionKindLabel(kind) });
if (command) rows.push({ label: '命令', value: command });
return {
toolName: toolName,
kind: kind,
kindLabel: pageAiPermissionKindLabel(kind),
command: command,
path: path,
summary: primary,
rows: rows
};
}
function pageAiPermissionKindLabel(kind) {
var normalized = String(kind || '').trim().toLowerCase();
if (normalized === 'execute') return '执行命令';
if (normalized === 'edit') return '写入文件';
if (normalized === 'read') return '读取';
if (normalized === 'other') return '其他';
return kind || '';
}
function pageAiPermissionOptions(payload) {
var rawOptions = payload && Array.isArray(payload.options)
? payload.options
: (payload && payload.params && Array.isArray(payload.params.options) ? payload.params.options : []);
return rawOptions.map(function(option) {
option = option && typeof option === 'object' ? option : {};
var optionId = String(option.optionId || option.option_id || option.id || '').trim();
if (!optionId) return null;
return {
optionId: optionId,
name: pageAiPermissionOptionLabel(optionId, option),
kind: String(option.kind || '').trim()
};
}).filter(Boolean).slice(0, 8);
}
function pageAiPermissionOptionLabel(optionId, option) {
var id = String(optionId || '').trim().toLowerCase();
var raw = String((option && (option.name || option.title)) || '').trim();
if (id === 'allow_once') return raw && !/^allow$/i.test(raw) ? raw.replace(/^Allow\b/i, '允许') : '允许一次';
if (id === 'allow_always') return '本会话允许';
if (id === 'allow_persistent') return '始终允许';
if (id === 'reject_once' || id === 'reject') return '拒绝';
if (id === 'cancel') return '取消';
if (id === 'refine' || id === 'revise') return '要求修改';
if (id === 'accept') return '接受';
return raw || optionId || '选择';
}
function pageAiApplyPermissionEvent(eventName, payloadText) {
var payload = null;
try {
@@ -46,6 +153,10 @@ export function createSidebarPageAiPermissionRuntime(context) {
pageUiState.pageAiPermissionRequests = pageUiState.pageAiPermissionRequests.filter(function(item) {
return item.permissionId !== message.permissionId;
}).concat([message]).slice(-20);
pageAiPersistPermissionRequests();
if (!message.resolved && pageAiApplyPermissionMode(message)) {
return;
}
if (!message.resolved) {
pageAiShowPermissionDialog(message);
} else {
@@ -53,15 +164,76 @@ export function createSidebarPageAiPermissionRuntime(context) {
}
}
function pageAiResolvePermission(permissionId, decision) {
function pageAiReasonixApprovalMode() {
var preferences = pageUiState.pageAiSkillPreferences && typeof pageUiState.pageAiSkillPreferences === 'object'
? pageUiState.pageAiSkillPreferences
: {};
var mode = String(preferences['ai.agent.reasonix.approval_mode'] || 'ask').trim() || 'ask';
return mode === 'allow' || mode === 'deny' ? mode : 'ask';
}
function pageAiApplyPermissionMode(message) {
if (!message || message.resolved) return false;
var mode = pageAiReasonixApprovalMode();
if (mode === 'ask') return false;
var optionId = pageAiPermissionPreferredOptionId(message, mode);
window.setTimeout(function() {
pageAiResolvePermission(message.permissionId, mode, optionId);
}, 0);
return true;
}
function pageAiPermissionPreferredOptionId(message, decision) {
var options = Array.isArray(message && message.options) ? message.options : [];
if (!options.length) return '';
var allowKeywords = ['allow_once', 'allow', 'approve', 'yes', 'allow_always', 'allow_persistent'];
var denyKeywords = ['deny_once', 'reject_once', 'deny', 'reject', 'no', 'cancel', 'stop'];
var keywords = decision === 'allow' ? allowKeywords : denyKeywords;
for (var i = 0; i < keywords.length; i += 1) {
var keyword = keywords[i];
var found = options.find(function(option) {
var text = [option && option.optionId, option && option.kind, option && option.name].map(function(value) {
return String(value || '').toLowerCase();
}).join(' ');
return text.indexOf(keyword) >= 0;
});
if (found && found.optionId) return String(found.optionId || '');
}
if (decision === 'allow') {
var allowFallback = options.find(function(option) { return !pageAiPermissionOptionRejectLike(option); });
return allowFallback && allowFallback.optionId ? String(allowFallback.optionId || '') : '';
}
return '';
}
function pageAiResolvePermission(permissionId, decision, optionId) {
permissionId = String(permissionId || '').trim();
decision = String(decision || '').trim() || 'deny';
optionId = String(optionId || '').trim();
if (!permissionId) return;
var runId = pageUiState.pageAiCurrentRunId;
var runId = String(pageUiState.pageAiCurrentRunId || '').trim();
if (!runId && pageUiState.pageAiActiveSessionId && Array.isArray(pageUiState.pageAiSessions)) {
var activeSession = pageUiState.pageAiSessions.find(function(session) {
return session && session.id === pageUiState.pageAiActiveSessionId;
});
runId = String(activeSession && (activeSession.runId || activeSession.hostRunId) || '').trim();
}
if (!runId && Array.isArray(pageUiState.pageAiPermissionRequests)) {
var pending = pageUiState.pageAiPermissionRequests.find(function(item) {
return item && item.permissionId === permissionId;
});
runId = String(pending && pending.runId || '').trim();
}
if (!runId && doc && doc.documentElement) {
runId = String(doc.documentElement.getAttribute('data-mnote-page-ai-run-id') || doc.documentElement.getAttribute('data-mnote-page-ai-active-host-run-id') || '').trim();
}
if (runId) {
var body = { permissionId: permissionId, decision: decision };
if (optionId) body.optionId = optionId;
fetch('/api/hermes/client/runs/' + encodeURIComponent(runId) + '/resolve-permission', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ permissionId: permissionId, decision: decision })
body: JSON.stringify(body)
}).then(function(response) {
if (!response.ok) console.warn('resolve-permission 后端返回异常', response.status);
}).catch(function(err) {
@@ -79,6 +251,7 @@ export function createSidebarPageAiPermissionRuntime(context) {
});
var dialog = doc.querySelector('[data-page-ai-permission-dialog]');
if (dialog instanceof HTMLElement) dialog.hidden = true;
pageAiPersistPermissionRequests();
renderPageAiConversation();
}
@@ -101,31 +274,70 @@ export function createSidebarPageAiPermissionRuntime(context) {
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 class="wolai-page-ai-permission-summary" data-page-ai-permission-args></div>' +
'<div class="wolai-page-ai-message-actions" data-page-ai-permission-actions></div>' +
'</div>';
doc.body.appendChild(dialog);
}
var tool = dialog.querySelector('[data-page-ai-permission-tool]');
if (tool instanceof HTMLElement) tool.textContent = message.toolName || 'session/request_permission';
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);
}
});
if (args instanceof HTMLElement) args.innerHTML = pageAiPermissionDetailsHtml(message);
var actions = dialog.querySelector('[data-page-ai-permission-actions]');
if (actions instanceof HTMLElement) actions.innerHTML = pageAiPermissionActionsHtml(message);
dialog.hidden = false;
}
function pageAiPermissionDetailsHtml(message) {
var details = message && message.permissionDetails && typeof message.permissionDetails === 'object'
? message.permissionDetails
: {};
var rows = Array.isArray(details.rows) ? details.rows : [];
if (!rows.length) {
return '<div class="wolai-page-ai-permission-row"><span>请求</span><strong>' + escapeHtml(message.argsSummary || message.content || '') + '</strong></div>';
}
return rows.slice(0, 6).map(function(row) {
return '<div class="wolai-page-ai-permission-row"><span>' + escapeHtml(row.label || '') + '</span><strong>' + escapeHtml(row.value || '') + '</strong></div>';
}).join('');
}
function pageAiPermissionActionsHtml(message) {
var options = Array.isArray(message.options) ? message.options : [];
if (!options.length) {
return '' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="allow" data-page-ai-permission-id="' + escapeAttr(message.permissionId || '') + '" 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-id="' + escapeAttr(message.permissionId || '') + '" data-page-ai-permission-dialog-action>拒绝</button>';
}
return options.map(function(option) {
var denyLike = pageAiPermissionOptionRejectLike(option);
return '<button type="button" class="wolai-page-ai-ghost' + (denyLike ? ' wolai-page-ai-ghost--danger' : '') + '" data-page-ai-permission-action="' + (denyLike ? 'deny' : 'allow') + '" data-page-ai-permission-option-id="' + escapeAttr(option.optionId || '') + '" data-page-ai-permission-id="' + escapeAttr(message.permissionId || '') + '" data-page-ai-permission-dialog-action>' + escapeHtml(option.name || option.optionId || '选择') + '</button>';
}).join('');
}
function pageAiPermissionOptionRejectLike(option) {
var text = String((option && option.optionId) || '') + ' ' + String((option && option.kind) || '') + ' ' + String((option && option.name) || '');
text = text.toLowerCase();
return /reject|deny|cancel|stop|no/.test(text);
}
function escapeAttr(value) {
return escapeHtml(String(value || ''));
}
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
return {
pageAiPermissionMessage,
pageAiApplyPermissionEvent,
pageAiResolvePermission,
pageAiRestorePermissionRequests,
pageAiHidePermissionDialog,
pageAiShowPermissionDialog
};
@@ -163,6 +163,50 @@ export function createSidebarPageAiProfileRuntime(context) {
});
}
function pageAiSessionStatusFilterValue(session) {
var status = String(session && session.status || '').trim();
var mode = String(session && (session.runtimeMode || session.mode) || '').trim();
if (pageUiState.pageAiActiveSessionId && session && session.id === pageUiState.pageAiActiveSessionId) return 'active';
if (session && session.replaySeen) return 'replay_seen';
if (mode === 'native_live' || mode === 'cold_resumed' || mode === 'replay_seen') return mode;
if (['running', 'tool_calling', 'queued', 'pending', 'acp_pending'].indexOf(status) >= 0) return 'active';
if (['failed', 'aborted', 'cancelled', 'canceled'].indexOf(status) >= 0) return 'failed';
if (status === 'completed') return 'completed';
return status || 'unknown';
}
function pageAiSessionStatusLabel(value) {
return {
all: '全部状态',
active: 'Active',
completed: 'Completed',
failed: 'Failed',
native_live: 'Reasonix native-live',
cold_resumed: 'Cold resumed',
replay_seen: 'Replay seen',
unknown: 'Unknown'
}[value] || value;
}
function pageAiSessionStatusFilterOptions(rows) {
var byValue = {
all: pageAiSessionStatusLabel('all'),
active: pageAiSessionStatusLabel('active'),
completed: pageAiSessionStatusLabel('completed'),
failed: pageAiSessionStatusLabel('failed'),
native_live: pageAiSessionStatusLabel('native_live'),
cold_resumed: pageAiSessionStatusLabel('cold_resumed'),
replay_seen: pageAiSessionStatusLabel('replay_seen')
};
pageAiNormalizeArray(rows).filter(pageAiVisibleHistorySession).forEach(function(session) {
var value = pageAiSessionStatusFilterValue(session);
byValue[value] = pageAiSessionStatusLabel(value);
});
return Object.keys(byValue).map(function(value) {
return { value: value, label: byValue[value] };
});
}
function pageAiVisibleHistorySession(session) {
var source = String(session && session.source || '').trim();
var status = String(session && session.status || '').trim();
@@ -172,10 +216,11 @@ export function createSidebarPageAiProfileRuntime(context) {
function pageAiFilteredHistoryRows(rows) {
var filterValue = String(pageUiState.pageAiSessionAgentFilter || 'all').trim() || 'all';
var statusFilter = String(pageUiState.pageAiSessionStatusFilter || 'all').trim() || 'all';
var normalized = pageAiNormalizeArray(rows).filter(pageAiVisibleHistorySession);
if (filterValue === 'all') return normalized;
return normalized.filter(function(session) {
return pageAiSessionAgentFilterValue(session) === filterValue;
return (filterValue === 'all' || pageAiSessionAgentFilterValue(session) === filterValue)
&& (statusFilter === 'all' || pageAiSessionStatusFilterValue(session) === statusFilter);
});
}
@@ -220,6 +265,9 @@ export function createSidebarPageAiProfileRuntime(context) {
pageAiSessionAgentLabel,
pageAiSessionPreviewText,
pageAiSessionAgentFilterOptions,
pageAiSessionStatusFilterValue,
pageAiSessionStatusLabel,
pageAiSessionStatusFilterOptions,
pageAiFilteredHistoryRows,
pageAiTimestamp,
pageAiUsageSummary
@@ -39,6 +39,9 @@ export function createSidebarPageAiRenderRuntime(context) {
pageAiSessionAgentFilterOptions,
pageAiSessionAgentLabel,
pageAiSessionAgentFilterValue,
pageAiSessionStatusFilterOptions,
pageAiSessionStatusFilterValue,
pageAiSessionStatusLabel,
pageAiSessionPreviewText,
pageAiSessionStorageLabel,
pageAiSessionSelected,
@@ -192,6 +195,50 @@ export function createSidebarPageAiRenderRuntime(context) {
'</label>';
}
function pageAiReasonixQuickControlLabel(key, fallback) {
if (key === 'ai.agent.reasonix.model_id') return '模型';
if (key === 'ai.agent.reasonix.approval_mode') return '审批';
if (key === 'ai.agent.reasonix.plan_mode') return 'Plan';
return fallback || key;
}
function pageAiRenderReasonixQuickControl(field) {
var key = String(field && field.key || '').trim();
if (!key || field.secret === true) return '';
var value = pageAiDescriptorFieldValue(field);
var options = pageAiDescriptorFieldOptions(field);
if (!options.length) return '';
return '' +
'<label class="wolai-page-ai-reasonix-control" data-page-ai-reasonix-control-row="' + escapeHtml(key) + '">' +
'<span>' + escapeHtml(pageAiReasonixQuickControlLabel(key, field.label)) + '</span>' +
'<select data-page-ai-descriptor-field="' + escapeHtml(key) + '" data-page-ai-descriptor-kind="select" data-page-ai-reasonix-quick-control="' + escapeHtml(key) + '">' +
options.map(function(option) {
return '<option value="' + escapeHtml(option.value) + '"' + (String(value) === String(option.value) ? ' selected' : '') + '>' + escapeHtml(option.label) + '</option>';
}).join('') +
'</select>' +
'</label>';
}
function pageAiReasonixQuickControlsHtml() {
if (pageAiCurrentAgentId() !== 'reasonix') return '';
var descriptor = pageAiAgentDescriptor('reasonix') || pageAiAgentRecord('reasonix');
var wanted = {
'ai.agent.reasonix.model_id': true,
'ai.agent.reasonix.approval_mode': true,
'ai.agent.reasonix.plan_mode': true
};
var fields = pageAiNormalizeArray(descriptor && descriptor.fields)
.filter(function(field) { return wanted[String(field && field.key || '').trim()] === true; })
.map(pageAiRenderReasonixQuickControl)
.filter(Boolean)
.join('');
if (!fields) return '';
return '' +
'<div class="wolai-page-ai-reasonix-controls" data-page-ai-reasonix-quick-controls>' +
fields +
'</div>';
}
function pageAiRenderAgentDescriptorCard(agentId, fallbackTitle) {
var descriptor = pageAiAgentDescriptor(agentId) || pageAiAgentRecord(agentId);
var title = String(descriptor && (descriptor.displayName || descriptor.label) || fallbackTitle || agentId).trim();
@@ -210,11 +257,27 @@ export function createSidebarPageAiRenderRuntime(context) {
function pageAiCurrentModelLabel() {
var profile = pageAiCurrentProfileRecord();
var prefs = pageUiState.pageAiSkillPreferences || {};
var toolModel = pageAiMnoteToolModel();
var reasonixModel = String(prefs['ai.agent.reasonix.model_id'] || '').trim();
var approvalMode = String(prefs['ai.agent.reasonix.approval_mode'] || 'ask').trim() || 'ask';
var planMode = String(prefs['ai.agent.reasonix.plan_mode'] || 'manual').trim() || 'manual';
var approvalLabel = {
ask: '审批:询问',
allow: '审批:自动允许',
deny: '审批:自动拒绝'
}[approvalMode] || ('审批:' + approvalMode);
var planLabel = {
manual: '计划:手动',
auto: '计划:自动'
}[planMode] || ('计划:' + planMode);
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 决定';
if (pageAiCurrentAgentId() === 'reasonix') {
return ['Reasonix', reasonixModel || toolModel, approvalLabel, planLabel].filter(Boolean).join(' · ');
}
return 'tool: ' + toolModel + ' · profile: ' + profileLabel;
}
@@ -283,6 +346,88 @@ export function createSidebarPageAiRenderRuntime(context) {
});
}
function pageAiRuntimeStatusValue(path, fallback) {
var node = pageUiState.pageAiRuntimeStatus || {};
String(path || '').split('.').forEach(function(part) {
if (!part || node == null || typeof node !== 'object') return;
node = node[part];
});
if (node == null || node === '') return fallback || '';
return String(node);
}
function pageAiRuntimeStatusLabel() {
var status = pageUiState.pageAiRuntimeStatus || {};
var runtime = String(status.runtime || pageUiState.pageAiAcpRuntime || 'reasonix');
var mode = String(status.mode || 'new_session');
if (runtime === 'reasonix') return pageAiCurrentModelLabel() + ' · ' + mode.replace(/_/g, '-');
return runtime + ' · ' + mode.replace(/_/g, '-');
}
function pageAiRuntimeDetailHtml() {
var status = pageUiState.pageAiRuntimeStatus || {};
var acp = status.acp && typeof status.acp === 'object' ? status.acp : {};
var roots = status.roots && typeof status.roots === 'object' ? status.roots : {};
var tools = status.tools && typeof status.tools === 'object' ? status.tools : {};
var mcp = status.mcp && typeof status.mcp === 'object' ? status.mcp : {};
var queue = status.session && status.session.queue && typeof status.session.queue === 'object' ? status.session.queue : {};
var logs = status.logs && Array.isArray(status.logs.items) ? status.logs.items : [];
var rows = [
['runtime', pageAiRuntimeStatusLabel()],
['session', String(status.mnoteSessionId || pageUiState.pageAiActiveSessionId || '无')],
['acpSessionId', String(acp.acpSessionId || '无')],
['load/replay', String(acp.loadStatus || 'not_applicable') + ' / replay=' + String(acp.replaySeen === true)],
['live binding', acp.liveBinding && typeof acp.liveBinding === 'object' ? String(acp.liveBinding.status || 'unknown') : '无'],
['roots', 'write=' + String(roots.write || 0) + ' readOnly=' + String(roots.readOnly || 0)],
['MCP/tools', String(mcp.status || 'unknown') + ' · enabled=' + String(tools.enabled || 0) + ' disabled=' + String(tools.disabled || 0)],
['queue', String(queue.queuedCount || pageUiState.pageAiQueueLength || 0)],
['jobs', pageAiRuntimeStatusValue('jobs.running', '0')]
];
var html = rows.map(function(row) {
return '<div class="wolai-page-ai-tool-row"><span class="wolai-page-ai-tool-name">' + escapeHtml(row[0]) + '</span><span class="wolai-page-ai-tool-meta">' + escapeHtml(row[1]) + '</span></div>';
}).join('');
if (pageUiState.pageAiRuntimeStatusError) {
html += '<div class="wolai-page-ai-inline-error">' + escapeHtml(pageUiState.pageAiRuntimeStatusError) + '</div>';
}
if (logs.length) {
html += logs.slice(-4).map(function(item) {
return '<div class="wolai-page-ai-tool-row"><span class="wolai-page-ai-tool-name">' + escapeHtml(String(item.kind || 'log')) + '</span><span class="wolai-page-ai-tool-meta">' + escapeHtml(String(item.seq || '')) + '</span></div>';
}).join('');
}
return html || '<div class="wolai-page-ai-empty">Runtime status 尚未加载。</div>';
}
function pageAiUsageDetailHtml() {
var status = pageUiState.pageAiRuntimeStatus || {};
var usage = status.usage && typeof status.usage === 'object' ? status.usage : {};
var rows = [
['last run', usage.lastRunTokens || usage.lastRunCost || 'unknown'],
['session total', usage.sessionTokens || usage.sessionCost || 'unknown'],
['cache', usage.cacheTokens || usage.cacheHit || 'unknown']
];
return rows.map(function(row) {
return '<div class="wolai-page-ai-tool-row"><span class="wolai-page-ai-tool-name">' + escapeHtml(row[0]) + '</span><span class="wolai-page-ai-tool-meta">' + escapeHtml(String(row[1])) + '</span></div>';
}).join('');
}
function pageAiSettingsTabsHtml(activeTab) {
return [
['agent', 'MNote'],
['reasonix-settings', 'Reasonix'],
['hermes-settings', 'Hermes']
].map(function(tab) {
var active = tab[0] === activeTab;
return '<button type="button" class="wolai-page-ai-tab' + (active ? ' is-active' : '') + '" data-page-ai-tab="' + escapeHtml(tab[0]) + '" role="tab" aria-selected="' + (active ? 'true' : 'false') + '">' + escapeHtml(tab[1]) + '</button>';
}).join('');
}
function pageAiStatusTabsHtml() {
return '' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="chat" role="tab" aria-selected="false">聊天</button>' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="status" role="tab" aria-selected="true">状态</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="agent" role="tab" aria-selected="false">设置</button>';
}
function renderPageAiProviderButtons() {
var drawer = ensurePageAiDrawer();
var isAcp = pageUiState.pageAiAcpRuntime !== '';
@@ -303,6 +448,8 @@ export function createSidebarPageAiRenderRuntime(context) {
var isAcp = pageUiState.pageAiAcpRuntime !== '';
var activeAgentId = pageAiCurrentAgentId();
var activeProfile = pageAiCurrentProfile();
if (pageUiState.pageAiPage === 'runtime') pageUiState.pageAiPage = 'status';
if (pageUiState.pageAiPage === 'chat-only-settings') pageUiState.pageAiPage = 'agent';
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');
@@ -478,16 +625,32 @@ export function createSidebarPageAiRenderRuntime(context) {
if (agentPanel instanceof HTMLElement) {
agentPanel.innerHTML = '' +
(pageUiState.pageAiAgentDescriptorError ? '<div class="wolai-page-ai-inline-error">' + escapeHtml(pageUiState.pageAiAgentDescriptorError) + '</div>' : '') +
'<section class="wolai-page-ai-memory-card" data-page-ai-agent-descriptor-card="mnote">' +
'<div class="wolai-page-ai-memory-head"><div><div class="wolai-page-ai-memory-title">MNote</div><div class="wolai-page-ai-memory-scope">上下文、权限、工具、会话历史和审计</div></div></div>' +
'<div class="wolai-page-ai-tool-meta">这里配置 MNote 发送给 agent 的 envelope,不配置 Reasonix 或 Hermes 原生 runtime。</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">' + escapeHtml(pageAiBuildAllowedRoots().length ? 'SQLite directory_grants' : '需要授权') + '</div></div>' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">授权区域</div><div class="wolai-page-ai-memory-scope">SQLite directory_grants</div></div>' +
'<div class="wolai-page-ai-tool-list" data-page-ai-allowed-roots></div>' +
'</section>' +
pageAiRenderAgentDescriptorCard(activeAgentId, pageAiCurrentAgentSelectionLabel()) +
'<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>' +
'<div class="wolai-page-ai-tool-meta">由 MNote 生成 run envelope,不写入 Reasonix/Hermes native memory。</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>' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">会话与审计</div><div class="wolai-page-ai-memory-scope">history / export / changed files</div></div>' +
'<div class="wolai-page-ai-tool-meta">删除会话只删除 MNote history;外部 Hermes/provider 会话不会默认删除。</div>' +
'</section>';
var mnoteAllowedRootsNode = agentPanel.querySelector('[data-page-ai-allowed-roots]');
if (mnoteAllowedRootsNode instanceof HTMLElement) {
var mnoteAllowedRoots = pageAiBuildAllowedRoots();
mnoteAllowedRootsNode.innerHTML = mnoteAllowedRoots.length
? mnoteAllowedRoots.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('')
: '<span class="wolai-page-ai-tool-meta">' + escapeHtml(String(pageUiState.pageAiAllowedRootsError || '').trim() || '未选择授权区域') + '</span>';
}
}
var profileSummary = drawer.querySelector('[data-page-ai-profile-summary]');
if (profileSummary instanceof HTMLElement) profileSummary.textContent = '上下文可选';
@@ -512,6 +675,40 @@ export function createSidebarPageAiRenderRuntime(context) {
var queueSuffix = pageUiState.pageAiQueueLength > 0 ? ' · 队列 ' + pageUiState.pageAiQueueLength : '';
runStatus.textContent = pageAiRunStatusLabel(pageUiState.pageAiRunStatus) + queueSuffix;
}
drawer.querySelectorAll('[data-page-ai-runtime-badge]').forEach(function(runtimeBadge) {
if (runtimeBadge instanceof HTMLElement) runtimeBadge.textContent = pageAiRuntimeStatusLabel();
});
var runtimeActive = drawer.querySelector('[data-page-ai-runtime-active-run]');
if (runtimeActive instanceof HTMLElement) {
var activeRunStatus = pageAiRuntimeStatusValue('activeRun.status', pageUiState.pageAiRunStatus || 'idle');
runtimeActive.textContent = pageAiRunStatusLabel(activeRunStatus);
}
var runtimeAcp = drawer.querySelector('[data-page-ai-runtime-acp]');
if (runtimeAcp instanceof HTMLElement) runtimeAcp.textContent = pageAiRuntimeStatusValue('acp.acpSessionId', '无 ACP session');
var runtimeDetail = drawer.querySelector('[data-page-ai-runtime-detail]');
if (runtimeDetail instanceof HTMLElement) runtimeDetail.innerHTML = pageAiRuntimeDetailHtml();
var jobsList = drawer.querySelector('[data-page-ai-jobs-list]');
if (jobsList instanceof HTMLElement) {
var runtimeStatus = pageUiState.pageAiRuntimeStatus || {};
var jobs = runtimeStatus.jobs && Array.isArray(runtimeStatus.jobs.items) ? runtimeStatus.jobs.items : [];
jobsList.innerHTML = jobs.length
? jobs.map(function(job) {
return '<div class="wolai-page-ai-tool-row"><span class="wolai-page-ai-tool-name">' + escapeHtml(String(job.id || job.name || 'job')) + '</span><span class="wolai-page-ai-tool-meta">' + escapeHtml(String(job.status || 'running')) + '</span></div>';
}).join('')
: '<div class="wolai-page-ai-empty">暂无运行中的 job。</div>';
}
var logsList = drawer.querySelector('[data-page-ai-logs-list]');
if (logsList instanceof HTMLElement) {
var logStatus = pageUiState.pageAiRuntimeStatus || {};
var logs = logStatus.logs && Array.isArray(logStatus.logs.items) ? logStatus.logs.items : [];
logsList.innerHTML = logs.length
? logs.slice(-8).map(function(item) {
return '<div class="wolai-page-ai-tool-row"><span class="wolai-page-ai-tool-name">' + escapeHtml(String(item.kind || 'log')) + '</span><span class="wolai-page-ai-tool-meta">' + escapeHtml(String(item.seq || '')) + '</span></div>';
}).join('')
: '<div class="wolai-page-ai-empty">暂无 runtime logs。</div>';
}
var usageDetail = drawer.querySelector('[data-page-ai-usage-detail]');
if (usageDetail instanceof HTMLElement) usageDetail.innerHTML = pageAiUsageDetailHtml();
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;
@@ -530,9 +727,10 @@ export function createSidebarPageAiRenderRuntime(context) {
} else {
queueList.innerHTML = pageUiState.pageAiQueuedItems.map(function(item, index) {
var label = '队列 ' + (index + 1);
var preview = String(item.preview || item.traceId || item.queueId || '').trim();
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>' +
'<div><strong>' + escapeHtml(label) + '</strong><br /><span>' + escapeHtml(preview || 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('');
@@ -547,7 +745,17 @@ export function createSidebarPageAiRenderRuntime(context) {
: '等待 AI session';
}
var modelNode = drawer.querySelector('[data-page-ai-model-status]');
if (modelNode instanceof HTMLElement) modelNode.textContent = pageAiBuildAllowedRoots().length ? '已授权' : '需授权';
if (modelNode instanceof HTMLElement) modelNode.textContent = pageAiCurrentModelLabel();
var reasonixControls = drawer.querySelector('[data-page-ai-reasonix-quick-controls]');
if (reasonixControls instanceof HTMLElement) {
if (pageAiCurrentAgentId() === 'reasonix') {
reasonixControls.innerHTML = pageAiReasonixQuickControlsHtml().replace(/^<div[^>]*>|<\/div>$/g, '');
reasonixControls.hidden = false;
} else {
reasonixControls.innerHTML = '';
reasonixControls.hidden = true;
}
}
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]');
@@ -630,6 +838,20 @@ export function createSidebarPageAiRenderRuntime(context) {
sessionAgentFilter.value = activeFilter;
pageUiState.pageAiSessionAgentFilter = activeFilter;
}
var sessionStatusFilter = drawer.querySelector('[data-page-ai-session-status-filter]');
if (sessionStatusFilter instanceof HTMLSelectElement) {
var historyRowsForStatusFilter = pageUiState.pageAiSessionSearchResults.length
? pageUiState.pageAiSessionSearchResults
: pageUiState.pageAiSessions;
var statusOptions = pageAiSessionStatusFilterOptions(historyRowsForStatusFilter);
var activeStatusFilter = String(pageUiState.pageAiSessionStatusFilter || 'all').trim() || 'all';
if (!statusOptions.some(function(option) { return option.value === activeStatusFilter; })) activeStatusFilter = 'all';
sessionStatusFilter.innerHTML = statusOptions.map(function(option) {
return '<option value="' + escapeHtml(option.value) + '"' + (option.value === activeStatusFilter ? ' selected' : '') + '>' + escapeHtml(option.label) + '</option>';
}).join('');
sessionStatusFilter.value = activeStatusFilter;
pageUiState.pageAiSessionStatusFilter = activeStatusFilter;
}
var skillSearch = drawer.querySelector('[data-page-ai-skill-search]');
if (skillSearch instanceof HTMLInputElement && documentRef.activeElement !== skillSearch) {
skillSearch.value = pageUiState.pageAiSkillQuery;
@@ -754,7 +976,7 @@ export function createSidebarPageAiRenderRuntime(context) {
}
if (gatewayDetail instanceof HTMLElement) {
if (!gatewayHealth) {
gatewayDetail.innerHTML = '<div class="wolai-page-ai-empty">打开高级后会检查 agent runtime 与当前 profile。</div>';
gatewayDetail.innerHTML = '<div class="wolai-page-ai-empty">打开 Hermes 设置后会检查 gateway 与当前 profile。</div>';
} else {
var gatewayInfo = gatewayHealth.gateway || {};
var profileInfo = gatewayHealth.profile || {};
@@ -798,8 +1020,8 @@ export function createSidebarPageAiRenderRuntime(context) {
}
var chatOnlyPanel = drawer.querySelector('[data-page-ai-chat-only-panel]');
if (chatOnlyPanel instanceof HTMLElement) {
chatOnlyPanel.innerHTML = pageAiRenderAgentDescriptorCard('chat_only', 'Chat-only') +
'<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>';
chatOnlyPanel.innerHTML = pageAiRenderAgentDescriptorCard('chat_only', 'API Chat / Chat-only profile') +
'<section class="wolai-page-ai-memory-card"><div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">API Chat / Chat-only profile</div><div class="wolai-page-ai-memory-scope">默认不申请文件写权限,归属 MNote 设置。</div></div></section>';
}
}
@@ -859,6 +1081,11 @@ export function createSidebarPageAiRenderRuntime(context) {
'<button type="button" class="wolai-page-ai-icon" data-page-ai-action="close" aria-label="关闭页面 AI">×</button>' +
'</div>' +
'</div>' +
'<button type="button" class="wolai-page-ai-runtime-strip" data-page-ai-runtime-strip data-page-ai-tab="status" aria-label="打开 Page AI 状态">' +
'<span data-page-ai-runtime-badge>reasonix · new-session</span>' +
'<span data-page-ai-runtime-active-run>空闲</span>' +
'<span data-page-ai-runtime-acp>无 ACP session</span>' +
'</button>' +
'<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">' +
@@ -885,26 +1112,36 @@ export function createSidebarPageAiRenderRuntime(context) {
'<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>' +
pageAiSettingsTabsHtml('agent') +
'</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>' +
'<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-settings-stack" data-page-ai-agent-panel></div>' +
'<div class="wolai-page-ai-settings-stack" data-page-ai-chat-only-panel></div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>MNote 工具</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="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>' +
pageAiSettingsTabsHtml('hermes-settings') +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-settings-grid">' +
@@ -914,17 +1151,21 @@ export function createSidebarPageAiRenderRuntime(context) {
'</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-inline-error" data-page-ai-gateway-error hidden></div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>Hermes 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-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>' +
pageAiSettingsTabsHtml('reasonix-settings') +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-settings-grid">' +
@@ -935,26 +1176,13 @@ export function createSidebarPageAiRenderRuntime(context) {
'</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" data-page-ai-tab="agent" role="tab" aria-selected="false">设置</button>' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="skills" role="tab" aria-selected="true">能力</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="runtime" role="tab" aria-selected="false">Runtime</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="status" role="tab" aria-selected="false">状态</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-skills-toolbar">' +
@@ -974,47 +1202,51 @@ export function createSidebarPageAiRenderRuntime(context) {
'<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>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="status" 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">能力</button>' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="runtime" role="tab" aria-selected="true">Runtime</button>' +
pageAiStatusTabsHtml() +
'</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 class="wolai-page-ai-toolbar">' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="stop-run">Stop turn</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="reset-runtime">Reset current runtime</button>' +
'</div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>queue</span>' +
'<span>Current runtime</span>' +
'<span data-page-ai-runtime-badge>未加载</span>' +
'</div>' +
'<div class="wolai-page-ai-tool-list" data-page-ai-runtime-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" hidden>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>MNote 工具调试</span>' +
'<span data-page-ai-last-tool>暂无 tool call</span>' +
'<span>Jobs</span>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="stop-all-jobs">Stop all</button>' +
'</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 class="wolai-page-ai-tool-list" data-page-ai-jobs-list></div>' +
'</div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>Logs tail</span>' +
'<span>ACP stderr / tool failed / MCP failed</span>' +
'</div>' +
'<div class="wolai-page-ai-tool-list" data-page-ai-logs-list></div>' +
'</div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>Usage</span>' +
'<span>last run / session / cache</span>' +
'</div>' +
'<div class="wolai-page-ai-tool-list" data-page-ai-usage-detail></div>' +
'</div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="history" hidden>' +
@@ -1034,6 +1266,10 @@ export function createSidebarPageAiRenderRuntime(context) {
'<span>按 agent 筛选</span>' +
'<select data-page-ai-session-agent-filter><option value="all">全部 agent</option></select>' +
'</label>' +
'<label class="wolai-page-ai-skill-search">' +
'<span>按状态筛选</span>' +
'<select data-page-ai-session-status-filter><option value="all">全部状态</option></select>' +
'</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>' +
@@ -1041,6 +1277,7 @@ export function createSidebarPageAiRenderRuntime(context) {
'<div class="wolai-page-ai-footer">' +
'<div class="wolai-page-ai-composer">' +
'<textarea class="wolai-page-ai-input" data-page-ai-input rows="3" placeholder="输入消息…"></textarea>' +
'<div class="wolai-page-ai-reasonix-controls" data-page-ai-reasonix-quick-controls hidden></div>' +
'<div class="wolai-page-ai-composer-bar">' +
'<button type="button" class="wolai-page-ai-tool-button" data-page-ai-action="new-session" title="新会话"></button>' +
'<div class="wolai-page-ai-agent-picker">' +
@@ -1209,6 +1446,98 @@ export function createSidebarPageAiRenderRuntime(context) {
'</div>';
}
function pageAiLooksLikeReasonixPlanMessage(item) {
if (!item || item.role !== 'assistant') return false;
var text = String(item.content || '').trim();
if (!text || text.length < 40) return false;
if (/ACP connection closed|AI 当前请求失败|run failed/i.test(text)) return false;
var hasPlanWord = /计划|步骤|执行|修改|写入|新建|验证|plan/i.test(text);
var awaitsApproval = /请批准|待批准|批准后|确认后|审批|批准,|开始执行|approve/i.test(text);
var hasStepShape = /(^|\n)\s*(?:\d+[\.\、)]|[-*]\s+|\|\s*\d+\s*\|)/.test(text);
return hasPlanWord && awaitsApproval && hasStepShape;
}
function pageAiExtractReasonixPlanSteps(text) {
var seen = {};
var steps = [];
String(text || '').replace(/\r\n/g, '\n').split('\n').forEach(function(line) {
var trimmed = line.trim();
if (!trimmed) return;
var step = '';
var tableCells = trimmed.indexOf('|') >= 0
? trimmed.split('|').map(function(cell) { return cell.trim(); }).filter(Boolean)
: [];
if (tableCells.length >= 2 && /^\d+$/.test(tableCells[0])) {
step = tableCells.slice(1).join(' · ');
} else {
var numbered = trimmed.match(/^\s*\d+[\.\、)]\s+(.+)$/);
var bullet = trimmed.match(/^\s*[-*]\s+(?:\[[ xX~>|-]\]\s*)?(.+)$/);
step = numbered ? numbered[1] : (bullet ? bullet[1] : '');
}
step = String(step || '').replace(/\s+/g, ' ').trim();
if (!step || step.length < 3 || /^[-:|]+$/.test(step)) return;
if (seen[step]) return;
seen[step] = true;
var lower = step.toLowerCase();
var status = /✅|已完成|done|completed/.test(step) ? 'completed'
: (/当前|进行中|执行中|待批准|in progress|running/.test(lower) ? 'inProgress' : 'pending');
steps.push({ text: step, status: status });
});
return steps.slice(0, 8);
}
function pageAiPlanStepIcon(status) {
if (status === 'completed') return '✓';
if (status === 'inProgress') return '…';
return String.fromCharCode(8226);
}
function pageAiRenderReasonixPlanCard(item) {
var text = String(item.content || '').trim();
var steps = pageAiExtractReasonixPlanSteps(text);
var stepHtml = steps.length ? (
'<ol class="wolai-page-ai-plan-step-list">' + steps.map(function(step) {
return '' +
'<li class="wolai-page-ai-plan-step-item" data-status="' + escapeHtml(step.status || 'pending') + '">' +
'<span class="wolai-page-ai-plan-step-status" data-status="' + escapeHtml(step.status || 'pending') + '">' + escapeHtml(pageAiPlanStepIcon(step.status)) + '</span>' +
'<span class="wolai-page-ai-plan-step-text">' + escapeHtml(step.text || '') + '</span>' +
'</li>';
}).join('') + '</ol>'
) : '';
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--assistant wolai-page-ai-plan-card" data-page-ai-reasonix-plan-card="true">' +
'<div class="wolai-page-ai-plan-card-header">' +
'<div class="wolai-page-ai-plan-card-title-wrap">' +
'<span class="wolai-page-ai-plan-card-kicker">Reasonix</span>' +
'<strong class="wolai-page-ai-plan-card-title">执行计划</strong>' +
'</div>' +
'<span class="wolai-page-ai-plan-card-badge">待批准</span>' +
'</div>' +
'<div class="wolai-page-ai-plan-card-markdown">' + renderPageAiMarkdown(text) + '</div>' +
stepHtml +
'<div class="wolai-page-ai-plan-card-actions">' +
'<button type="button" class="wolai-page-ai-plan-card-button wolai-page-ai-plan-card-button--primary" data-page-ai-plan-action="approve">批准执行</button>' +
'<button type="button" class="wolai-page-ai-plan-card-button" data-page-ai-plan-action="revise">要求修改</button>' +
'<button type="button" class="wolai-page-ai-plan-card-button wolai-page-ai-plan-card-button--danger" data-page-ai-plan-action="cancel">取消</button>' +
'</div>' +
'</div>';
}
function pageAiRenderPermissionDetails(item) {
var details = item && item.permissionDetails && typeof item.permissionDetails === 'object'
? item.permissionDetails
: {};
var rows = Array.isArray(details.rows) ? details.rows : [];
if (!rows.length) {
rows = [
{ label: '请求', value: item.argsSummary || item.content || '' }
];
}
return '<div class="wolai-page-ai-permission-summary">' + rows.slice(0, 6).map(function(row) {
return '<div class="wolai-page-ai-permission-row"><span>' + escapeHtml(row.label || '') + '</span><strong>' + escapeHtml(row.value || '') + '</strong></div>';
}).join('') + '</div>';
}
function pageAiRenderNonToolMessage(item) {
var roleLabel = item.role === 'user' ? '你' : (item.role === 'tool' ? '工具' : 'AI');
if (item.kind === 'thought') {
@@ -1224,18 +1553,34 @@ export function createSidebarPageAiRenderRuntime(context) {
'</div>';
}
if (item.kind === 'permission') {
var permissionActions = item.resolved ? '' : (
var permissionOptions = Array.isArray(item.options) ? item.options : [];
var permissionActions = '';
if (!item.resolved) {
if (permissionOptions.length) {
permissionActions = '<div class="wolai-page-ai-message-actions">' + permissionOptions.map(function(option) {
var optionText = String(option.optionId || '') + ' ' + String(option.kind || '') + ' ' + String(option.name || '');
var denyLike = /reject|deny|cancel|stop|no/.test(optionText.toLowerCase());
return '<button type="button" class="wolai-page-ai-ghost' + (denyLike ? ' wolai-page-ai-ghost--danger' : '') + '" data-page-ai-permission-action="' + (denyLike ? 'deny' : 'allow') + '" data-page-ai-permission-option-id="' + escapeHtml(option.optionId || '') + '" data-page-ai-permission-id="' + escapeHtml(item.permissionId || '') + '">' + escapeHtml(option.name || option.optionId || '选择') + '</button>';
}).join('') + '</div>';
} else {
permissionActions = '' +
'<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>'
);
'</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 wolai-page-ai-message--tool wolai-page-ai-permission-card" data-page-ai-permission-request="' + escapeHtml(item.permissionId || '') + '">' +
'<div class="wolai-page-ai-permission-card-header">' +
'<div class="wolai-page-ai-plan-card-title-wrap">' +
'<span class="wolai-page-ai-plan-card-kicker">权限</span>' +
'<strong class="wolai-page-ai-plan-card-title">' + escapeHtml(item.toolName || 'session/request_permission') + '</strong>' +
'</div>' +
'<span class="wolai-page-ai-permission-badge">' + escapeHtml(item.resolved ? (item.decision === 'allow' || item.decision === 'allowed' ? '已允许' : '已处理') : '待确认') + '</span>' +
'</div>' +
'<div class="wolai-page-ai-message-text">' +
'<strong>' + escapeHtml(item.toolName || 'session/request_permission') + '</strong><br />' +
'<span>' + escapeHtml(item.argsSummary || item.content || '') + '</span>' +
pageAiRenderPermissionDetails(item) +
permissionActions +
'</div>' +
'</div>';
@@ -1255,6 +1600,7 @@ export function createSidebarPageAiRenderRuntime(context) {
'</details>' +
'</div>';
}
if (pageAiLooksLikeReasonixPlanMessage(item)) return pageAiRenderReasonixPlanCard(item);
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 + '>' +
@@ -1345,9 +1691,10 @@ export function createSidebarPageAiRenderRuntime(context) {
var selected = pageAiSessionSelected(session.id);
var usage = pageAiUsageSummary(session.usage);
var agentLabel = pageAiSessionAgentLabel(session);
var meta = [agentLabel, pageAiSessionStorageLabel(session), session.permissionLevel, session.shareId, session.status, usage].filter(Boolean).join(' · ');
var statusValue = pageAiSessionStatusFilterValue(session);
var meta = [agentLabel, pageAiSessionStorageLabel(session), session.runtimeMode, session.permissionLevel, session.shareId, pageAiSessionStatusLabel(statusValue), usage].filter(Boolean).join(' · ');
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--history' + (active ? ' is-active' : '') + (selected ? ' is-selected' : '') + '" data-page-ai-session-row="' + escapeHtml(session.id) + '" data-page-ai-session-agent="' + escapeHtml(pageAiSessionAgentFilterValue(session)) + '">' +
'<div class="wolai-page-ai-message wolai-page-ai-message--history' + (active ? ' is-active' : '') + (selected ? ' is-selected' : '') + '" data-page-ai-session-row="' + escapeHtml(session.id) + '" data-page-ai-session-agent="' + escapeHtml(pageAiSessionAgentFilterValue(session)) + '" data-page-ai-session-status="' + escapeHtml(statusValue) + '">' +
'<button type="button" class="wolai-page-ai-session-check" data-page-ai-session-select="' + escapeHtml(session.id) + '" aria-checked="' + (selected ? 'true' : 'false') + '" aria-label="选择会话 ' + escapeHtml(session.title || session.id) + '">' +
'<span>' + (selected ? '✓' : '') + '</span>' +
'</button>' +
@@ -140,6 +140,7 @@ export function createSidebarPageAiRuntime(context) {
pageAiAcpRuntimes: [],
pageAiQueueLength: 0,
pageAiQueuedItems: [],
pageAiClientQueuedPrompts: [],
pageAiStoppedRunIds: {},
pageAiAbortController: null,
pageAiContextScope: 'page',
@@ -184,8 +185,11 @@ export function createSidebarPageAiRuntime(context) {
pageAiSessionSearchResults: [],
pageAiSessionSearchTimer: 0,
pageAiSessionAgentFilter: 'all',
pageAiSessionStatusFilter: 'all',
pageAiSelectedSessionIds: {},
pageAiSessionError: '',
pageAiRuntimeStatus: null,
pageAiRuntimeStatusError: '',
pageAiPermissionRequests: []
};
Object.keys(defaults).forEach(function(key) {
@@ -205,6 +209,9 @@ export function createSidebarPageAiRuntime(context) {
if (!state.pageAiSkills || typeof state.pageAiSkills !== 'object') {
state.pageAiSkills = clonePageAiDefaultValue(defaults.pageAiSkills);
}
if (!Array.isArray(state.pageAiClientQueuedPrompts)) {
state.pageAiClientQueuedPrompts = [];
}
return state;
}
@@ -435,6 +442,9 @@ export function createSidebarPageAiRuntime(context) {
const pageAiSessionAgentLabel = (...args) => pageAiProfileRuntime.pageAiSessionAgentLabel(...args);
const pageAiSessionPreviewText = (...args) => pageAiProfileRuntime.pageAiSessionPreviewText(...args);
const pageAiSessionAgentFilterOptions = (...args) => pageAiProfileRuntime.pageAiSessionAgentFilterOptions(...args);
const pageAiSessionStatusFilterValue = (...args) => pageAiProfileRuntime.pageAiSessionStatusFilterValue(...args);
const pageAiSessionStatusLabel = (...args) => pageAiProfileRuntime.pageAiSessionStatusLabel(...args);
const pageAiSessionStatusFilterOptions = (...args) => pageAiProfileRuntime.pageAiSessionStatusFilterOptions(...args);
const pageAiFilteredHistoryRows = (...args) => pageAiProfileRuntime.pageAiFilteredHistoryRows(...args);
const pageAiTimestamp = (...args) => pageAiProfileRuntime.pageAiTimestamp(...args);
const pageAiUsageSummary = (...args) => pageAiProfileRuntime.pageAiUsageSummary(...args);
@@ -447,6 +457,7 @@ export function createSidebarPageAiRuntime(context) {
const pageAiPermissionMessage = (...args) => pageAiPermissionRuntime.pageAiPermissionMessage(...args);
const pageAiApplyPermissionEvent = (...args) => pageAiPermissionRuntime.pageAiApplyPermissionEvent(...args);
const pageAiResolvePermission = (...args) => pageAiPermissionRuntime.pageAiResolvePermission(...args);
const pageAiRestorePermissionRequests = (...args) => pageAiPermissionRuntime.pageAiRestorePermissionRequests(...args);
const pageAiHidePermissionDialog = (...args) => pageAiPermissionRuntime.pageAiHidePermissionDialog(...args);
const pageAiShowPermissionDialog = (...args) => pageAiPermissionRuntime.pageAiShowPermissionDialog(...args);
const pageAiSessionRuntime = createSidebarPageAiSessionRuntime({
@@ -638,6 +649,9 @@ export function createSidebarPageAiRuntime(context) {
pageAiSessionAgentFilterValue,
pageAiSessionAgentLabel,
pageAiSessionPreviewText,
pageAiSessionStatusFilterOptions,
pageAiSessionStatusFilterValue,
pageAiSessionStatusLabel,
pageAiSessionStorageLabel,
pageAiSessionSelected,
pageAiSelectedSessionCount,
@@ -806,6 +820,28 @@ export function createSidebarPageAiRuntime(context) {
renderPageAiControls();
}
function pageAiReasonixPreference(key, fallback) {
var preferences = pageAiDescriptorPreferenceValues();
var value = preferences[key];
if (value == null || String(value).trim() === '') return fallback;
return String(value).trim();
}
function pageAiReasonixControlState() {
return {
modelId: pageAiReasonixPreference('ai.agent.reasonix.model_id', 'deepseek-flash'),
approvalMode: pageAiReasonixPreference('ai.agent.reasonix.approval_mode', 'ask'),
planMode: pageAiReasonixPreference('ai.agent.reasonix.plan_mode', 'manual')
};
}
function pageAiApplyReasonixPlanMode(prompt) {
if (pageAiCurrentAgentId() !== 'reasonix') return prompt;
var control = pageAiReasonixControlState();
if (control.planMode !== 'auto') return prompt;
return '请按 Reasonix 自动计划模式处理:复杂或会写文件的任务先给出简短计划,并等待我批准后再执行。\n\n' + prompt;
}
function pageAiApplyAgentDescriptorsPayload(payload) {
var descriptors = pageAiNormalizeArray(payload && payload.descriptors)
.map(pageAiNormalizeAgentDescriptor)
@@ -1316,6 +1352,68 @@ export function createSidebarPageAiRuntime(context) {
}
}
function pageAiApplyRuntimeStatus(payload) {
if (!payload || typeof payload !== 'object') return null;
pageUiState.pageAiRuntimeStatus = payload;
pageUiState.pageAiRuntimeStatusError = '';
var activeRun = payload.activeRun && typeof payload.activeRun === 'object' ? payload.activeRun : null;
if (activeRun && activeRun.hostRunId) {
pageAiSetRunStatus(String(activeRun.status || 'running'), String(activeRun.hostRunId || ''));
} else if (pageAiRunStatusIsTerminal(pageUiState.pageAiRunStatus)) {
pageAiUpdateActiveRunSnapshotFromStatus(pageUiState.pageAiRunStatus, pageUiState.pageAiCurrentRunId);
} else if (!activeRun && ['running', 'tool_calling', 'queued', 'pending', 'acp_pending'].indexOf(pageUiState.pageAiRunStatus) >= 0) {
pageAiSetRunStatus('idle', pageUiState.pageAiCurrentRunId);
}
var queue = payload.session && payload.session.queue && typeof payload.session.queue === 'object' ? payload.session.queue : null;
if (queue) {
var queuedCount = Number(queue.queuedCount || 0);
pageUiState.pageAiQueueLength = Number.isFinite(queuedCount) ? Math.max(0, queuedCount) : 0;
if (Array.isArray(queue.items)) {
pageUiState.pageAiQueuedItems = queue.items.map(function(item) {
return {
queueId: String(item.queueId || ''),
sessionId: String(item.sessionId || payload.mnoteSessionId || ''),
traceId: String(item.traceId || ''),
preview: String(item.preview || ''),
queuedAt: Number(item.queuedAt || Date.now())
};
}).filter(function(item) { return item.queueId; });
}
}
var acpSessionId = payload.acp && typeof payload.acp === 'object' ? String(payload.acp.acpSessionId || '') : '';
var current = pageAiCurrentSession();
if (current && acpSessionId) {
current.acpSessionId = acpSessionId;
current.runtimeMode = String(payload.mode || '');
current.updatedAt = Date.now();
pageAiPersistSessions();
}
document.documentElement.setAttribute('data-mnote-page-ai-runtime-status-loaded', 'true');
document.documentElement.setAttribute('data-mnote-page-ai-runtime', String(payload.runtime || ''));
document.documentElement.setAttribute('data-mnote-page-ai-runtime-mode', String(payload.mode || ''));
return payload;
}
async function pageAiLoadRuntimeStatus() {
var params = new URLSearchParams();
var sessionId = String(pageUiState.pageAiActiveSessionId || '').trim();
if (sessionId) params.set('sessionId', sessionId);
var workspaceId = String(resolveWorkspaceId(document.body) || '').trim();
if (workspaceId) params.set('workspaceId', workspaceId);
var documentId = String(currentDocumentId() || '').trim();
if (documentId) params.set('documentId', documentId);
var profile = String(pageAiCurrentProfile() || '').trim();
if (profile) params.set('profile', profile);
var response = await fetch('/api/page-ai/runtime/status?' + params.toString(), {
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, 'page_ai_runtime_status_failed_' + response.status));
}
return pageAiApplyRuntimeStatus(payload);
}
function pageAiActiveRunSnapshotKey() {
return pageAiStorageKey() + ':active-run';
}
@@ -1409,6 +1507,60 @@ export function createSidebarPageAiRuntime(context) {
return true;
}
function pageAiRefreshClientQueueState() {
var clientItems = pageAiNormalizeArray(pageUiState.pageAiClientQueuedPrompts);
var serverItems = pageAiNormalizeArray(pageUiState.pageAiQueuedItems)
.filter(function(item) { return String(item.queueId || '').indexOf('client_') !== 0; });
pageUiState.pageAiQueuedItems = serverItems.concat(clientItems.map(function(item) {
return {
queueId: item.queueId,
sessionId: item.sessionId || pageUiState.pageAiActiveSessionId || '',
traceId: item.traceId || '',
preview: item.preview || item.prompt || '',
queuedAt: item.queuedAt || Date.now()
};
}));
pageUiState.pageAiQueueLength = pageUiState.pageAiQueuedItems.length;
}
function pageAiEnqueueClientPrompt(prompt) {
var queueId = 'client_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
pageUiState.pageAiClientQueuedPrompts.push({
queueId: queueId,
prompt: prompt,
preview: String(prompt || '').slice(0, 120),
sessionId: pageUiState.pageAiActiveSessionId || '',
queuedAt: Date.now()
});
pageAiRefreshClientQueueState();
pageAiSetRunStatus('queued', pageUiState.pageAiCurrentRunId);
return queueId;
}
function pageAiRemoveClientQueuedPrompt(queueId) {
var before = pageUiState.pageAiClientQueuedPrompts.length;
pageUiState.pageAiClientQueuedPrompts = pageUiState.pageAiClientQueuedPrompts.filter(function(item) {
return item.queueId !== queueId;
});
pageAiRefreshClientQueueState();
return before !== pageUiState.pageAiClientQueuedPrompts.length;
}
function pageAiStartNextClientQueuedPrompt() {
if (pageUiState.pageAiBusy) return false;
if (['running', 'tool_calling', 'pending', 'acp_pending'].indexOf(pageUiState.pageAiRunStatus) >= 0) return false;
var next = pageUiState.pageAiClientQueuedPrompts.shift();
if (!next) {
pageAiRefreshClientQueueState();
return false;
}
pageAiRefreshClientQueueState();
window.setTimeout(function() {
void sendPageAiMessage(next.prompt);
}, 0);
return true;
}
function pageAiUnwrapSearchCitationUrl(url) {
try {
var raw = '';
@@ -1668,13 +1820,21 @@ export function createSidebarPageAiRuntime(context) {
async function pageAiCancelQueuedRun(queueId) {
queueId = String(queueId || '').trim();
if (!queueId) return;
if (queueId.indexOf('client_') === 0) {
if (pageAiRemoveClientQueuedPrompt(queueId)) {
pageUiState.pageAiMessages.push({ role: 'assistant', content: '已取消一条 AI 队列项。' });
}
renderPageAiConversation();
renderPageAiControls();
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), {
var response = await fetch('/api/page-ai/sessions/' + encodeURIComponent(sessionId) + '/queue/' + encodeURIComponent(queueId), {
method: 'DELETE',
headers: { 'accept': 'application/json' }
});
@@ -1696,6 +1856,40 @@ export function createSidebarPageAiRuntime(context) {
renderPageAiControls();
}
async function pageAiResetRuntime() {
var sessionId = String(pageUiState.pageAiActiveSessionId || '').trim();
if (!sessionId) return;
try {
var response = await fetch('/api/page-ai/runtime/reset', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
sessionId: sessionId,
runtime: pageUiState.pageAiAcpRuntime || '',
profile: pageAiCurrentProfile()
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'runtime_reset_failed_' + response.status));
}
pageUiState.pageAiRuntimeStatus = null;
pageUiState.pageAiRuntimeStatusError = '';
pageUiState.pageAiMessages.push({
role: 'assistant',
content: '已重置 runtimeMNote 会话历史保留,外部 provider 会话未删除。'
});
await pageAiLoadRuntimeStatus().catch(function(error) {
pageUiState.pageAiRuntimeStatusError = error instanceof Error ? error.message : String(error);
return null;
});
} catch (error) {
pageUiState.pageAiRuntimeStatusError = error instanceof Error ? error.message : String(error);
}
renderPageAiConversation();
renderPageAiControls();
}
function pageAiSetContextScope(scope) {
var next = String(scope || '').trim() || 'page';
pageUiState.pageAiContextScope = next;
@@ -2141,6 +2335,7 @@ export function createSidebarPageAiRuntime(context) {
pageUiState.pageAiAgentId = pageAiCurrentAgentId();
pageAiEnsureContextRefState();
pageAiLoadSessions();
pageAiRestorePermissionRequests();
renderPageAiSuggestions();
renderPageAiConversation();
renderPageAiProviderButtons();
@@ -2153,6 +2348,12 @@ export function createSidebarPageAiRuntime(context) {
void pageAiCheckAndResumeActiveRun().catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
}).then(function() {
return pageAiLoadRuntimeStatus().catch(function(error) {
pageUiState.pageAiRuntimeStatusError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
return null;
});
});
void pageAiLoadBackendSessions().then(function() {
return pageAiCheckAndResumeActiveRun();
@@ -2169,7 +2370,8 @@ export function createSidebarPageAiRuntime(context) {
pageAiLoadGatewayHealth(),
pageAiLoadAiPreferences(),
pageAiLoadAllowedRoots(),
pageAiLoadBackendSessions()
pageAiLoadBackendSessions(),
pageAiLoadRuntimeStatus()
]).then(function() {
renderPageAiControls();
}).then(function() {
@@ -2803,10 +3005,27 @@ export function createSidebarPageAiRuntime(context) {
}
async function sendPageAiMessage(text) {
var allowQueue = ['running', 'tool_calling'].indexOf(pageUiState.pageAiRunStatus) >= 0;
var allowQueue = pageUiState.pageAiBusy
|| ['queued', 'running', 'tool_calling', 'pending', 'acp_pending'].indexOf(pageUiState.pageAiRunStatus) >= 0;
if (pageUiState.pageAiBusy && !allowQueue) return;
var prompt = searchText(text);
if (!prompt) return;
if (allowQueue) {
pageAiEnqueueClientPrompt(prompt);
pageUiState.pageAiMessages.push({
role: 'assistant',
content: '已加入 AI 队列,前一条 run 完成后继续处理。'
});
var queuedSession = pageAiCurrentSession();
if (queuedSession) {
queuedSession.messages = pageUiState.pageAiMessages.slice();
queuedSession.updatedAt = Date.now();
}
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
return;
}
if (!allowQueue) pageUiState.pageAiBusy = true;
var currentSession = null;
try {
@@ -2846,6 +3065,8 @@ export function createSidebarPageAiRuntime(context) {
if (!allowQueue && await pageAiTryBlockEditWorkflow(prompt, scopedContext, runTargetSnapshot)) {
return;
}
var reasonixSettings = pageAiCurrentAgentId() === 'reasonix' ? pageAiReasonixControlState() : null;
var promptForRun = pageAiApplyReasonixPlanMode(prompt);
var response = await fetch('/api/hermes/client/runs', {
method: 'POST',
headers: { 'content-type': 'application/json' },
@@ -2863,13 +3084,14 @@ export function createSidebarPageAiRuntime(context) {
contextScope: pageUiState.pageAiContextScope,
contextRefs: contextRefs,
allowedRoots: allowedRoots,
reasonixSettings: reasonixSettings,
skillPreferences: {
mnote: pageAiSkillPreferenceTable('mnote', ''),
reasonix: pageAiSkillPreferenceTable('reasonix', ''),
hermes: pageAiSkillPreferenceTable('hermes', runProfile)
},
message: prompt,
model: pageAiMnoteToolModel(),
message: promptForRun,
model: reasonixSettings && reasonixSettings.modelId ? reasonixSettings.modelId : pageAiMnoteToolModel(),
pageContext: requestPageContext,
editorTarget: scopedContext.editorTarget,
runTargetSnapshot: runTargetSnapshot,
@@ -2896,6 +3118,10 @@ export function createSidebarPageAiRuntime(context) {
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
void pageAiLoadRuntimeStatus().catch(function(error) {
pageUiState.pageAiRuntimeStatusError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
return;
}
if (allowQueue) {
@@ -3114,8 +3340,13 @@ export function createSidebarPageAiRuntime(context) {
pageAiPersistSessions();
} finally {
if (!allowQueue) pageUiState.pageAiBusy = false;
await pageAiLoadRuntimeStatus().catch(function(error) {
pageUiState.pageAiRuntimeStatusError = error instanceof Error ? error.message : String(error);
return null;
});
renderPageAiConversation();
renderPageAiControls();
pageAiStartNextClientQueuedPrompt();
}
}
@@ -3148,6 +3379,20 @@ export function createSidebarPageAiRuntime(context) {
void pageAiStopRun();
return true;
}
var pageAiReset = closestAction(event.target, '[data-page-ai-action="reset-runtime"]');
if (pageAiReset) {
event.preventDefault();
void pageAiResetRuntime();
return true;
}
var pageAiStopAllJobs = closestAction(event.target, '[data-page-ai-action="stop-all-jobs"]');
if (pageAiStopAllJobs) {
event.preventDefault();
pageUiState.pageAiMessages.push({ role: 'assistant', content: '当前没有可停止的后台 job;长任务 job 将在 runtime status 中独立显示。' });
renderPageAiConversation();
renderPageAiControls();
return true;
}
var pageAiRotate = closestAction(event.target, '[data-page-ai-action="rotate"]');
if (pageAiRotate) {
event.preventDefault();
@@ -3167,7 +3412,15 @@ export function createSidebarPageAiRuntime(context) {
if (pageAiTab) {
event.preventDefault();
pageUiState.pageAiPage = pageAiTab.getAttribute('data-page-ai-tab') || 'chat';
if (pageUiState.pageAiPage === 'runtime') void pageAiLoadGatewayHealth();
if (pageUiState.pageAiPage === 'runtime') pageUiState.pageAiPage = 'status';
if (pageUiState.pageAiPage === 'chat-only-settings') pageUiState.pageAiPage = 'agent';
if (pageUiState.pageAiPage === 'status') {
void pageAiLoadGatewayHealth();
void pageAiLoadRuntimeStatus().catch(function(error) {
pageUiState.pageAiRuntimeStatusError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
}
if (pageUiState.pageAiPage === 'skills') void pageAiLoadSkills();
renderPageAiControls();
renderPageAiConversation();
@@ -3351,7 +3604,30 @@ export function createSidebarPageAiRuntime(context) {
var pageAiPermissionAction = closestAction(event.target, '[data-page-ai-permission-action]');
if (pageAiPermissionAction) {
event.preventDefault();
pageAiResolvePermission(pageAiPermissionAction.getAttribute('data-page-ai-permission-id') || '', pageAiPermissionAction.getAttribute('data-page-ai-permission-action') || 'deny');
pageAiResolvePermission(
pageAiPermissionAction.getAttribute('data-page-ai-permission-id') || '',
pageAiPermissionAction.getAttribute('data-page-ai-permission-action') || 'deny',
pageAiPermissionAction.getAttribute('data-page-ai-permission-option-id') || ''
);
return true;
}
var pageAiPlanAction = closestAction(event.target, '[data-page-ai-plan-action]');
if (pageAiPlanAction) {
event.preventDefault();
var action = String(pageAiPlanAction.getAttribute('data-page-ai-plan-action') || '').trim();
var inputNode = ensurePageAiDrawer().querySelector('[data-page-ai-input]');
if (action === 'approve') {
void sendPageAiMessage('批准,开始执行。');
return true;
}
if (action === 'cancel') {
void sendPageAiMessage('取消执行。');
return true;
}
if (inputNode instanceof HTMLTextAreaElement) {
inputNode.value = action === 'revise' ? '请修改计划:' : '';
inputNode.focus();
}
return true;
}
var pageAiOpenLocationAction = closestAction(event.target, '[data-page-ai-open-location]');
@@ -3474,6 +3750,13 @@ export function createSidebarPageAiRuntime(context) {
renderPageAiConversation();
return true;
}
var pageAiSessionStatusFilter = closestAction(event.target, '[data-page-ai-session-status-filter]');
if (pageAiSessionStatusFilter instanceof HTMLSelectElement) {
pageUiState.pageAiSessionStatusFilter = String(pageAiSessionStatusFilter.value || 'all').trim() || 'all';
renderPageAiControls();
renderPageAiConversation();
return true;
}
var pageAiHideHermesBuiltin = closestAction(event.target, '[data-page-ai-hide-hermes-builtin]');
if (pageAiHideHermesBuiltin instanceof HTMLInputElement) {
pageAiSetHideHermesBuiltinSkills(pageAiHideHermesBuiltin.checked);
@@ -98,12 +98,16 @@ export function createSidebarPageAiSessionRuntime(context) {
acpSessionId: String(session && (session.acpSessionId || session.acp_session_id) || '').trim(),
runId: String(session && (session.runId || session.run_id) || '').trim(),
status: String(session && session.status || '').trim(),
runtimeMode: String(session && (session.runtimeMode || session.runtime_mode) || '').trim(),
replaySeen: Boolean(session && session.replaySeen),
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) {
if (a.id === pageUiState.pageAiActiveSessionId && b.id !== pageUiState.pageAiActiveSessionId) return -1;
if (b.id === pageUiState.pageAiActiveSessionId && a.id !== pageUiState.pageAiActiveSessionId) return 1;
return Number(b.updatedAt || 0) - Number(a.updatedAt || 0);
});
}
@@ -117,7 +121,8 @@ export function createSidebarPageAiSessionRuntime(context) {
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();
var status = String(row.status || (row.runtime && row.runtime.status) || '').trim();
var runtime = row.runtime && typeof row.runtime === 'object' ? row.runtime : {};
var status = String(row.status || runtime.status || '').trim();
var hasConversationSignal = String(payload.message || payload.input || row.snippet || '').trim()
|| pageAiNormalizeArray(row.messages).length > 0;
if (status === 'session.created' && !hasConversationSignal) return null;
@@ -148,6 +153,8 @@ export function createSidebarPageAiSessionRuntime(context) {
acpSessionId: String(row.acpSessionId || row.acp_session_id || payload.acpSessionId || payload.acp_session_id || '').trim(),
runId: String(row.runId || row.run_id || '').trim(),
status: status,
runtimeMode: String(row.runtimeMode || row.runtime_mode || runtime.mode || payload.reasonixSessionMode || '').trim(),
replaySeen: Boolean(row.replaySeen || row.replay_seen || runtime.replaySeen || payload.replay === true),
usage: row.usage && typeof row.usage === 'object' ? row.usage : null,
preview: String(payload.message || row.snippet || '').trim(),
messages: []
@@ -209,7 +216,7 @@ export function createSidebarPageAiSessionRuntime(context) {
}
async function pageAiLoadBackendSessions() {
var response = await fetch('/api/hermes/client/sessions?' + pageAiBackendSessionQuery({ limit: 50 }), {
var response = await fetch('/api/page-ai/sessions?' + pageAiBackendSessionQuery({ limit: 50 }), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
@@ -374,7 +381,7 @@ export function createSidebarPageAiSessionRuntime(context) {
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 }), {
var response = await fetch('/api/page-ai/sessions/' + encodeURIComponent(sessionId) + '?' + pageAiBackendSessionQuery({ limit: 200 }), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
@@ -395,7 +402,7 @@ export function createSidebarPageAiSessionRuntime(context) {
renderPageAiConversation();
return [];
}
var response = await fetch('/api/hermes/client/sessions/search?' + pageAiBackendSessionQuery({ q: q, limit: 20 }), {
var response = await fetch('/api/page-ai/sessions/search?' + pageAiBackendSessionQuery({ q: q, limit: 20 }), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
@@ -414,7 +421,7 @@ export function createSidebarPageAiSessionRuntime(context) {
async function pageAiExportBackendSession(sessionId) {
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
if (!sessionId) return null;
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/export?' + pageAiBackendSessionQuery({ limit: 200 }), {
var response = await fetch('/api/page-ai/sessions/' + encodeURIComponent(sessionId) + '/export?' + pageAiBackendSessionQuery({ limit: 200 }), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
@@ -454,7 +461,7 @@ export function createSidebarPageAiSessionRuntime(context) {
var current = pageAiCurrentSession();
var runProfile = pageAiRunProfile();
if (!forceCreate && current && String(current.id || '').startsWith('mnote_') && current.profile === runProfile) return current;
var response = await fetch('/api/hermes/client/sessions', {
var response = await fetch('/api/page-ai/sessions', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
@@ -504,7 +511,7 @@ export function createSidebarPageAiSessionRuntime(context) {
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({}), {
var response = await fetch('/api/page-ai/sessions/' + encodeURIComponent(current.id) + '/resume?' + pageAiBackendSessionQuery({}), {
method: 'POST',
headers: { 'accept': 'application/json' }
});
@@ -637,7 +644,7 @@ export function createSidebarPageAiSessionRuntime(context) {
if (title === null) return;
title = String(title || '').trim();
if (!title) return;
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/rename?' + pageAiBackendSessionQuery({}), {
var response = await fetch('/api/page-ai/sessions/' + encodeURIComponent(sessionId) + '/rename?' + pageAiBackendSessionQuery({}), {
method: 'POST',
headers: { 'content-type': 'application/json', 'accept': 'application/json' },
body: JSON.stringify({ title: title })
@@ -712,13 +719,13 @@ export function createSidebarPageAiSessionRuntime(context) {
var sessions = ids.map(pageAiFindSessionById).filter(Boolean);
if (!sessions.length) return;
var confirmText = sessions.length === 1
? '确定删除 AI 会话“' + (sessions[0].title || sessions[0].id) + '”吗?'
: '确定删除选中的 ' + String(sessions.length) + ' 个 AI 会话吗?';
? '确定删除 AI 会话“' + (sessions[0].title || sessions[0].id) + '”吗?仅删除 MNote 历史,不删除外部 Hermes/provider 会话。'
: '确定删除选中的 ' + String(sessions.length) + ' 个 AI 会话吗?仅删除 MNote 历史,不删除外部 Hermes/provider 会话。';
if (!win.confirm(confirmText)) return;
for (var index = 0; index < sessions.length; index += 1) {
var session = sessions[index];
if (!pageAiSessionRequiresBackendDelete(session)) continue;
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(session.id) + '?' + pageAiBackendSessionQuery({}), {
var response = await fetch('/api/page-ai/sessions/' + encodeURIComponent(session.id) + '?' + pageAiBackendSessionQuery({}), {
method: 'DELETE',
headers: { 'accept': 'application/json' }
});
@@ -745,7 +752,7 @@ export function createSidebarPageAiSessionRuntime(context) {
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
if (!sessionId) return;
pageAiSetActiveSession(sessionId);
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/resume?' + pageAiBackendSessionQuery({}), {
var response = await fetch('/api/page-ai/sessions/' + encodeURIComponent(sessionId) + '/resume?' + pageAiBackendSessionQuery({}), {
method: 'POST',
headers: { 'accept': 'application/json' }
});
@@ -1684,11 +1684,12 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
}
var controller = typeof AbortController === 'function' ? new AbortController() : null;
var timer = 0;
var timeout = Math.max(1000, Number(timeoutMs) || 15000);
try {
if (controller) {
timer = window.setTimeout(function() {
controller.abort();
}, Math.max(1000, Number(timeoutMs) || 15000));
}, timeout);
}
var nextInit = Object.assign({}, init || {});
if (controller) nextInit.signal = controller.signal;
@@ -1703,6 +1704,16 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
}
}
function uploadTimeoutMsForFile(file, requestedMs) {
var runtimeFn = localUploadRuntimeFunction('uploadTimeoutMsForFile');
if (runtimeFn) return runtimeFn(file, requestedMs);
var requested = Number(requestedMs) || 0;
var size = Number(file && file.size || 0);
if (!Number.isFinite(size) || size <= 0) return Math.max(requested, 15000);
var sizeBased = 15000 + Math.ceil(size / (1024 * 1024)) * 12000;
return Math.min(10 * 60 * 1000, Math.max(requested, 60000, sizeBased));
}
async function insertUploadedAssetIntoEditor(asset, targetRoot) {
var runtimeFn = localUploadRuntimeFunction('insertUploadedAssetIntoEditor');
if (runtimeFn) {
@@ -1842,7 +1853,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
var localResult = await localUploadRuntime(file, plan, {
rootUri: rootUri,
documentId: documentId,
timeoutMs: 15000
timeoutMs: uploadTimeoutMsForFile(file, 15000)
});
localPayload = localResult && localResult.asset ? { asset: localResult.asset } : null;
} else {
@@ -1857,7 +1868,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
method: 'POST',
credentials: 'include',
body: localForm
}, 15000, '本地上传');
}, uploadTimeoutMsForFile(file, 15000), '本地上传');
localPayload = await localResponse.json().catch(function() { return null; });
if (!localResponse.ok || !localPayload || !localPayload.asset) {
throw new Error(localPayload && localPayload.error ? localPayload.error : '上传失败');
+19 -10
View File
@@ -219,7 +219,7 @@ impl AcpClient {
///
/// Returns `Result<R>` where `R` is the deserialized `result` field.
/// On JSON-RPC error, returns [`AcpError::JsonRpc`].
/// Default timeout: 300 seconds.
/// Default timeout: 300 seconds. Override with `MNOTE_ACP_REQUEST_TIMEOUT_SECS`.
///
/// Reference: `acpClient.ts` L95-110 (`call()` method)
pub async fn request<P: Serialize, R: DeserializeOwned>(
@@ -227,7 +227,12 @@ impl AcpClient {
method: &str,
params: P,
) -> Result<R, AcpError> {
self.request_with_timeout(method, params, Duration::from_secs(300))
let timeout_secs = std::env::var("MNOTE_ACP_REQUEST_TIMEOUT_SECS")
.ok()
.and_then(|value| value.parse::<u64>().ok())
.filter(|value| *value >= 30)
.unwrap_or(300);
self.request_with_timeout(method, params, Duration::from_secs(timeout_secs))
.await
}
@@ -252,10 +257,12 @@ impl AcpClient {
let line = serde_json::to_string(&req)?;
debug!("ACP --> {} #{} ({} bytes)", method, id, line.len());
let mut writer = self.writer.lock().await;
writer.write_all(line.as_bytes()).await?;
writer.write_all(b"\n").await?;
writer.flush().await?;
{
let mut writer = self.writer.lock().await;
writer.write_all(line.as_bytes()).await?;
writer.write_all(b"\n").await?;
writer.flush().await?;
}
match timeout(dur, rx).await {
Ok(Ok(Ok(value))) => {
@@ -280,10 +287,12 @@ impl AcpClient {
let line = serde_json::to_string(&msg)?;
debug!("ACP ~~> {} ({} bytes)", method, line.len());
let mut writer = self.writer.lock().await;
writer.write_all(line.as_bytes()).await?;
writer.write_all(b"\n").await?;
writer.flush().await?;
{
let mut writer = self.writer.lock().await;
writer.write_all(line.as_bytes()).await?;
writer.write_all(b"\n").await?;
writer.flush().await?;
}
Ok(())
}
+307 -15
View File
@@ -187,6 +187,33 @@ impl AcpSessionManager {
.unwrap_or("session/request_permission")
.to_string();
if acp_permission_tool_auto_allowed(&tool_name)
|| acp_permission_params_contain_auto_allowed_tool(&params)
{
let response = permission_response_for_decision(&params, "allow");
if let Some(Ok(result)) = response {
let handler = event_handler_for_incoming.lock().unwrap();
if let Some(ref h) = *handler {
h(AcpSessionEvent::PermissionRequest {
permission_id: permission_id.clone(),
tool_name: tool_name.clone(),
params: params.clone(),
decision: "allowed".into(),
});
}
drop(handler);
let client_for_allow = client_for_incoming.clone();
let allow_jsonrpc_id = id.clone();
tokio::spawn(async move {
let _ = client_for_allow
.respond_to_incoming(allow_jsonrpc_id, result)
.await;
});
return true;
}
}
// 记录到 pending map,等待前端 allow / deny。
{
let mut pending = pending_for_incoming.lock().unwrap();
@@ -336,7 +363,8 @@ impl AcpSessionManager {
pub async fn resolve_permission(
&self,
permission_id: &str,
decision: &str,
decision: Option<&str>,
option_id: Option<&str>,
) -> Result<(), String> {
let pending = {
let mut map = self.pending_permissions.lock().unwrap();
@@ -367,20 +395,46 @@ impl AcpSessionManager {
.unwrap_or("session/request_permission")
.to_string();
let normalized_decision = match decision {
"allow" | "allowed" => "allow",
"deny" | "denied" => "deny",
other => return Err(format!("unknown decision: {other} (expected allow/deny)")),
let selected_option_id = option_id
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);
let normalized_decision = if let Some(option_id) = selected_option_id.as_deref() {
if permission_option_is_reject_like(&pending.params, option_id) {
"deny"
} else {
"allow"
}
} else {
match decision.unwrap_or("").trim() {
"allow" | "allowed" => "allow",
"deny" | "denied" => "deny",
other => {
return Err(format!(
"unknown decision: {other} (expected allow/deny or optionId)"
))
}
}
};
let response = permission_response_for_decision(&pending.params, normalized_decision)
.unwrap_or_else(|| {
if normalized_decision == "allow" {
Err((-32000, "permission allow option not available".into()))
} else {
Err((-32000, "permission denied by user".into()))
}
});
let response = if let Some(option_id) = selected_option_id.as_deref() {
permission_response_for_option_id(&pending.params, option_id).unwrap_or_else(|| {
Err((
-32000,
format!("permission option not available: {option_id}"),
))
})
} else {
permission_response_for_decision(&pending.params, normalized_decision).unwrap_or_else(
|| {
if normalized_decision == "allow" {
Err((-32000, "permission allow option not available".into()))
} else {
Err((-32000, "permission denied by user".into()))
}
},
)
};
match response {
Ok(result) => {
@@ -867,10 +921,69 @@ fn permission_response_for_decision(
}
}
fn permission_response_for_option_id(
params: &Value,
option_id: &str,
) -> Option<Result<Value, (i64, String)>> {
let options = params.get("options").and_then(Value::as_array)?;
let selected = option_id.trim();
if selected.is_empty() {
return None;
}
let exists = options
.iter()
.filter_map(permission_option_id)
.any(|id| id == selected);
if !exists {
return None;
}
Some(Ok(json!({
"outcome": {
"outcome": "selected",
"optionId": selected
}
})))
}
fn acp_permission_tool_auto_allowed(tool_name: &str) -> bool {
ACP_AUTO_ALLOWED_PERMISSION_TOOLS.contains(&tool_name)
}
const ACP_AUTO_ALLOWED_PERMISSION_TOOLS: &[&str] = &[
"mcp__mnote_lightrag_bridge__mnote_knowledge_rag_status",
"mcp__mnote_lightrag_bridge__mnote_knowledge_rag_query",
"mcp__mnote_lightrag_bridge__mnote_knowledge_rag_section_context",
"mcp__mnote_lightrag_bridge__mnote_knowledge_rag_open_reference",
"mcp__mnote_lightrag_bridge__query_knowledge_graph",
"mcp__mnote_lightrag_bridge__open_mnote_reference",
];
fn acp_permission_params_contain_auto_allowed_tool(value: &Value) -> bool {
match value {
Value::String(text) => ACP_AUTO_ALLOWED_PERMISSION_TOOLS
.iter()
.any(|tool_name| text.contains(tool_name)),
Value::Array(items) => items
.iter()
.any(acp_permission_params_contain_auto_allowed_tool),
Value::Object(map) => map
.values()
.any(acp_permission_params_contain_auto_allowed_tool),
_ => false,
}
}
fn permission_option_id_by_decision(params: &Value, decision: &str) -> Option<String> {
let options = params.get("options").and_then(Value::as_array)?;
let preferred: &[&str] = if decision == "allow" {
&["allow_once", "allow", "approve", "yes"]
&[
"allow_once",
"allow",
"approve",
"yes",
"allow_always",
"allow_persistent",
]
} else {
&["deny_once", "reject_once", "deny", "reject", "no"]
};
@@ -922,6 +1035,36 @@ fn permission_option_id(option: &Value) -> Option<String> {
.map(ToOwned::to_owned)
}
fn permission_option_is_reject_like(params: &Value, option_id: &str) -> bool {
let selected = option_id.trim();
if selected.is_empty() {
return false;
}
let Some(options) = params.get("options").and_then(Value::as_array) else {
return false;
};
options.iter().any(|option| {
if permission_option_id(option).as_deref() != Some(selected) {
return false;
}
let haystack = [
option
.get("optionId")
.or_else(|| option.get("option_id"))
.or_else(|| option.get("id"))
.and_then(Value::as_str)
.unwrap_or(""),
option.get("kind").and_then(Value::as_str).unwrap_or(""),
option.get("name").and_then(Value::as_str).unwrap_or(""),
]
.join(" ")
.to_ascii_lowercase();
["reject", "deny", "cancel", "stop", "no"]
.iter()
.any(|needle| haystack.contains(needle))
})
}
// ── Tests ────────────────────────────────────────────
#[cfg(test)]
@@ -979,6 +1122,55 @@ rl.on('line', (line) => {
Arc::new(client)
}
/// Creates a mock ACP that asks for permission during session/prompt and
/// only completes after MNote responds to that incoming request.
async fn spawn_mock_acp_with_permission_gate() -> Arc<AcpClient> {
let script = r#"
import * as readline from 'node:readline';
import { stdin as input, stdout as output } from 'node:process';
const rl = readline.createInterface({ input, output, terminal: false });
let promptId = null;
let sessionId = 'test_session_1';
rl.on('line', (line) => {
const msg = JSON.parse(line);
if (msg.method === 'initialize') {
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: { protocolVersion: 1, agentCapabilities: {}, agentInfo: { name: 'mock', version: '1.0' }, authMethods: [] } }) + '\n');
} else if (msg.method === 'session/new') {
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: { sessionId } }) + '\n');
} else if (msg.method === 'session/prompt') {
promptId = msg.id;
process.stdout.write(JSON.stringify({
jsonrpc: '2.0',
id: 7001,
method: 'session/request_permission',
params: {
permissionId: 'perm_prompt_gate_1',
toolCall: { toolCallId: 'perm_prompt_gate_1', title: 'bash', kind: 'execute' },
options: [
{ optionId: 'allow_once', name: 'Allow', kind: 'allow_once' },
{ optionId: 'reject_once', name: 'Reject', kind: 'reject_once' }
]
}
}) + '\n');
} else if (msg.id === 7001 && msg.result) {
process.stdout.write(JSON.stringify({
jsonrpc: '2.0',
method: 'session/update',
params: { sessionId, update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: 'PERMISSION_GATE_CONTINUED' } } }
}) + '\n');
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: promptId, result: { stopReason: 'end_turn' } }) + '\n');
}
});
"#;
let dir = std::env::temp_dir();
let script_path = dir.join("acp_session_permission_gate_mock.mjs");
std::fs::write(&script_path, script).expect("write permission mock");
let client = AcpClient::spawn("node", &[script_path.to_str().unwrap()])
.await
.expect("spawn permission mock");
Arc::new(client)
}
#[tokio::test]
async fn test_create_session() {
let client = spawn_mock_acp().await;
@@ -1048,6 +1240,58 @@ rl.on('line', (line) => {
);
}
#[tokio::test]
async fn permission_resolution_unblocks_prompt_request() {
let client = spawn_mock_acp_with_permission_gate().await;
let mgr = Arc::new(AcpSessionManager::new(client));
let permission_id = Arc::new(std::sync::Mutex::new(None::<String>));
let permission_id_for_event = permission_id.clone();
mgr.on_event(move |event| {
if let AcpSessionEvent::PermissionRequest {
permission_id,
decision,
..
} = event
{
if decision == "requested" {
*permission_id_for_event.lock().unwrap() = Some(permission_id);
}
}
});
mgr.create_session(Some("/test"), None)
.await
.expect("create_session");
let mgr_for_prompt = mgr.clone();
let prompt_task = tokio::spawn(async move {
mgr_for_prompt
.run_prompt(vec![ContentBlock::Text {
text: "needs permission".into(),
}])
.await
});
let mut observed_permission = None;
for _ in 0..50 {
observed_permission = permission_id.lock().unwrap().clone();
if observed_permission.is_some() {
break;
}
sleep(Duration::from_millis(20)).await;
}
let observed_permission = observed_permission.expect("permission requested");
mgr.resolve_permission(&observed_permission, Some("allow"), Some("allow_once"))
.await
.expect("resolve permission");
let result = tokio::time::timeout(Duration::from_secs(2), prompt_task)
.await
.expect("prompt should not remain blocked")
.expect("prompt task join")
.expect("prompt result");
assert_eq!(format!("{:?}", result.stop_reason), "EndTurn".to_string());
}
/// Creates a mock ACP that supports session/load (success case).
async fn spawn_mock_acp_with_load() -> Arc<AcpClient> {
let script = r#"
@@ -1488,7 +1732,10 @@ rl.on('line', (line) => {
.await
.expect("prompt after load");
let output = chunks.lock().unwrap().join("\n");
assert!(output.contains("CURRENT_PROMPT_AFTER_LOAD"), "prompt output missing: {output}");
assert!(
output.contains("CURRENT_PROMPT_AFTER_LOAD"),
"prompt output missing: {output}"
);
}
#[test]
@@ -1563,4 +1810,49 @@ rl.on('line', (line) => {
})
);
}
#[test]
fn permission_response_can_select_exact_plan_option() {
let params = json!({
"toolCall": {
"toolCallId": "gate-plan_1",
"title": "Approve plan",
"kind": "other"
},
"options": [
{"optionId": "allow_once", "name": "Approve plan", "kind": "allow_once"},
{"optionId": "refine", "name": "Refine", "kind": "allow_once"},
{"optionId": "cancel", "name": "Cancel", "kind": "reject_once"}
]
});
let refine = permission_response_for_option_id(&params, "refine")
.expect("exact option response")
.expect("refine should select exact option");
assert_eq!(refine["outcome"]["optionId"], "refine");
assert!(!permission_option_is_reject_like(&params, "refine"));
assert!(permission_option_is_reject_like(&params, "cancel"));
}
#[test]
fn mnote_lightrag_bridge_read_tools_are_auto_allowed() {
assert!(acp_permission_tool_auto_allowed(
"mcp__mnote_lightrag_bridge__mnote_knowledge_rag_query"
));
assert!(acp_permission_tool_auto_allowed(
"mcp__mnote_lightrag_bridge__mnote_knowledge_rag_section_context"
));
assert!(acp_permission_params_contain_auto_allowed_tool(&json!({
"toolCall": {
"title": "调用工具",
"rawInput": {
"name": "mcp__mnote_lightrag_bridge__mnote_knowledge_rag_section_context"
}
}
})));
assert!(acp_permission_tool_auto_allowed(
"mcp__mnote_lightrag_bridge__open_mnote_reference"
));
assert!(!acp_permission_tool_auto_allowed("bash"));
}
}
@@ -3,7 +3,8 @@ use crate::context::RequestContext;
use crate::error::WebError;
use crate::hermes_tools::ToolCallInput;
use crate::routes::knowledge_rag::{
KnowledgeRagOpenReferenceRequest, KnowledgeRagQueryRequest, KnowledgeRagStatusQuery,
KnowledgeRagOpenReferenceRequest, KnowledgeRagQueryRequest, KnowledgeRagSectionContextRequest,
KnowledgeRagStatusQuery,
};
use axum::extract::{Extension, Json, Query, State};
use serde_json::{json, Value};
@@ -100,6 +101,39 @@ pub async fn open_reference(
Ok(payload)
}
pub async fn section_context(
state: &AppState,
context: &RequestContext,
input: &ToolCallInput,
) -> Result<Value, WebError> {
let mut args = input.args.clone().unwrap_or_else(|| json!({}));
if args.get("workspaceId").is_none() {
if let Some(workspace_id) = input.effective_workspace_id() {
args["workspaceId"] = json!(workspace_id);
}
}
if args.get("rootUri").is_none() {
if let Some(root_uri) = input.effective_root_uri() {
args["rootUri"] = json!(root_uri);
}
}
let body =
serde_json::from_value::<KnowledgeRagSectionContextRequest>(args).map_err(|error| {
WebError::bad_request_code(
"mnote_knowledge_rag_section_context_payload_invalid",
format!("资料库章节上下文参数无效: {error}"),
)
.with_context(context)
})?;
let Json(payload) = crate::routes::knowledge_rag::section_context(
State(state.clone()),
Extension(context.clone()),
Json(body),
)
.await?;
Ok(compact_section_context_for_agent(payload))
}
fn compact_query_result_for_agent(payload: Value) -> Value {
let references = payload
.get("references")
@@ -139,16 +173,25 @@ fn compact_query_result_for_agent(payload: Value) -> Value {
})
.collect::<Vec<_>>();
let ui_citations = citations.clone();
let document_structure_index = payload
.get("documentStructureIndex")
.cloned()
.filter(|value| !value.is_null())
.unwrap_or(Value::Null);
json!({
"ok": payload.get("ok").cloned().unwrap_or_else(|| json!(true)),
"schema": "mnote.knowledge_rag.agent_query_result.v1",
"provider": payload.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
"answerGuidance": "Final answers should answer the user in plain text. Do not copy citationMarkdown, citationUrl, /documents, mnote://, or search-engine wrapped local links into the answer. MNote UI will render citations[] / references[].citationMarkdown after the answer as clickable source locators. Treat rawMetadata.keywords and entity/relation counts only as query analysis, not proof that a source contains those words. Use references[].quote/contentDiagnostics as evidence. If locatorDegraded is true, say the source location is degraded instead of inventing page or bbox. If contentDiagnostics.ocrTextExposed is false, say OCR text was not exposed in the returned reference instead of claiming OCR succeeded.",
"answerGuidance": "Final answers should answer the user in plain text. Do not copy citationMarkdown, citationUrl, /documents, mnote://, or search-engine wrapped local links into the answer. MNote UI will render citations[] / references[].citationMarkdown after the answer as clickable source locators. Treat rawMetadata.keywords and entity/relation counts only as query analysis, not proof that a source contains those words. Use references[].quote/contentDiagnostics as evidence. For book-like or skip-KG documents, call the tool with sourcePaths and includeDocumentStructureIndex=true; MNote fixes their effective retrieval mode to naive because they intentionally do not build KG. If documentStructureIndex is present, use it as a section map and call mnote.knowledge_rag.section_context with the section range when you need bounded chapter text for second-pass reading; do not cite the map itself unless the same claim appears in references[].quote or section_context text. If locatorDegraded is true, say the source location is degraded instead of inventing page or bbox. If contentDiagnostics.ocrTextExposed is false, say OCR text was not exposed in the returned reference instead of claiming OCR succeeded.",
"references": references,
"citations": citations,
"citationMarkdowns": citation_markdowns,
"uiCitations": ui_citations,
"documentStructureIndex": compact_document_structure_index_for_agent(&document_structure_index),
"citationRendering": "MNote UI appends uiCitations/citations after the final answer; agent must not hand-write citation links.",
"requestedRetrievalMode": payload.get("requestedRetrievalMode").cloned().unwrap_or(Value::Null),
"effectiveRetrievalMode": payload.get("effectiveRetrievalMode").cloned().or_else(|| payload.get("retrievalMode").cloned()).unwrap_or(Value::Null),
"retrievalModeReason": payload.get("retrievalModeReason").cloned().unwrap_or(Value::Null),
"sourceScope": payload.get("sourceScope").cloned().unwrap_or(Value::Array(Vec::new())),
"sourceScopeMode": payload.get("sourceScopeMode").cloned().unwrap_or_else(|| json!("post_filter_mapped_references")),
"rawScopeFiltered": payload.get("rawScopeFiltered").cloned().unwrap_or(Value::Bool(false)),
@@ -159,6 +202,168 @@ fn compact_query_result_for_agent(payload: Value) -> Value {
})
}
fn compact_document_structure_index_for_agent(value: &Value) -> Value {
if value.is_null() {
return Value::Null;
}
let documents = value
.get("documents")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.take(4)
.map(|doc| {
let mut sections = doc
.get("sections")
.and_then(Value::as_array)
.map(|sections| {
sections
.iter()
.filter(|section| {
section
.get("queryMatchCount")
.and_then(Value::as_u64)
.unwrap_or(0)
> 0
|| section
.get("matchedReferenceCount")
.and_then(Value::as_u64)
.unwrap_or(0)
> 0
})
.take(12)
.map(compact_structure_section_for_agent)
.collect::<Vec<_>>()
})
.unwrap_or_default();
if sections.is_empty() {
sections = doc
.get("sections")
.and_then(Value::as_array)
.map(|sections| {
sections
.iter()
.take(12)
.map(compact_structure_section_for_agent)
.collect::<Vec<_>>()
})
.unwrap_or_default();
}
json!({
"sourceRootRelativePath": doc.get("sourceRootRelativePath").cloned().unwrap_or(Value::Null),
"sourceId": doc.get("sourceId").cloned().unwrap_or(Value::Null),
"lightRagDocId": doc.get("lightRagDocId").cloned().unwrap_or(Value::Null),
"sectionCount": doc.get("sectionCount").cloned().unwrap_or(Value::Null),
"queryMatchedSections": doc.get("queryMatchedSections").cloned().unwrap_or(Value::Null),
"referenceMatchedSections": doc.get("referenceMatchedSections").cloned().unwrap_or(Value::Null),
"sections": sections,
})
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
json!({
"schema": value.get("schema").cloned().unwrap_or_else(|| json!("mnote.knowledge_rag.document_structure_index.v1")),
"mode": value.get("mode").cloned().unwrap_or(Value::Null),
"referenceCount": value.get("referenceCount").cloned().unwrap_or(Value::Null),
"documents": documents,
})
}
fn compact_structure_section_for_agent(section: &Value) -> Value {
json!({
"sectionId": section.get("sectionId").cloned().unwrap_or(Value::Null),
"title": section.get("title").cloned().unwrap_or(Value::Null),
"level": section.get("level").cloned().unwrap_or(Value::Null),
"headingPath": section.get("headingPath").cloned().unwrap_or_else(|| json!([])),
"startBlockOrdinal": section.get("startBlockOrdinal").cloned().unwrap_or(Value::Null),
"endBlockOrdinal": section.get("endBlockOrdinal").cloned().unwrap_or(Value::Null),
"startParagraphOrdinal": section.get("startParagraphOrdinal").cloned().unwrap_or(Value::Null),
"endParagraphOrdinal": section.get("endParagraphOrdinal").cloned().unwrap_or(Value::Null),
"blockCount": section.get("blockCount").cloned().unwrap_or(Value::Null),
"queryMatchCount": section.get("queryMatchCount").cloned().unwrap_or(Value::Null),
"matchedReferenceCount": section.get("matchedReferenceCount").cloned().unwrap_or(Value::Null),
"sample": section
.get("sample")
.and_then(Value::as_str)
.map(|value| value.chars().take(220).collect::<String>())
.unwrap_or_default(),
})
}
fn compact_section_context_for_agent(payload: Value) -> Value {
let blocks = payload
.get("blocks")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.take(40)
.map(|block| {
let text = block
.get("text")
.and_then(Value::as_str)
.map(|value| value.chars().take(900).collect::<String>())
.unwrap_or_default();
json!({
"blockOrdinal": block.get("blockOrdinal").cloned().unwrap_or(Value::Null),
"blockId": block.get("blockId").cloned().unwrap_or(Value::Null),
"paragraphOrdinal": block.get("paragraphOrdinal").cloned().unwrap_or(Value::Null),
"headingPath": block.get("headingPath").cloned().unwrap_or_else(|| json!([])),
"text": text,
"textTruncated": block.get("textTruncated").cloned().unwrap_or(Value::Bool(false)),
"locator": block.get("locator").cloned().unwrap_or(Value::Null),
})
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
let text = payload
.get("text")
.and_then(Value::as_str)
.map(|value| value.chars().take(12_000).collect::<String>())
.unwrap_or_default();
let chunks = payload
.get("chunks")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.take(20)
.map(|chunk| {
let text = chunk
.get("text")
.and_then(Value::as_str)
.map(|value| value.chars().take(2_400).collect::<String>())
.unwrap_or_default();
json!({
"chunkOrdinal": chunk.get("chunkOrdinal").cloned().unwrap_or(Value::Null),
"startBlockOrdinal": chunk.get("startBlockOrdinal").cloned().unwrap_or(Value::Null),
"endBlockOrdinal": chunk.get("endBlockOrdinal").cloned().unwrap_or(Value::Null),
"blockIds": chunk.get("blockIds").cloned().unwrap_or_else(|| json!([])),
"text": text,
})
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
json!({
"ok": payload.get("ok").cloned().unwrap_or_else(|| json!(true)),
"schema": payload.get("schema").cloned().unwrap_or_else(|| json!("mnote.knowledge_rag.section_context.v1")),
"provider": payload.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
"sourceId": payload.get("sourceId").cloned().unwrap_or(Value::Null),
"sourceRootRelativePath": payload.get("sourceRootRelativePath").cloned().unwrap_or(Value::Null),
"lightRagDocId": payload.get("lightRagDocId").cloned().unwrap_or(Value::Null),
"section": payload.get("section").cloned().unwrap_or(Value::Null),
"limits": payload.get("limits").cloned().unwrap_or(Value::Null),
"blocks": blocks,
"chunks": chunks,
"text": text,
"usageGuidance": "This is bounded sidecar context for interpreting a documentStructureIndex section. Use it as supporting reading context, but cite final answers with references/citations returned by mnote.knowledge_rag.query when possible.",
})
}
fn compact_reference_for_agent(reference: &Value) -> Value {
let quote = reference
.get("displayQuote")
@@ -14,6 +14,7 @@ pub fn manifest() -> Value {
doc_find_tool(),
knowledge_rag_status_tool(),
knowledge_rag_query_tool(),
knowledge_rag_section_context_tool(),
knowledge_rag_open_reference_tool(),
block_fetch_tool(),
doc_plan_update_tool(),
@@ -328,6 +329,14 @@ fn knowledge_rag_query_tool() -> Value {
"includeChunkContent".into(),
json!({ "type": "boolean", "default": true }),
);
map.insert(
"includeDocumentStructureIndex".into(),
json!({
"type": "boolean",
"default": false,
"description": "返回由 LightRAG sidecar headings 派生的 document_structure_index;适合大书/长文档问题做章节导航和上下文扩展,不替代 references 引用证据。"
}),
);
map.insert(
"sourcePaths".into(),
json!({
@@ -376,6 +385,51 @@ fn knowledge_rag_open_reference_tool() -> Value {
})
}
fn knowledge_rag_section_context_tool() -> Value {
let mut properties = base_identity_properties();
if let Value::Object(map) = &mut properties {
map.insert("rootUri".into(), json!({ "type": "string" }));
map.insert("sourcePath".into(), json!({ "type": "string" }));
map.insert("sourceId".into(), json!({ "type": "string" }));
map.insert("lightRagDocId".into(), json!({ "type": "string" }));
map.insert("filePath".into(), json!({ "type": "string" }));
map.insert("sectionId".into(), json!({ "type": "string" }));
map.insert("startBlockOrdinal".into(), json!({ "type": "integer" }));
map.insert("endBlockOrdinal".into(), json!({ "type": "integer" }));
map.insert("startParagraphOrdinal".into(), json!({ "type": "integer" }));
map.insert("endParagraphOrdinal".into(), json!({ "type": "integer" }));
map.insert(
"contextBefore".into(),
json!({ "type": "integer", "default": 1 }),
);
map.insert(
"contextAfter".into(),
json!({ "type": "integer", "default": 1 }),
);
map.insert(
"maxBlocks".into(),
json!({ "type": "integer", "default": 24 }),
);
map.insert(
"maxChars".into(),
json!({ "type": "integer", "default": 12000 }),
);
}
json!({
"name": "mnote.knowledge_rag.section_context",
"description": "按 documentStructureIndex section 的 block/paragraph range,从 LightRAG native sidecar 拉取有限正文 blocks/chunks,供大书/长文档二次解读;不做检索、重排或 fallback。",
"schemaVersion": TOOL_SCHEMA_VERSION,
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
"status": "available",
"annotations": tool_annotations(true, false, true, false),
"inputSchema": {
"type": "object",
"required": ["workspaceId", "rootUri"],
"properties": properties
}
})
}
// 旧 local index agent 工具仅保留为历史对照;当前 manifest() 不注册这些工具。
#[allow(dead_code)]
fn index_status_tool() -> Value {
@@ -49,6 +49,7 @@ const CAPABILITY_PACKS: &[MnoteCapabilityPack] = &[
"mnote.context.resolve_target",
"mnote.knowledge_rag.status",
"mnote.knowledge_rag.query",
"mnote.knowledge_rag.section_context",
"mnote.knowledge_rag.open_reference",
],
content: include_str!("../../../../../skills/mnote-knowledge-rag/SKILL.md"),
File diff suppressed because it is too large Load Diff
@@ -374,6 +374,9 @@ pub(crate) async fn execute_mnote_tool_call(
}
"mnote.knowledge_rag.status" => knowledge_rag::status(&state, &context, &input).await,
"mnote.knowledge_rag.query" => knowledge_rag::query(&state, &context, &input).await,
"mnote.knowledge_rag.section_context" => {
knowledge_rag::section_context(&state, &context, &input).await
}
"mnote.knowledge_rag.open_reference" => {
knowledge_rag::open_reference(&state, &context, &input).await
}
@@ -726,6 +729,7 @@ fn is_read_tool(tool_name: &str) -> bool {
| "mnote.evidence.open"
| "mnote.knowledge_rag.status"
| "mnote.knowledge_rag.query"
| "mnote.knowledge_rag.section_context"
| "mnote.knowledge_rag.open_reference"
| "mnote.index.status"
| "mnote.index.refresh"
@@ -758,6 +762,7 @@ fn is_evidence_receipt_tool(tool_name: &str) -> bool {
| "mnote.evidence.open"
| "mnote.knowledge_rag.status"
| "mnote.knowledge_rag.query"
| "mnote.knowledge_rag.section_context"
| "mnote.knowledge_rag.open_reference"
)
}
File diff suppressed because it is too large Load Diff
+46
View File
@@ -91,6 +91,10 @@ pub fn build_router(state: AppState) -> Router {
.route("/api/knowledge-rag/ingest", post(knowledge_rag::ingest))
.route("/api/knowledge-rag/search", post(knowledge_rag::search))
.route("/api/knowledge-rag/query", post(knowledge_rag::query_rag))
.route(
"/api/knowledge-rag/section-context",
post(knowledge_rag::section_context),
)
.route(
"/api/knowledge-rag/open-reference",
post(knowledge_rag::open_reference),
@@ -517,6 +521,46 @@ pub fn build_router(state: AppState) -> Router {
"/api/page-ai/sessions/{session_id}/active-run",
get(hermes_client::get_page_ai_session_active_run),
)
.route(
"/api/page-ai/runtime/status",
get(hermes_client::get_page_ai_runtime_status),
)
.route(
"/api/page-ai/runtime/reset",
post(hermes_client::reset_page_ai_runtime),
)
.route(
"/api/page-ai/sessions",
get(hermes_client::list_sessions).post(hermes_client::create_session),
)
.route(
"/api/page-ai/sessions/search",
get(hermes_client::search_sessions),
)
.route(
"/api/page-ai/sessions/{session_id}",
get(hermes_client::get_session).delete(hermes_client::delete_session),
)
.route(
"/api/page-ai/sessions/{session_id}/resume",
post(hermes_client::resume_session),
)
.route(
"/api/page-ai/sessions/{session_id}/rename",
post(hermes_client::rename_session),
)
.route(
"/api/page-ai/sessions/{session_id}/export",
get(hermes_client::export_session),
)
.route(
"/api/page-ai/sessions/{session_id}/auto-title",
post(hermes_client::auto_title_session),
)
.route(
"/api/page-ai/sessions/{session_id}/queue/{queue_id}",
delete(hermes_client::cancel_queued_run),
)
.route(
"/api/onlyoffice/bridge/commands",
post(onlyoffice_bridge::enqueue_command),
@@ -936,6 +980,8 @@ mod tests {
assert!(!html.contains(">下载<"));
assert!(html.contains(r#"data-page-width-content-type="word""#));
assert!(html.contains("applyPreviewWidthPreference"));
assert!(html.contains("data-mnote-office-preview-footnotes-disabled"));
assert!(html.contains("renderFootnotes: false"));
}
#[tokio::test]
+16 -2
View File
@@ -1930,13 +1930,27 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
}}
const buffer = await fetchArrayBuffer();
viewer.replaceChildren();
await window.docx.renderAsync(buffer, viewer, null, {{
const options = {{
className: 'mnote-docx',
inWrapper: true,
breakPages: true,
renderHeaders: true,
renderFooters: true
}});
}};
try {{
await window.docx.renderAsync(buffer, viewer, null, options);
}} catch (error) {{
if (!String(error && error.message || '').includes("reading 'type'")) {{
throw error;
}}
viewer.replaceChildren();
document.documentElement.setAttribute('data-mnote-office-preview-footnotes-disabled', 'true');
await window.docx.renderAsync(buffer.slice(0), viewer, null, {{
...options,
renderFootnotes: false,
renderEndnotes: false
}});
}}
}}
async function renderWorkbook() {{
+298 -2
View File
@@ -2626,7 +2626,7 @@ body {
}
.mnote-main-tab {
height: 34px;
height: 36px;
max-width: 240px;
min-width: 0;
display: inline-flex;
@@ -2639,7 +2639,7 @@ body {
padding: 0 9px;
cursor: pointer;
font-size: 13px;
line-height: 1;
line-height: 20px;
}
.mnote-main-tab:hover {
@@ -2725,6 +2725,7 @@ body {
.mnote-main-tab-title {
min-width: 0;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -4731,6 +4732,39 @@ html[data-mnote-page-ai-resizing="true"] {
content: "·";
}
.wolai-page-ai-runtime-strip {
display: flex;
width: 100%;
min-height: 28px;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding: 0 14px 8px;
border: 0;
background: transparent;
color: #6B6762;
cursor: pointer;
font-size: 11px;
line-height: 16px;
text-align: left;
}
.wolai-page-ai-runtime-strip:hover span {
border-color: rgba(27, 28, 28, 0.16);
background: #F1F1EF;
}
.wolai-page-ai-runtime-strip span {
max-width: 100%;
overflow: hidden;
padding: 2px 6px;
border: 1px solid rgba(27, 28, 28, 0.08);
border-radius: 6px;
background: #F8F8F7;
text-overflow: ellipsis;
white-space: nowrap;
}
.wolai-page-ai-statusvalue {
display: block;
min-width: 0;
@@ -5024,6 +5058,11 @@ html[data-mnote-page-ai-resizing="true"] {
overflow: auto;
}
.wolai-page-ai-settings-stack {
display: grid;
gap: 10px;
}
.wolai-page-ai-memory-card {
display: grid;
gap: 8px;
@@ -5396,6 +5435,228 @@ html[data-mnote-page-ai-resizing="true"] {
background: rgba(27, 28, 28, 0.12);
}
.wolai-page-ai-plan-card {
gap: 10px;
border: 1px solid rgba(14, 116, 144, 0.22);
background: #F0F9FF;
color: #0F172A;
}
.wolai-page-ai-plan-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.wolai-page-ai-plan-card-title-wrap {
display: grid;
min-width: 0;
gap: 1px;
}
.wolai-page-ai-plan-card-kicker {
color: #0369A1;
font-size: 11px;
line-height: 16px;
font-weight: 600;
}
.wolai-page-ai-plan-card-title {
color: #0C4A6E;
font-size: 14px;
line-height: 20px;
}
.wolai-page-ai-plan-card-badge {
display: inline-flex;
flex-shrink: 0;
align-items: center;
border-radius: 999px;
padding: 2px 8px;
background: #BAE6FD;
color: #075985;
font-size: 11px;
line-height: 16px;
font-weight: 600;
}
.wolai-page-ai-plan-card-markdown {
display: grid;
gap: 6px;
color: #1E293B;
font-size: 13px;
line-height: 20px;
}
.wolai-page-ai-plan-card-markdown > * {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.wolai-page-ai-plan-card-markdown .wolai-page-ai-markdown-table-wrap {
border: 1px solid rgba(14, 116, 144, 0.16);
border-radius: 8px;
background: rgba(255, 255, 255, 0.72);
}
.wolai-page-ai-plan-step-list {
display: grid;
gap: 6px;
margin: 0;
padding: 0;
list-style: none;
}
.wolai-page-ai-plan-step-item {
display: flex;
align-items: flex-start;
gap: 8px;
min-width: 0;
padding: 8px 10px;
border: 1px solid rgba(255, 255, 255, 0.78);
border-radius: 8px;
background: rgba(255, 255, 255, 0.78);
color: #334155;
font-size: 12px;
line-height: 18px;
}
.wolai-page-ai-plan-step-item[data-status="completed"] {
border-color: rgba(16, 185, 129, 0.28);
background: rgba(236, 253, 245, 0.88);
}
.wolai-page-ai-plan-step-item[data-status="inProgress"] {
border-color: rgba(245, 158, 11, 0.28);
background: rgba(255, 251, 235, 0.9);
}
.wolai-page-ai-plan-step-status {
display: inline-flex;
width: 18px;
height: 18px;
flex: 0 0 18px;
align-items: center;
justify-content: center;
border-radius: 999px;
background: #E2E8F0;
color: #475569;
font-size: 11px;
line-height: 18px;
font-weight: 700;
}
.wolai-page-ai-plan-step-status[data-status="completed"] {
background: #BBF7D0;
color: #166534;
}
.wolai-page-ai-plan-step-status[data-status="inProgress"] {
background: #FDE68A;
color: #92400E;
}
.wolai-page-ai-plan-step-text {
min-width: 0;
overflow-wrap: anywhere;
}
.wolai-page-ai-plan-card-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
}
.wolai-page-ai-plan-card-button {
height: 30px;
padding: 0 10px;
border: 1px solid rgba(14, 116, 144, 0.14);
border-radius: 999px;
background: rgba(255, 255, 255, 0.86);
color: #0F172A;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.wolai-page-ai-plan-card-button--primary {
border-color: #0284C7;
background: #0284C7;
color: #FFFFFF;
}
.wolai-page-ai-plan-card-button--danger {
color: #B33A3A;
}
.wolai-page-ai-permission-card {
gap: 10px;
border: 1px solid rgba(180, 83, 9, 0.18);
background: #FFF7ED;
}
.wolai-page-ai-permission-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.wolai-page-ai-permission-badge {
display: inline-flex;
flex-shrink: 0;
align-items: center;
border-radius: 999px;
padding: 2px 8px;
background: #FED7AA;
color: #9A3412;
font-size: 11px;
line-height: 16px;
font-weight: 600;
}
.wolai-page-ai-permission-summary {
display: grid;
gap: 6px;
}
.wolai-page-ai-permission-row {
display: grid;
grid-template-columns: 54px minmax(0, 1fr);
gap: 8px;
align-items: start;
padding: 7px 8px;
border: 1px solid rgba(180, 83, 9, 0.12);
border-radius: 8px;
background: rgba(255, 255, 255, 0.74);
}
.wolai-page-ai-permission-row span {
color: #9A3412;
font-size: 11px;
line-height: 17px;
font-weight: 600;
}
.wolai-page-ai-permission-row strong {
min-width: 0;
overflow-wrap: anywhere;
color: #1B1C1C;
font-size: 12px;
line-height: 18px;
font-weight: 600;
}
.wolai-page-ai-permission-card .wolai-page-ai-message-actions,
.wolai-page-ai-permission-dialog .wolai-page-ai-message-actions {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.wolai-page-ai-markdown-table-wrap {
max-width: 100%;
overflow-x: auto;
@@ -5802,6 +6063,37 @@ button.wolai-page-ai-history-main span {
border-color: rgba(27, 28, 28, 0.32);
}
.wolai-page-ai-reasonix-controls {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
padding: 0 8px 6px;
}
.wolai-page-ai-reasonix-controls[hidden] {
display: none !important;
}
.wolai-page-ai-reasonix-control {
display: grid;
gap: 2px;
min-width: 0;
color: #8B8782;
font-size: 11px;
}
.wolai-page-ai-reasonix-control select {
width: 100%;
min-width: 0;
height: 28px;
padding: 0 8px;
border: 1px solid rgba(27, 28, 28, 0.1);
border-radius: 7px;
background: #FFF;
color: #1B1C1C;
font-size: 12px;
}
.wolai-page-ai-composer-bar {
display: flex;
min-height: 36px;
@@ -6037,6 +6329,10 @@ mod tests {
assert!(MNOTE_CSS.contains("[data-mnote-tab-badge-kind=\"code\"]"));
assert!(MNOTE_CSS.contains("[data-mnote-tab-badge-kind=\"pdf\"]"));
assert!(MNOTE_CSS.contains("[data-mnote-tab-badge-kind=\"file\"]"));
assert!(MNOTE_CSS.contains(".mnote-main-tab {\n height: 36px;"));
assert!(
MNOTE_CSS.contains(".mnote-main-tab-title {\n min-width: 0;\n line-height: 20px;")
);
assert!(MNOTE_CSS.contains("background: #e74c3c"));
}