2026-06-01 09:29:12 +08:00
|
|
|
export function createSidebarPageAiPermissionRuntime(context) {
|
|
|
|
|
const {
|
|
|
|
|
documentRef,
|
|
|
|
|
pageAiPreviewValue,
|
|
|
|
|
pageUiState,
|
|
|
|
|
renderPageAiConversation,
|
|
|
|
|
} = context;
|
|
|
|
|
const doc = documentRef || document;
|
|
|
|
|
|
2026-06-13 22:20:01 +08:00
|
|
|
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 [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 09:29:12 +08:00
|
|
|
function pageAiPermissionMessage(payload, eventType) {
|
|
|
|
|
payload = payload && typeof payload === 'object' ? payload : {};
|
|
|
|
|
var permissionId = String(payload.permissionId || payload.permission_id || payload.id || ('perm_' + Date.now())).trim();
|
2026-06-13 22:20:01 +08:00
|
|
|
var detail = pageAiPermissionDetail(payload, permissionId);
|
|
|
|
|
var toolName = detail.toolName || String(payload.toolName || payload.tool || payload.name || payload.method || 'session/request_permission').trim();
|
2026-06-01 09:29:12 +08:00
|
|
|
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';
|
2026-06-13 22:20:01 +08:00
|
|
|
var options = pageAiPermissionOptions(payload);
|
2026-06-01 09:29:12 +08:00
|
|
|
return {
|
|
|
|
|
role: 'tool',
|
|
|
|
|
kind: 'permission',
|
|
|
|
|
permissionId: permissionId,
|
2026-06-13 22:20:01 +08:00
|
|
|
runId: String(payload.runId || payload.run_id || pageUiState.pageAiCurrentRunId || '').trim(),
|
2026-06-01 09:29:12 +08:00
|
|
|
toolName: toolName,
|
2026-06-13 22:20:01 +08:00
|
|
|
argsSummary: detail.summary || pageAiPreviewValue(args),
|
2026-06-01 09:29:12 +08:00
|
|
|
content: decision === 'denied' ? '已自动拒绝权限请求' : (decision === 'allowed' ? '已自动允许权限请求' : '等待权限确认'),
|
|
|
|
|
resolved: decision === 'denied' || decision === 'allowed',
|
2026-06-13 22:20:01 +08:00
|
|
|
decision: decision,
|
|
|
|
|
options: options,
|
|
|
|
|
permissionDetails: detail
|
2026-06-01 09:29:12 +08:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 22:20:01 +08:00
|
|
|
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 || '选择';
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 09:29:12 +08:00
|
|
|
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);
|
2026-06-13 22:20:01 +08:00
|
|
|
pageAiPersistPermissionRequests();
|
|
|
|
|
if (!message.resolved && pageAiApplyPermissionMode(message)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-01 09:29:12 +08:00
|
|
|
if (!message.resolved) {
|
|
|
|
|
pageAiShowPermissionDialog(message);
|
|
|
|
|
} else {
|
|
|
|
|
pageAiHidePermissionDialog();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 22:20:01 +08:00
|
|
|
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) {
|
2026-06-01 09:29:12 +08:00
|
|
|
permissionId = String(permissionId || '').trim();
|
2026-06-13 22:20:01 +08:00
|
|
|
decision = String(decision || '').trim() || 'deny';
|
|
|
|
|
optionId = String(optionId || '').trim();
|
2026-06-01 09:29:12 +08:00
|
|
|
if (!permissionId) return;
|
2026-06-13 22:20:01 +08:00
|
|
|
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();
|
|
|
|
|
}
|
2026-06-01 09:29:12 +08:00
|
|
|
if (runId) {
|
2026-06-13 22:20:01 +08:00
|
|
|
var body = { permissionId: permissionId, decision: decision };
|
|
|
|
|
if (optionId) body.optionId = optionId;
|
2026-06-01 09:29:12 +08:00
|
|
|
fetch('/api/hermes/client/runs/' + encodeURIComponent(runId) + '/resolve-permission', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'content-type': 'application/json' },
|
2026-06-13 22:20:01 +08:00
|
|
|
body: JSON.stringify(body)
|
2026-06-01 09:29:12 +08:00
|
|
|
}).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;
|
2026-06-13 22:20:01 +08:00
|
|
|
pageAiPersistPermissionRequests();
|
2026-06-01 09:29:12 +08:00
|
|
|
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>' +
|
2026-06-13 22:20:01 +08:00
|
|
|
'<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>' +
|
2026-06-01 09:29:12 +08:00
|
|
|
'</div>';
|
|
|
|
|
doc.body.appendChild(dialog);
|
|
|
|
|
}
|
|
|
|
|
var tool = dialog.querySelector('[data-page-ai-permission-tool]');
|
2026-06-13 22:20:01 +08:00
|
|
|
if (tool instanceof HTMLElement) tool.textContent = "权限 - " + (message.toolName || "session/request_permission");
|
2026-06-01 09:29:12 +08:00
|
|
|
var args = dialog.querySelector('[data-page-ai-permission-args]');
|
2026-06-13 22:20:01 +08:00
|
|
|
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);
|
2026-06-01 09:29:12 +08:00
|
|
|
dialog.hidden = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 22:20:01 +08:00
|
|
|
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, '&')
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
|
.replace(/"/g, '"')
|
|
|
|
|
.replace(/'/g, ''');
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 09:29:12 +08:00
|
|
|
return {
|
|
|
|
|
pageAiPermissionMessage,
|
|
|
|
|
pageAiApplyPermissionEvent,
|
|
|
|
|
pageAiResolvePermission,
|
2026-06-13 22:20:01 +08:00
|
|
|
pageAiRestorePermissionRequests,
|
2026-06-01 09:29:12 +08:00
|
|
|
pageAiHidePermissionDialog,
|
|
|
|
|
pageAiShowPermissionDialog
|
|
|
|
|
};
|
|
|
|
|
}
|