Files
mnote/rust/crates/mnote-web/browser/sidebar-page-ai-permission-runtime.js
T
lix-2026 4a7efd4a30 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
2026-06-13 22:20:01 +08:00

345 lines
16 KiB
JavaScript

export function createSidebarPageAiPermissionRuntime(context) {
const {
documentRef,
pageAiPreviewValue,
pageUiState,
renderPageAiConversation,
} = 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 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: detail.summary || pageAiPreviewValue(args),
content: decision === 'denied' ? '已自动拒绝权限请求' : (decision === 'allowed' ? '已自动允许权限请求' : '等待权限确认'),
resolved: decision === 'denied' || decision === 'allowed',
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 {
payload = JSON.parse(payloadText || 'null');
} catch (_) {
payload = {};
}
var message = pageAiPermissionMessage(payload, eventName);
var existing = pageUiState.pageAiMessages.find(function(item) {
return item.kind === 'permission' && item.permissionId === message.permissionId;
});
if (existing) {
Object.assign(existing, message);
} else {
pageUiState.pageAiMessages.push(message);
}
pageUiState.pageAiPermissionRequests = pageUiState.pageAiPermissionRequests.filter(function(item) {
return item.permissionId !== message.permissionId;
}).concat([message]).slice(-20);
pageAiPersistPermissionRequests();
if (!message.resolved && pageAiApplyPermissionMode(message)) {
return;
}
if (!message.resolved) {
pageAiShowPermissionDialog(message);
} else {
pageAiHidePermissionDialog();
}
}
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 = 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(body)
}).then(function(response) {
if (!response.ok) console.warn('resolve-permission 后端返回异常', response.status);
}).catch(function(err) {
console.warn('resolve-permission 请求失败', err);
});
} else {
console.warn('resolve-permission: 无活跃 runId,只能本地更新');
}
pageUiState.pageAiMessages.forEach(function(item) {
if (item.kind === 'permission' && item.permissionId === permissionId) {
item.resolved = true;
item.decision = decision;
item.content = decision === 'allow' ? '已允许权限请求' : '已拒绝权限请求';
}
});
var dialog = doc.querySelector('[data-page-ai-permission-dialog]');
if (dialog instanceof HTMLElement) dialog.hidden = true;
pageAiPersistPermissionRequests();
renderPageAiConversation();
}
function pageAiHidePermissionDialog() {
var dialog = doc.querySelector('[data-page-ai-permission-dialog]');
if (dialog instanceof HTMLElement) dialog.hidden = true;
}
function pageAiShowPermissionDialog(message) {
if (!message || message.kind !== 'permission') return;
if (message.resolved) {
pageAiHidePermissionDialog();
return;
}
var dialog = doc.querySelector('[data-page-ai-permission-dialog]');
if (!(dialog instanceof HTMLElement)) {
dialog = doc.createElement('div');
dialog.className = 'wolai-page-ai-permission-dialog';
dialog.setAttribute('data-page-ai-permission-dialog', 'true');
dialog.innerHTML = '' +
'<div class="wolai-page-ai-permission-panel" role="dialog" aria-modal="false">' +
'<div class="wolai-page-ai-memory-title" data-page-ai-permission-tool></div>' +
'<div class="wolai-page-ai-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");
var args = dialog.querySelector('[data-page-ai-permission-args]');
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
};
}