feat: purge legacy agent hosts and land vault Chrome extension path
Retire ACP/Hermes/OpenCode surfaces and rename hermes_tools to mnote_agent_tools so Page AI stays on Pi Lab only. Add Chrome vault extension + extension token route, pre-release purge design, and soft-retire legacy smokes for the small-group production cut.
This commit is contained in:
@@ -1,127 +0,0 @@
|
||||
// AgentStreamEventRouter — 统一的 agent 流式事件状态机
|
||||
// 供 Page AI sidebar runtime 和 document editor adapter runtime 共用
|
||||
//
|
||||
// 事件状态:
|
||||
// init — 建立 request_id 绑定
|
||||
// loading — message_delta / tool_call_delta → 增量更新
|
||||
// stream_event — tool-started / tool-finished / agent_state
|
||||
// finished — 正常终止
|
||||
// interrupted — 中断(等待审批/恢复)
|
||||
// error — 错误终止
|
||||
// approval_required — 等待用户确认
|
||||
|
||||
export const AGENT_EVENT_STATES = {
|
||||
INIT: 'init',
|
||||
LOADING: 'loading',
|
||||
STREAM_EVENT: 'stream_event',
|
||||
FINISHED: 'finished',
|
||||
INTERRUPTED: 'interrupted',
|
||||
ERROR: 'error',
|
||||
APPROVAL_REQUIRED: 'approval_required',
|
||||
};
|
||||
|
||||
export const TERMINAL_STATES = new Set([
|
||||
AGENT_EVENT_STATES.FINISHED,
|
||||
AGENT_EVENT_STATES.INTERRUPTED,
|
||||
AGENT_EVENT_STATES.ERROR,
|
||||
]);
|
||||
|
||||
export function isTerminalState(status) {
|
||||
return TERMINAL_STATES.has(status);
|
||||
}
|
||||
|
||||
// 解析 SSE 帧为 eventName + payloadText
|
||||
export function parseSSEFrames(buffer, lastBoundary) {
|
||||
var frames = buffer.split('\n\n');
|
||||
var remaining = frames.pop() || '';
|
||||
var events = [];
|
||||
frames.forEach(function(frame) {
|
||||
var eventName = '';
|
||||
var dataLines = [];
|
||||
frame.split('\n').forEach(function(line) {
|
||||
if (line.startsWith('event:')) eventName = line.slice(6).trim();
|
||||
if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
|
||||
});
|
||||
var payloadText = dataLines.join('\n');
|
||||
if (!eventName && payloadText) {
|
||||
try {
|
||||
var parsed = JSON.parse(payloadText);
|
||||
eventName = parsed && parsed.event ? String(parsed.event) : '';
|
||||
} catch (_) {}
|
||||
}
|
||||
if (eventName) events.push({ eventName: eventName, payloadText: payloadText });
|
||||
});
|
||||
return { events: events, remaining: remaining };
|
||||
}
|
||||
|
||||
// 从 SSE 流读取事件
|
||||
export async function readSSEStream(response, onFrame) {
|
||||
if (!response.body || typeof response.body.getReader !== 'function') return;
|
||||
var reader = response.body.getReader();
|
||||
var decoder = new TextDecoder();
|
||||
var buffer = '';
|
||||
while (true) {
|
||||
var chunk = await reader.read();
|
||||
if (chunk.done) break;
|
||||
buffer += decoder.decode(chunk.value, { stream: true });
|
||||
var result = parseSSEFrames(buffer, '');
|
||||
buffer = result.remaining;
|
||||
result.events.forEach(function(evt) {
|
||||
onFrame(evt.eventName, evt.payloadText);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建标准事件路由器
|
||||
// handlers: { onInit, onDelta, onToolCall, onToolResult, onAgentState, onTerminal, onApprovalRequired }
|
||||
export function createAgentStreamRouter(handlers) {
|
||||
var h = handlers || {};
|
||||
|
||||
return function routeEvent(eventName, payloadText) {
|
||||
var payload = null;
|
||||
try { payload = JSON.parse(payloadText || 'null'); } catch (_) {}
|
||||
|
||||
var status = (payload && payload.status) || eventName;
|
||||
|
||||
switch (status) {
|
||||
case AGENT_EVENT_STATES.INIT:
|
||||
if (h.onInit) h.onInit(payload);
|
||||
break;
|
||||
|
||||
case AGENT_EVENT_STATES.LOADING:
|
||||
if (h.onDelta) h.onDelta(payload);
|
||||
if (h.onToolCall) {
|
||||
var toolChunks = (payload && payload.tool_call_chunks) || (payload && payload.msg && payload.msg.tool_call_chunks);
|
||||
if (toolChunks && toolChunks.length) h.onToolCall(payload);
|
||||
}
|
||||
break;
|
||||
|
||||
case AGENT_EVENT_STATES.STREAM_EVENT:
|
||||
if (payload && payload.event === 'tool-finished') {
|
||||
if (h.onToolResult) h.onToolResult(payload);
|
||||
} else if (payload && payload.event === 'tool-started') {
|
||||
// tool start — 可选处理
|
||||
} else if (payload && payload.agent_state) {
|
||||
if (h.onAgentState) h.onAgentState(payload.agent_state);
|
||||
}
|
||||
break;
|
||||
|
||||
case AGENT_EVENT_STATES.FINISHED:
|
||||
case AGENT_EVENT_STATES.INTERRUPTED:
|
||||
case AGENT_EVENT_STATES.ERROR:
|
||||
if (h.onTerminal) h.onTerminal(status, payload);
|
||||
break;
|
||||
|
||||
case AGENT_EVENT_STATES.APPROVAL_REQUIRED:
|
||||
if (h.onApprovalRequired) h.onApprovalRequired(payload);
|
||||
break;
|
||||
|
||||
default:
|
||||
// 未知状态:尝试作为 loading 处理
|
||||
if (payload && (payload.text || payload.delta || payload.content)) {
|
||||
if (h.onDelta) h.onDelta(payload);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -621,7 +621,9 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
|
||||
const conflictSourceLabel = (session) => {
|
||||
if (!session) return '';
|
||||
if (session.lastExternalWriteSource === 'mnote-hermes-tool') {
|
||||
const source = String(session.lastExternalWriteSource || '');
|
||||
// 兼容历史 externalActor 名;产品面统一称 agent tool
|
||||
if (source === 'mnote-agent-tool' || source === 'mnote-hermes-tool') {
|
||||
const runId = String(session.lastExternalWriteRunId || '').trim();
|
||||
return runId ? `agent run ${runId}` : 'agent run';
|
||||
}
|
||||
@@ -1820,7 +1822,7 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
window.addEventListener('tree:delta', handleTreeExternalChange);
|
||||
window.addEventListener('tree:resync', handleTreeExternalChange);
|
||||
window.addEventListener('mnote:page-ai-tool-write-completed', (event) => {
|
||||
refreshDocumentSessionsFromExternalWrite(event?.detail || {}, 'mnote-hermes-tool');
|
||||
refreshDocumentSessionsFromExternalWrite(event?.detail || {}, 'mnote-agent-tool');
|
||||
});
|
||||
window.addEventListener('mnote:local-upload-editor-save-completed', (event) => {
|
||||
applyLocalUploadEditorSave(event?.detail || {});
|
||||
|
||||
@@ -1,359 +0,0 @@
|
||||
export function createSidebarPageAiMarkdownRuntime(context) {
|
||||
const { escapeHtml } = context;
|
||||
|
||||
function textFromUnknown(value) {
|
||||
if (value == null) return '';
|
||||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value);
|
||||
if (Array.isArray(value)) return value.map(textFromUnknown).filter(Boolean).join(' ');
|
||||
if (typeof value !== 'object') return '';
|
||||
var parts = [];
|
||||
['text', 'title', 'content', 'children', 'blocks', 'body'].forEach(function(key) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
||||
var text = textFromUnknown(value[key]);
|
||||
if (text) parts.push(text);
|
||||
}
|
||||
});
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
function renderPageAiMarkdownInline(text) {
|
||||
var html = escapeHtml(String(text || ''));
|
||||
var codeSpans = [];
|
||||
var htmlSpans = [];
|
||||
function stashHtml(value) {
|
||||
var key = '\u0000HTML' + htmlSpans.length + '\u0000';
|
||||
htmlSpans.push(value);
|
||||
return key;
|
||||
}
|
||||
html = html.replace(/`([^`\n]+)`/g, function(_, code) {
|
||||
var key = '\u0000CODE' + codeSpans.length + '\u0000';
|
||||
codeSpans.push('<code>' + code + '</code>');
|
||||
return key;
|
||||
});
|
||||
html = html.replace(/\[((?:\\.|[^\]\n])+)\]\(([^)\n]+)\)/g, function(match, label, href) {
|
||||
var normalizedHref = normalizePageAiMarkdownHref(href);
|
||||
if (!normalizedHref) return match;
|
||||
var citationAttr = isMnoteCitationHref(normalizedHref) ? ' data-page-ai-citation-link="true"' : '';
|
||||
return stashHtml('<a href="' + escapeHtml(normalizedHref) + '" target="_blank" rel="noopener noreferrer"' + citationAttr + '>' + unescapePageAiMarkdownLabel(label) + '</a>');
|
||||
});
|
||||
html = html.replace(/(^|[\s(])((?:https?:\/\/|\/documents\/|mnote:\/\/open(?:Resource)?)[^\s<>()]+[^\s<>().,;:!?])/g, function(_, prefix, href) {
|
||||
var normalizedHref = normalizePageAiMarkdownHref(href);
|
||||
if (!normalizedHref) return prefix + href;
|
||||
var citationAttr = isMnoteCitationHref(normalizedHref) ? ' data-page-ai-citation-link="true"' : '';
|
||||
return prefix + stashHtml('<a href="' + escapeHtml(normalizedHref) + '" target="_blank" rel="noopener noreferrer"' + citationAttr + '>' + escapeHtml(normalizedHref) + '</a>');
|
||||
});
|
||||
html = html.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>');
|
||||
html = html.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>');
|
||||
codeSpans.forEach(function(value, index) {
|
||||
html = html.replace('\u0000CODE' + index + '\u0000', value);
|
||||
});
|
||||
htmlSpans.forEach(function(value, index) {
|
||||
html = html.replace('\u0000HTML' + index + '\u0000', value);
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
function normalizePageAiMarkdownHref(value) {
|
||||
var href = String(value || '').trim()
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
while (href.includes('&')) href = href.replace(/&/g, '&');
|
||||
if (href.startsWith('<') && href.endsWith('>')) href = href.slice(1, -1).trim();
|
||||
if (!href || /[\u0000-\u001f<>"']/.test(href)) return '';
|
||||
var lower = href.toLowerCase();
|
||||
if (lower.startsWith('javascript:') || lower.startsWith('data:') || lower.startsWith('vbscript:')) return '';
|
||||
if (lower.startsWith('mnote://open')) href = normalizePageAiMnoteOpenHref(href);
|
||||
href = normalizePageAiLegacyCitationHref(href);
|
||||
if (href.startsWith('/') || href.startsWith('#')) return href;
|
||||
if (/^https?:\/\//i.test(href) || /^mailto:/i.test(href)) return href;
|
||||
return '';
|
||||
}
|
||||
|
||||
function unescapePageAiMarkdownLabel(value) {
|
||||
return String(value || '').replace(/\\([\\\[\]()*_`])/g, '$1');
|
||||
}
|
||||
|
||||
function normalizePageAiMnoteOpenHref(href) {
|
||||
try {
|
||||
var url = new URL(href);
|
||||
if (url.protocol !== 'mnote:' || (url.hostname !== 'open' && url.hostname !== 'openResource')) return href;
|
||||
var path = String(url.searchParams.get('path') || url.searchParams.get('resourcePath') || '').trim();
|
||||
if (!path) return '';
|
||||
var params = new URLSearchParams();
|
||||
var rootUri = String(url.searchParams.get('rootUri') || '').trim();
|
||||
if (!rootUri) {
|
||||
try {
|
||||
rootUri = new URL(window.location.href).searchParams.get('rootUri') || '';
|
||||
} catch (_locationError) {}
|
||||
}
|
||||
if (rootUri) params.set('rootUri', rootUri);
|
||||
params.set('path', path);
|
||||
['page', 'bbox', 'sourceMapPath', 'blockId', 'evidenceText', 'lineRange', 'charRange'].forEach(function(key) {
|
||||
var value = String(url.searchParams.get(key) || '').trim();
|
||||
if (value) params.set(key, value);
|
||||
});
|
||||
return '/api/local-folder/files/open?' + params.toString();
|
||||
} catch (_error) {
|
||||
return href;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePageAiLegacyCitationHref(href) {
|
||||
try {
|
||||
var url = new URL(href, window.location.origin);
|
||||
var unwrappedHref = normalizePageAiSearchWrappedCitationHref(url);
|
||||
if (unwrappedHref) return normalizePageAiLegacyCitationHref(unwrappedHref);
|
||||
if (!isPageAiSameMnoteOrigin(url) && !isPageAiPortableMnoteCitationUrl(url)) return href;
|
||||
if (url.pathname === '/api/local-folder/files/open') return url.pathname + url.search;
|
||||
if (!url.pathname.startsWith('/documents/')) return href;
|
||||
if (isPageAiUnsafeCitationDocumentId(url)) {
|
||||
var fileOpenHref = normalizePageAiCitationFileOpenHref(url);
|
||||
if (fileOpenHref) return fileOpenHref;
|
||||
}
|
||||
var decodedHash = '';
|
||||
try {
|
||||
decodedHash = decodeURIComponent(url.hash || '');
|
||||
} catch (_decodeError) {
|
||||
decodedHash = url.hash || '';
|
||||
}
|
||||
var marker = '#resource-tab-';
|
||||
var markerIndex = decodedHash.indexOf(marker);
|
||||
if (markerIndex < 0 || url.searchParams.get('resourceTab')) {
|
||||
return url.pathname + url.search + url.hash;
|
||||
}
|
||||
var identity = decodedHash.slice(markerIndex + marker.length).replace(/^#+/, '').trim();
|
||||
if (!identity.startsWith('resource:file:')) return url.pathname + url.search + url.hash;
|
||||
url.searchParams.set('resourceTab', identity);
|
||||
if (!url.searchParams.get('sourceKind')) url.searchParams.set('sourceKind', 'local_folder');
|
||||
var rootUri = String(url.searchParams.get('rootUri') || '').trim();
|
||||
var prefix = 'resource:file:' + rootUri + ':';
|
||||
if (rootUri && identity.startsWith(prefix) && !url.searchParams.get('resourcePath')) {
|
||||
url.searchParams.set('resourcePath', identity.slice(prefix.length).replace(/^\/+/, ''));
|
||||
}
|
||||
url.hash = '';
|
||||
return url.pathname + url.search;
|
||||
} catch (_error) {
|
||||
return href;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePageAiSearchWrappedCitationHref(url) {
|
||||
var raw = '';
|
||||
['wd', 'q', 'query'].some(function(key) {
|
||||
raw = String(url.searchParams.get(key) || '').trim();
|
||||
return Boolean(raw);
|
||||
});
|
||||
if (!raw) return '';
|
||||
if (/^documents\//i.test(raw)) raw = '/' + raw;
|
||||
if (!/^\/documents\//i.test(raw) && !/^https?:\/\/[^/]+\/documents\//i.test(raw) && !/^mnote:\/\/open/i.test(raw)) return '';
|
||||
if (/^mnote:\/\/open/i.test(raw)) return normalizePageAiMnoteOpenHref(raw);
|
||||
var nested = new URL(raw, window.location.origin);
|
||||
if (!nested.pathname.startsWith('/documents/')) return '';
|
||||
['sourceKind', 'rootUri', 'workspaceId', 'resourceTab', 'resourcePath', 'page', 'bbox', 'sourceMapPath', 'blockId', 'evidenceText', 'lineRange', 'charRange'].forEach(function(key) {
|
||||
if (nested.searchParams.get(key)) return;
|
||||
var value = String(url.searchParams.get(key) || '').trim();
|
||||
if (value) nested.searchParams.set(key, value);
|
||||
});
|
||||
return nested.pathname + nested.search;
|
||||
}
|
||||
|
||||
function normalizePageAiCitationFileOpenHref(url) {
|
||||
var rootUri = String(url.searchParams.get('rootUri') || '').trim();
|
||||
var path = String(url.searchParams.get('resourcePath') || '').trim();
|
||||
if (!rootUri || !path) return '';
|
||||
var params = new URLSearchParams();
|
||||
params.set('rootUri', rootUri);
|
||||
params.set('path', path);
|
||||
['page', 'bbox', 'sourceMapPath', 'blockId', 'evidenceText', 'lineRange', 'charRange'].forEach(function(key) {
|
||||
var value = String(url.searchParams.get(key) || '').trim();
|
||||
if (value) params.set(key, value);
|
||||
});
|
||||
return '/api/local-folder/files/open?' + params.toString();
|
||||
}
|
||||
|
||||
function isPageAiUnsafeCitationDocumentId(url) {
|
||||
try {
|
||||
var raw = String(url.pathname || '').replace(/^\/documents\//, '').split('/')[0] || '';
|
||||
if (!raw) return false;
|
||||
var decoded = decodeURIComponent(raw);
|
||||
return decoded.indexOf('%') >= 0 || decoded.indexOf('\uFFFD') >= 0;
|
||||
} catch (_error) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function isPageAiPortableMnoteCitationUrl(url) {
|
||||
try {
|
||||
if (url.pathname === '/api/local-folder/files/open') {
|
||||
return Boolean(String(url.searchParams.get('rootUri') || '').trim()) &&
|
||||
Boolean(String(url.searchParams.get('path') || '').trim());
|
||||
}
|
||||
if (!url.pathname.startsWith('/documents/')) return false;
|
||||
var resourceTab = String(url.searchParams.get('resourceTab') || '').trim();
|
||||
return String(url.searchParams.get('sourceKind') || '').trim() === 'local_folder' ||
|
||||
Boolean(String(url.searchParams.get('rootUri') || '').trim()) ||
|
||||
Boolean(String(url.searchParams.get('resourcePath') || '').trim()) ||
|
||||
resourceTab.startsWith('resource:file:');
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isPageAiMarkdownTableDivider(line) {
|
||||
var cells = splitPageAiMarkdownTableRow(line);
|
||||
if (cells.length < 2) return false;
|
||||
return cells.every(function(cell) {
|
||||
return /^:?-{3,}:?$/.test(cell.trim());
|
||||
});
|
||||
}
|
||||
|
||||
function splitPageAiMarkdownTableRow(line) {
|
||||
var text = String(line || '').trim();
|
||||
if (!text.includes('|')) return [];
|
||||
if (text.startsWith('|')) text = text.slice(1);
|
||||
if (text.endsWith('|')) text = text.slice(0, -1);
|
||||
return text.split('|').map(function(cell) { return cell.trim(); });
|
||||
}
|
||||
|
||||
function renderPageAiMarkdownTable(lines, startIndex) {
|
||||
if (startIndex + 1 >= lines.length || !isPageAiMarkdownTableDivider(lines[startIndex + 1])) return null;
|
||||
var header = splitPageAiMarkdownTableRow(lines[startIndex]);
|
||||
var divider = splitPageAiMarkdownTableRow(lines[startIndex + 1]);
|
||||
if (!header.length || header.length !== divider.length) return null;
|
||||
var rows = [];
|
||||
var index = startIndex + 2;
|
||||
while (index < lines.length && lines[index].trim() && lines[index].includes('|')) {
|
||||
var cells = splitPageAiMarkdownTableRow(lines[index]);
|
||||
if (!cells.length) break;
|
||||
rows.push(cells);
|
||||
index += 1;
|
||||
}
|
||||
function cellHtml(tag, value) {
|
||||
return '<' + tag + '>' + renderPageAiMarkdownInline(value) + '</' + tag + '>';
|
||||
}
|
||||
var head = '<thead><tr>' + header.map(function(cell) { return cellHtml('th', cell); }).join('') + '</tr></thead>';
|
||||
var body = rows.length
|
||||
? '<tbody>' + rows.map(function(row) {
|
||||
return '<tr>' + header.map(function(_, cellIndex) { return cellHtml('td', row[cellIndex] || ''); }).join('') + '</tr>';
|
||||
}).join('') + '</tbody>'
|
||||
: '';
|
||||
return {
|
||||
html: '<div class="wolai-page-ai-markdown-table-wrap"><table>' + head + body + '</table></div>',
|
||||
nextIndex: index
|
||||
};
|
||||
}
|
||||
|
||||
function isMnoteCitationHref(href) {
|
||||
try {
|
||||
var url = new URL(href, window.location.origin);
|
||||
if (!isPageAiSameMnoteOrigin(url)) return false;
|
||||
return url.pathname.startsWith('/documents/') || url.pathname === '/api/local-folder/files/open';
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isPageAiSameMnoteOrigin(url) {
|
||||
try {
|
||||
var current = new URL(window.location.origin);
|
||||
if (url.origin === current.origin) return true;
|
||||
if (url.hostname === 'mnote.local') return true;
|
||||
var localNames = ['localhost', '127.0.0.1', '::1', 'mnote.local'];
|
||||
return localNames.indexOf(url.hostname) >= 0 &&
|
||||
localNames.indexOf(current.hostname) >= 0 &&
|
||||
String(url.port || defaultPortForProtocol(url.protocol)) === String(current.port || defaultPortForProtocol(current.protocol));
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function defaultPortForProtocol(protocol) {
|
||||
return protocol === 'https:' ? '443' : protocol === 'http:' ? '80' : '';
|
||||
}
|
||||
|
||||
function renderPageAiMarkdown(content) {
|
||||
var lines = String(content || '').replace(/\r\n?/g, '\n').split('\n');
|
||||
var blocks = [];
|
||||
var index = 0;
|
||||
function isBlockBoundary(line) {
|
||||
return !line.trim() ||
|
||||
/^```/.test(line.trim()) ||
|
||||
/^#{1,6}\s+/.test(line) ||
|
||||
/^\s*[-*]\s+/.test(line) ||
|
||||
/^\s*\d+[.)]\s+/.test(line);
|
||||
}
|
||||
while (index < lines.length) {
|
||||
var line = lines[index];
|
||||
if (!line.trim()) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (/^```/.test(line.trim())) {
|
||||
index += 1;
|
||||
var codeLines = [];
|
||||
while (index < lines.length && !/^```/.test(lines[index].trim())) {
|
||||
codeLines.push(lines[index]);
|
||||
index += 1;
|
||||
}
|
||||
if (index < lines.length) index += 1;
|
||||
blocks.push('<pre><code>' + escapeHtml(codeLines.join('\n')) + '</code></pre>');
|
||||
continue;
|
||||
}
|
||||
if (/^\s*-{3,}\s*$/.test(line)) {
|
||||
blocks.push('<hr />');
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
var table = renderPageAiMarkdownTable(lines, index);
|
||||
if (table) {
|
||||
blocks.push(table.html);
|
||||
index = table.nextIndex;
|
||||
continue;
|
||||
}
|
||||
var heading = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
if (heading) {
|
||||
var level = Math.min(6, heading[1].length);
|
||||
blocks.push('<h' + level + '>' + renderPageAiMarkdownInline(heading[2]) + '</h' + level + '>');
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (/^\s*[-*]\s+/.test(line)) {
|
||||
var unordered = [];
|
||||
while (index < lines.length && /^\s*[-*]\s+/.test(lines[index])) {
|
||||
unordered.push('<li>' + renderPageAiMarkdownInline(lines[index].replace(/^\s*[-*]\s+/, '')) + '</li>');
|
||||
index += 1;
|
||||
}
|
||||
blocks.push('<ul>' + unordered.join('') + '</ul>');
|
||||
continue;
|
||||
}
|
||||
if (/^\s*\d+[.)]\s+/.test(line)) {
|
||||
var ordered = [];
|
||||
while (index < lines.length && /^\s*\d+[.)]\s+/.test(lines[index])) {
|
||||
ordered.push('<li>' + renderPageAiMarkdownInline(lines[index].replace(/^\s*\d+[.)]\s+/, '')) + '</li>');
|
||||
index += 1;
|
||||
}
|
||||
blocks.push('<ol>' + ordered.join('') + '</ol>');
|
||||
continue;
|
||||
}
|
||||
var paragraph = [];
|
||||
while (index < lines.length && !isBlockBoundary(lines[index])) {
|
||||
paragraph.push(renderPageAiMarkdownInline(lines[index]));
|
||||
index += 1;
|
||||
}
|
||||
if (paragraph.length) {
|
||||
blocks.push('<p>' + paragraph.join('<br />') + '</p>');
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
return blocks.join('') || escapeHtml(String(content || ''));
|
||||
}
|
||||
|
||||
return {
|
||||
textFromUnknown,
|
||||
renderPageAiMarkdown,
|
||||
renderPageAiMarkdownInline
|
||||
};
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
return {
|
||||
pageAiPermissionMessage,
|
||||
pageAiApplyPermissionEvent,
|
||||
pageAiResolvePermission,
|
||||
pageAiRestorePermissionRequests,
|
||||
pageAiHidePermissionDialog,
|
||||
pageAiShowPermissionDialog
|
||||
};
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
export function createSidebarPageAiProfileRuntime(context) {
|
||||
const {
|
||||
chatOnlyProfileRegistry,
|
||||
documentRef,
|
||||
pageAiAgentRecord,
|
||||
pageAiCurrentAgentId,
|
||||
pageAiNormalizeAgentId,
|
||||
pageUiState,
|
||||
} = context;
|
||||
const PAGE_AI_CHAT_ONLY_PROFILE_REGISTRY = Array.isArray(chatOnlyProfileRegistry) ? chatOnlyProfileRegistry : [];
|
||||
|
||||
function pageAiProviderLabel(provider) {
|
||||
if (provider === 'codex') return 'Codex';
|
||||
if (provider === 'claudecode') return 'ClaudeCode';
|
||||
return 'Hermes';
|
||||
}
|
||||
|
||||
function pageAiNormalizeArray(value) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function pageAiDefaultAcpRuntimes() {
|
||||
return [
|
||||
{
|
||||
name: 'reasonix',
|
||||
title: 'ACP · Reasonix',
|
||||
description: '通过 ACP 协议直连 Reasonix(DeepSeek 缓存优先)',
|
||||
model: 'deepseek-chat',
|
||||
preset: 'auto'
|
||||
},
|
||||
{
|
||||
name: 'hermes',
|
||||
title: 'ACP · Hermes',
|
||||
description: '通过 ACP 协议直连 Hermes agent runtime'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function pageAiNormalizeAcpRuntimes(runtimes) {
|
||||
var byName = {};
|
||||
pageAiDefaultAcpRuntimes().forEach(function(runtime) {
|
||||
byName[runtime.name] = Object.assign({}, runtime);
|
||||
});
|
||||
pageAiNormalizeArray(runtimes).forEach(function(runtime) {
|
||||
var name = String(runtime && runtime.name || '').trim();
|
||||
if (name !== 'reasonix' && name !== 'hermes') return;
|
||||
byName[name] = Object.assign({}, byName[name] || {}, runtime, { name: name });
|
||||
});
|
||||
return ['reasonix', 'hermes'].map(function(name) { return byName[name]; }).filter(Boolean);
|
||||
}
|
||||
|
||||
function pageAiUnwrapUpstream(payload) {
|
||||
if (payload && typeof payload === 'object' && payload.upstream) return payload.upstream;
|
||||
return payload || null;
|
||||
}
|
||||
|
||||
function pageAiProfileValue(profile) {
|
||||
if (profile && typeof profile === 'object') {
|
||||
return String(profile.profileId || profile.name || profile.profile || profile.id || '').trim();
|
||||
}
|
||||
return String(profile || '').trim();
|
||||
}
|
||||
|
||||
function pageAiCurrentProfile() {
|
||||
var active = String(pageUiState.pageAiActiveProfileName || '').trim();
|
||||
if (active) return active;
|
||||
var selected = pageUiState.pageAiProfiles.find(function(profile) {
|
||||
return profile && profile.active;
|
||||
});
|
||||
return pageAiProfileValue(selected) || 'mnoteai';
|
||||
}
|
||||
|
||||
function pageAiRunProfile() {
|
||||
if (String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix') return 'reasonix';
|
||||
if (pageAiCurrentAgentId() === 'chat_only') return pageAiNormalizeChatOnlyProfileId(pageAiCurrentProfile());
|
||||
return pageAiCurrentProfile();
|
||||
}
|
||||
|
||||
function pageAiMnoteToolModel() {
|
||||
var doc = documentRef || document;
|
||||
return String(doc.documentElement.getAttribute('data-mnote-page-ai-tool-model') || 'deepseek-v4-flash').trim() || 'deepseek-v4-flash';
|
||||
}
|
||||
|
||||
function pageAiCurrentProfileRecord() {
|
||||
var active = pageAiCurrentProfile();
|
||||
return pageUiState.pageAiProfiles.find(function(profile) {
|
||||
return pageAiProfileValue(profile) === active;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function pageAiChatOnlyProfileSpec(profile) {
|
||||
var profileId = pageAiProfileValue(profile);
|
||||
var baseProfile = String(profile && profile.baseProfile || '').trim();
|
||||
var isolatedProfile = String(profile && profile.isolatedProfile || '').trim();
|
||||
return PAGE_AI_CHAT_ONLY_PROFILE_REGISTRY.find(function(spec) {
|
||||
return spec.profileId === profileId || spec.baseProfile === profileId || spec.baseProfile === baseProfile || isolatedProfile.indexOf(spec.baseProfile) >= 0;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function pageAiDefaultChatOnlyProfileSpec() {
|
||||
return PAGE_AI_CHAT_ONLY_PROFILE_REGISTRY[0] || { profileId: 'shared_deepseek_chat', baseProfile: 'deepseek-chat', label: 'DeepSeek' };
|
||||
}
|
||||
|
||||
function pageAiNormalizeChatOnlyProfileId(profileId) {
|
||||
var profile = pageAiProfileRecordById(profileId) || { profileId: String(profileId || '').trim(), name: String(profileId || '').trim() };
|
||||
var spec = pageAiChatOnlyProfileSpec(profile);
|
||||
return (spec || pageAiDefaultChatOnlyProfileSpec()).profileId;
|
||||
}
|
||||
|
||||
function pageAiProfileDisplayLabel(profile, fallback) {
|
||||
var alias = String(profile && profile.alias || '').trim();
|
||||
var displayName = String(profile && (profile.displayName || profile.label || '') || '').trim();
|
||||
var name = pageAiProfileValue(profile);
|
||||
return alias || displayName || fallback || name || 'default';
|
||||
}
|
||||
|
||||
function pageAiProfileRecordById(profileId) {
|
||||
var normalized = String(profileId || '').trim();
|
||||
if (!normalized) return null;
|
||||
return pageAiNormalizeArray(pageUiState.pageAiProfiles).find(function(profile) {
|
||||
return pageAiProfileValue(profile) === normalized;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function pageAiSessionAgentFilterValue(session) {
|
||||
if (String(session && session.source || '') === 'board' || session && session.workerPresetId) return 'board:mnote-page-ai';
|
||||
var agentId = pageAiNormalizeAgentId(session && session.agentId);
|
||||
if (agentId === 'reasonix') return 'reasonix';
|
||||
var profileId = String(session && (session.profileId || session.profile) || '').trim();
|
||||
if (agentId === 'chat_only') profileId = pageAiNormalizeChatOnlyProfileId(profileId);
|
||||
return agentId + ':' + profileId;
|
||||
}
|
||||
|
||||
function pageAiSessionAgentLabel(session) {
|
||||
if (String(session && session.source || '') === 'board' || session && session.workerPresetId) {
|
||||
var worker = String(session && session.workerPresetId || 'mnote-page-ai-zcode').trim();
|
||||
var model = String(session && session.modelOverride || '').trim();
|
||||
return 'Agent Board / ' + worker + (model ? ' / ' + model : '');
|
||||
}
|
||||
var agentId = pageAiNormalizeAgentId(session && session.agentId);
|
||||
if (agentId === 'reasonix') return pageAiAgentRecord(agentId).label || 'Reasonix';
|
||||
var profileId = String(session && (session.profileId || session.profile) || '').trim();
|
||||
var profile = pageAiProfileRecordById(profileId) || { profileId: profileId, name: profileId };
|
||||
var chatOnlySpec = pageAiChatOnlyProfileSpec(profile);
|
||||
if (agentId === 'chat_only') {
|
||||
return 'ChatOnly / ' + (chatOnlySpec || pageAiDefaultChatOnlyProfileSpec()).label;
|
||||
}
|
||||
if (agentId === 'hermes') return 'Hermes / ' + pageAiProfileDisplayLabel(profile, profileId);
|
||||
return pageAiAgentRecord(agentId).label;
|
||||
}
|
||||
|
||||
function pageAiSessionPreviewText(session) {
|
||||
var preview = Array.isArray(session && session.messages) && session.messages.length
|
||||
? String(session.messages.slice(-1)[0].content || '')
|
||||
: String(session && (session.snippet || session.preview || '暂无消息') || '暂无消息');
|
||||
preview = preview.replace(/\s+/g, ' ').trim();
|
||||
var limit = 96;
|
||||
return preview.length > limit ? preview.slice(0, limit) + '…' : preview;
|
||||
}
|
||||
|
||||
function pageAiSessionAgentFilterOptions(rows) {
|
||||
var byValue = { all: '全部 agent' };
|
||||
pageAiNormalizeArray(rows).filter(pageAiVisibleHistorySession).forEach(function(session) {
|
||||
var value = pageAiSessionAgentFilterValue(session);
|
||||
byValue[value] = pageAiSessionAgentLabel(session);
|
||||
});
|
||||
return Object.keys(byValue).map(function(value) {
|
||||
return { value: value, label: byValue[value] };
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
var messages = pageAiNormalizeArray(session && session.messages);
|
||||
return !(source === 'draft' && status === 'draft' && messages.length === 0);
|
||||
}
|
||||
|
||||
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);
|
||||
return normalized.filter(function(session) {
|
||||
return (filterValue === 'all' || pageAiSessionAgentFilterValue(session) === filterValue)
|
||||
&& (statusFilter === 'all' || pageAiSessionStatusFilterValue(session) === statusFilter);
|
||||
});
|
||||
}
|
||||
|
||||
function pageAiTimestamp(value) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
var parsed = Date.parse(value);
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function pageAiUsageSummary(usage) {
|
||||
if (!usage || typeof usage !== 'object') return '';
|
||||
var used = usage.used ?? usage.contextUsed ?? usage.input_tokens ?? usage.inputTokens ?? usage.prompt_tokens ?? usage.promptTokens;
|
||||
var size = usage.size ?? usage.contextSize ?? usage.total_tokens ?? usage.totalTokens;
|
||||
var output = usage.output_tokens ?? usage.outputTokens ?? usage.completion_tokens ?? usage.completionTokens;
|
||||
var parts = [];
|
||||
if (Number.isFinite(Number(used))) parts.push('ctx ' + Number(used));
|
||||
if (Number.isFinite(Number(size)) && Number(size) > 0) parts.push('/ ' + Number(size));
|
||||
if (Number.isFinite(Number(output)) && Number(output) > 0) parts.push('out ' + Number(output));
|
||||
return parts.join(' ') || '';
|
||||
}
|
||||
|
||||
return {
|
||||
pageAiProviderLabel,
|
||||
pageAiNormalizeArray,
|
||||
pageAiDefaultAcpRuntimes,
|
||||
pageAiNormalizeAcpRuntimes,
|
||||
pageAiUnwrapUpstream,
|
||||
pageAiProfileValue,
|
||||
pageAiCurrentProfile,
|
||||
pageAiRunProfile,
|
||||
pageAiMnoteToolModel,
|
||||
pageAiCurrentProfileRecord,
|
||||
pageAiChatOnlyProfileSpec,
|
||||
pageAiDefaultChatOnlyProfileSpec,
|
||||
pageAiNormalizeChatOnlyProfileId,
|
||||
pageAiProfileDisplayLabel,
|
||||
pageAiProfileRecordById,
|
||||
pageAiSessionAgentFilterValue,
|
||||
pageAiSessionAgentLabel,
|
||||
pageAiSessionPreviewText,
|
||||
pageAiSessionAgentFilterOptions,
|
||||
pageAiSessionStatusFilterValue,
|
||||
pageAiSessionStatusLabel,
|
||||
pageAiSessionStatusFilterOptions,
|
||||
pageAiFilteredHistoryRows,
|
||||
pageAiTimestamp,
|
||||
pageAiUsageSummary
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,863 +0,0 @@
|
||||
export function createSidebarPageAiSessionRuntime(context) {
|
||||
const {
|
||||
currentDocumentId,
|
||||
currentRootUri,
|
||||
currentSourceKind,
|
||||
documentRef,
|
||||
pageAiApplyRuntimeState,
|
||||
pageAiCurrentAgentId,
|
||||
pageAiCurrentProfile,
|
||||
pageAiErrorMessage,
|
||||
pageAiNormalizeAgentId,
|
||||
pageAiNormalizeArray,
|
||||
pageAiNormalizeChatOnlyProfileId,
|
||||
pageAiPermissionMessage,
|
||||
pageAiPreviewValue,
|
||||
pageAiRunProfile,
|
||||
pageAiSetActiveProfile,
|
||||
pageAiTimestamp,
|
||||
pageUiState,
|
||||
renderPageAiControls,
|
||||
renderPageAiConversation,
|
||||
resolveWorkspaceId,
|
||||
sessionStorageVersion,
|
||||
windowRef,
|
||||
} = context;
|
||||
const doc = documentRef || document;
|
||||
const win = windowRef || window;
|
||||
|
||||
function pageAiStorageKey() {
|
||||
return 'hermes_page_ai_session:' + currentDocumentId();
|
||||
}
|
||||
|
||||
function pageAiBackendSessionQuery(extra) {
|
||||
var params = new URLSearchParams();
|
||||
params.set('source', 'acp');
|
||||
params.set('workspaceId', resolveWorkspaceId(doc.body));
|
||||
params.set('documentId', currentDocumentId());
|
||||
params.set('profile', pageAiRunProfile());
|
||||
params.set('sourceKind', currentSourceKind());
|
||||
if (currentRootUri()) params.set('rootUri', currentRootUri());
|
||||
Object.keys(extra || {}).forEach(function(key) {
|
||||
var value = extra[key];
|
||||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
});
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
function pageAiNewSession(title) {
|
||||
var now = Date.now();
|
||||
var agentId = pageAiCurrentAgentId();
|
||||
var profile = agentId === 'chat_only' ? pageAiNormalizeChatOnlyProfileId(pageAiCurrentProfile()) : pageAiCurrentProfile();
|
||||
return {
|
||||
id: 'sess_' + now + '_' + Math.random().toString(16).slice(2, 8),
|
||||
title: title || '新会话',
|
||||
agentId: agentId,
|
||||
profileId: pageUiState.pageAiAcpRuntime === 'hermes' ? profile : '',
|
||||
profile: profile,
|
||||
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
source: 'draft',
|
||||
usage: null,
|
||||
status: 'draft',
|
||||
messages: []
|
||||
};
|
||||
}
|
||||
|
||||
function pageAiNormalizeSessions(sessions) {
|
||||
return (Array.isArray(sessions) ? sessions : [])
|
||||
.slice(0, 20)
|
||||
.map(function(session) {
|
||||
var sessionStorage = String(session && (session.sessionStorage || session.session_storage) || '').trim();
|
||||
var persistence = String(session && session.persistence || '').trim();
|
||||
var agentId = pageAiNormalizeAgentId(session && (session.agentId || session.agent_id));
|
||||
var profileId = String(session && (session.profileId || session.profile_id || '') || '').trim();
|
||||
var profile = String(session && session.profile || pageAiCurrentProfile()).trim() || 'default';
|
||||
if (!profileId && agentId !== 'reasonix') profileId = profile;
|
||||
if (agentId === 'chat_only') {
|
||||
profileId = pageAiNormalizeChatOnlyProfileId(profileId || profile);
|
||||
profile = profileId;
|
||||
}
|
||||
return {
|
||||
schema: String(session && session.schema || '').trim(),
|
||||
id: String(session && session.id || '').trim() || pageAiNewSession().id,
|
||||
title: String(session && session.title || '').trim() || '新会话',
|
||||
agentId: agentId,
|
||||
profileId: profileId,
|
||||
profile: profile,
|
||||
acpRuntime: String(session && session.acpRuntime || 'reasonix').trim(),
|
||||
createdAt: pageAiTimestamp(session && session.createdAt),
|
||||
updatedAt: pageAiTimestamp(session && session.updatedAt),
|
||||
source: String(session && session.source || 'local').trim() || 'local',
|
||||
persistence: persistence,
|
||||
sessionStorage: sessionStorage,
|
||||
permissionLevel: String(session && (session.permissionLevel || session.permission_level) || '').trim(),
|
||||
shareId: String(session && (session.shareId || session.share_id) || '').trim(),
|
||||
acpSessionId: String(session && (session.acpSessionId || session.acp_session_id) || '').trim(),
|
||||
runId: String(session && (session.runId || session.run_id) || '').trim(),
|
||||
boardRunId: String(session && (session.boardRunId || session.board_run_id || session.runId || session.run_id) || '').trim(),
|
||||
workflowId: String(session && (session.workflowId || session.workflow_id) || '').trim(),
|
||||
workerPresetId: String(session && (session.workerPresetId || session.worker_preset_id) || '').trim(),
|
||||
modelOverride: String(session && (session.modelOverride || session.model_override) || '').trim(),
|
||||
receiptId: String(session && (session.receiptId || session.receipt_id) || '').trim(),
|
||||
boardRuns: session && session.boardRuns && typeof session.boardRuns === 'object' ? session.boardRuns : {},
|
||||
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).map(function(message) { return Object.assign({}, message); }) : []
|
||||
};
|
||||
})
|
||||
.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);
|
||||
});
|
||||
}
|
||||
|
||||
function pageAiNormalizeBackendSessionRow(row) {
|
||||
if (!row || typeof row !== 'object') return null;
|
||||
var payload = row.payload && typeof row.payload === 'object' ? row.payload : {};
|
||||
var sessionId = String(row.sessionId || row.session_id || '').trim();
|
||||
if (!sessionId) return null;
|
||||
var title = String(row.title || payload.title || payload.message || '').trim();
|
||||
if (title.length > 28) title = title.slice(0, 28) + '…';
|
||||
var persistence = String(row.persistence || payload.persistence || '').trim();
|
||||
var sessionStorage = String(row.sessionStorage || row.session_storage || payload.sessionStorage || '').trim();
|
||||
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;
|
||||
var agentId = pageAiNormalizeAgentId(row.agentId || row.agent_id || payload.agentId || payload.agent_id);
|
||||
var profileId = String(row.profileId || row.profile_id || payload.profileId || payload.profile_id || '').trim();
|
||||
var profile = String(row.profile || payload.profile || pageAiRunProfile()).trim() || 'default';
|
||||
if (!profileId && agentId !== 'reasonix') profileId = profile;
|
||||
if (agentId === 'chat_only') {
|
||||
profileId = pageAiNormalizeChatOnlyProfileId(profileId || profile);
|
||||
profile = profileId;
|
||||
}
|
||||
if (!sessionStorage && persistence === 'convex_acp_runtime_store') sessionStorage = 'cloud';
|
||||
if (!sessionStorage && persistence === 'local_ai_session_jsonl') sessionStorage = String(row.shareId || payload.shareId || '').trim() ? 'local_shared' : 'local_private';
|
||||
return {
|
||||
id: sessionId,
|
||||
title: title || '当前页问答',
|
||||
agentId: agentId,
|
||||
profileId: profileId,
|
||||
profile: profile,
|
||||
acpRuntime: String(row.acpRuntime || row.acp_runtime || payload.acpRuntime || 'reasonix').trim(),
|
||||
createdAt: pageAiTimestamp(row.createdAt || row.created_at),
|
||||
updatedAt: pageAiTimestamp(row.updatedAt || row.updated_at),
|
||||
source: sessionStorage === 'local_private' || sessionStorage === 'local_shared' ? 'local' : 'acp',
|
||||
persistence: persistence,
|
||||
sessionStorage: sessionStorage,
|
||||
permissionLevel: String(row.permissionLevel || row.permission_level || payload.permissionLevel || '').trim(),
|
||||
shareId: String(row.shareId || row.share_id || payload.shareId || '').trim(),
|
||||
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: []
|
||||
};
|
||||
}
|
||||
|
||||
function pageAiMergeSessions(localSessions, backendSessions) {
|
||||
var byId = {};
|
||||
pageAiNormalizeSessions(localSessions).forEach(function(session) {
|
||||
byId[session.id] = session;
|
||||
});
|
||||
pageAiNormalizeSessions(backendSessions).forEach(function(session) {
|
||||
var existing = byId[session.id];
|
||||
byId[session.id] = Object.assign({}, existing || {}, session, {
|
||||
messages: session.messages.length ? session.messages : (existing && existing.messages || [])
|
||||
});
|
||||
});
|
||||
return pageAiNormalizeSessions(Object.keys(byId).map(function(id) { return byId[id]; }));
|
||||
}
|
||||
|
||||
function pageAiDedupeSessions(sessions) {
|
||||
var byId = {};
|
||||
var ordered = [];
|
||||
pageAiNormalizeSessions(sessions).forEach(function(session) {
|
||||
var id = String(session && session.id || '').trim();
|
||||
if (!id || byId[id]) return;
|
||||
byId[id] = true;
|
||||
ordered.push(session);
|
||||
});
|
||||
return ordered;
|
||||
}
|
||||
|
||||
function pageAiSessionStorageLabel(session) {
|
||||
var storage = String(session && (session.sessionStorage || session.session_storage) || '').trim();
|
||||
var persistence = String(session && session.persistence || '').trim();
|
||||
if (storage === 'local_shared') return '共享会话';
|
||||
if (storage === 'local_private') return '本地私有';
|
||||
if (storage === 'sqlite_control_plane' || persistence === 'sqlite_acp_runtime_store') return '账号会话';
|
||||
if (storage === 'cloud' || persistence === 'convex_acp_runtime_store') return '云端会话';
|
||||
if (persistence === 'local_ai_session_jsonl') return '本地私有';
|
||||
return String(session && session.source || '').trim() === 'acp' ? '云端会话' : '本地私有';
|
||||
}
|
||||
|
||||
function pageAiLoadSessions() {
|
||||
if (pageUiState.pageAiSessions.length && pageUiState.pageAiActiveSessionId) return;
|
||||
try {
|
||||
var raw = win.localStorage.getItem(pageAiStorageKey());
|
||||
var parsed = raw ? JSON.parse(raw) : null;
|
||||
var activeProfile = String(parsed && parsed.activeProfileName || '').trim();
|
||||
var activeAcpRuntime = String(parsed && parsed.activeAcpRuntime || '').trim();
|
||||
var storageVersion = Number(parsed && parsed.version || 0);
|
||||
if (storageVersion >= sessionStorageVersion && activeAcpRuntime) pageUiState.pageAiAcpRuntime = activeAcpRuntime;
|
||||
if (activeProfile) pageAiSetActiveProfile(activeProfile);
|
||||
var storedSessions = pageAiNormalizeSessions(parsed && parsed.sessions);
|
||||
if (storageVersion >= sessionStorageVersion && storedSessions.length) {
|
||||
pageUiState.pageAiSessions = storedSessions;
|
||||
var activeSessionId = String(parsed && parsed.activeSessionId || '').trim();
|
||||
pageUiState.pageAiActiveSessionId = storedSessions.some(function(session) { return session.id === activeSessionId; }) ? activeSessionId : storedSessions[0].id;
|
||||
var active = pageAiCurrentSession();
|
||||
pageUiState.pageAiMessages = active && Array.isArray(active.messages) ? active.messages.slice() : [];
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
var fresh = pageAiNewSession();
|
||||
pageUiState.pageAiSessions = [fresh];
|
||||
pageUiState.pageAiActiveSessionId = fresh.id;
|
||||
pageUiState.pageAiMessages = [];
|
||||
}
|
||||
|
||||
async function pageAiLoadBackendSessions() {
|
||||
var response = await fetch('/api/page-ai/sessions?' + pageAiBackendSessionQuery({ limit: 50 }), {
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'session_list_failed_' + response.status));
|
||||
}
|
||||
var backendSessions = pageAiNormalizeArray(payload.sessions).map(pageAiNormalizeBackendSessionRow).filter(Boolean);
|
||||
var draftSessions = pageAiNormalizeArray(pageUiState.pageAiSessions).filter(function(session) {
|
||||
var id = String(session && session.id || '').trim();
|
||||
return id && !id.startsWith('mnote_')
|
||||
&& (id === pageUiState.pageAiActiveSessionId || (Array.isArray(session.messages) && session.messages.length > 0));
|
||||
});
|
||||
if (!backendSessions.length) {
|
||||
if (!draftSessions.length && !pageUiState.pageAiActiveSessionId) {
|
||||
var fresh = pageAiNewSession();
|
||||
pageUiState.pageAiSessions = [fresh];
|
||||
pageUiState.pageAiActiveSessionId = fresh.id;
|
||||
pageUiState.pageAiMessages = [];
|
||||
}
|
||||
pageUiState.pageAiSessionError = '';
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
return [];
|
||||
}
|
||||
pageUiState.pageAiSessions = pageAiDedupeSessions(backendSessions.concat(draftSessions));
|
||||
if (!pageUiState.pageAiActiveSessionId || !pageUiState.pageAiSessions.find(function(session) { return session.id === pageUiState.pageAiActiveSessionId; })) {
|
||||
pageUiState.pageAiActiveSessionId = pageUiState.pageAiSessions[0].id;
|
||||
}
|
||||
var active = pageAiCurrentSession();
|
||||
if (active) {
|
||||
if (active.profile) pageAiSetActiveProfile(active.profile);
|
||||
if (!pageUiState.pageAiAcpRuntime && active.acpRuntime) pageUiState.pageAiAcpRuntime = active.acpRuntime;
|
||||
pageUiState.pageAiMessages = Array.isArray(active.messages) ? active.messages.slice() : pageUiState.pageAiMessages;
|
||||
}
|
||||
pageUiState.pageAiSessionError = '';
|
||||
pageAiPersistSessions();
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
return backendSessions;
|
||||
}
|
||||
|
||||
function pageAiMessageFromRuntimeEvent(event) {
|
||||
if (!event || typeof event !== 'object') return null;
|
||||
var eventType = String(event.eventType || event.event_type || event.event || '').trim();
|
||||
var payload = event.payload && typeof event.payload === 'object' ? event.payload : event;
|
||||
if (payload && (payload.source === 'adapter_replay' || payload.replay === true)) return null;
|
||||
if (eventType === 'message.delta') {
|
||||
var delta = String(payload.delta || payload.text || payload.output_text || '');
|
||||
return delta ? { role: 'assistant', content: delta } : null;
|
||||
}
|
||||
if (eventType === 'thought.delta') {
|
||||
var thought = String(payload.delta || payload.text || '').trim();
|
||||
return thought ? { role: 'assistant', kind: 'thought', content: thought } : null;
|
||||
}
|
||||
if (eventType === 'tool.started' || eventType === 'tool.completed' || eventType === 'tool.failed') {
|
||||
var toolName = String(payload.toolName || payload.tool || payload.name || eventType).trim();
|
||||
var rawLocations = payload.locations;
|
||||
return {
|
||||
role: 'tool',
|
||||
content: toolName,
|
||||
toolName: toolName,
|
||||
toolCallId: String(payload.toolCallId || payload.tool_call_id || payload.id || ''),
|
||||
toolKind: String(payload.kind || ''),
|
||||
status: eventType === 'tool.completed' ? 'completed' : (eventType === 'tool.failed' ? 'failed' : 'running'),
|
||||
argsSummary: pageAiPreviewValue(payload.args || payload.arguments || payload.input),
|
||||
resultSummary: pageAiPreviewValue(payload.summary || payload.result || payload.output || payload.error),
|
||||
locations: Array.isArray(rawLocations) ? rawLocations.filter(function(l) { return typeof l === 'string' || (typeof l === 'object' && l && l.path); }).map(function(l) { return typeof l === 'string' ? l : l.path; }) : [],
|
||||
traceId: String(payload.traceId || payload.trace_id || ''),
|
||||
auditId: String(payload.auditId || payload.audit_id || '')
|
||||
};
|
||||
}
|
||||
if (eventType === 'permission.requested' || eventType === 'permission.denied' || eventType === 'permission.allowed') {
|
||||
return pageAiPermissionMessage(payload, eventType);
|
||||
}
|
||||
if (eventType === 'run.completed') {
|
||||
var output = String(payload.output || payload.text || '').trim();
|
||||
return output ? { role: 'assistant', content: output } : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function pageAiApplyBackendSessionDetail(payload) {
|
||||
var sessionPayload = payload && payload.session ? payload.session : {};
|
||||
var runs = pageAiNormalizeArray(sessionPayload.runs);
|
||||
var latest = runs.length ? pageAiNormalizeBackendSessionRow(runs[0]) : null;
|
||||
var events = pageAiNormalizeArray(payload && payload.events);
|
||||
var storedMessages = pageAiNormalizeArray(sessionPayload.messages).map(function(message) {
|
||||
return {
|
||||
role: message.role === 'user' ? 'user' : (message.role === 'tool' ? 'tool' : 'assistant'),
|
||||
content: String(message.content || '')
|
||||
};
|
||||
}).filter(function(message) { return message.content; });
|
||||
var eventsByRunId = {};
|
||||
events.forEach(function(event) {
|
||||
var runId = String(event && (event.runId || event.run_id) || '').trim();
|
||||
if (!runId) return;
|
||||
if (!eventsByRunId[runId]) eventsByRunId[runId] = [];
|
||||
eventsByRunId[runId].push(event);
|
||||
});
|
||||
var messages = [];
|
||||
if (runs.length) {
|
||||
runs.slice().reverse().forEach(function(run) {
|
||||
var runPayload = run && run.payload && typeof run.payload === 'object' ? run.payload : {};
|
||||
var userMessage = String(runPayload.message || runPayload.input || '').trim();
|
||||
if (userMessage) messages.push({ role: 'user', content: userMessage });
|
||||
var runId = String(run && (run.runId || run.run_id) || '').trim();
|
||||
var assistantDelta = '';
|
||||
var completedOutput = '';
|
||||
function flushAssistantDelta() {
|
||||
var content = assistantDelta.trim();
|
||||
if (content) messages.push({ role: 'assistant', content: content });
|
||||
assistantDelta = '';
|
||||
}
|
||||
pageAiNormalizeArray(eventsByRunId[runId]).forEach(function(event) {
|
||||
var message = pageAiMessageFromRuntimeEvent(event);
|
||||
if (!message) return;
|
||||
if (message.role === 'assistant' && !message.kind) {
|
||||
var eventType = String(event && (event.eventType || event.event_type || event.event) || '').trim();
|
||||
if (eventType === 'run.completed') {
|
||||
completedOutput = String(message.content || '').trim();
|
||||
return;
|
||||
}
|
||||
assistantDelta += String(message.content || '');
|
||||
return;
|
||||
}
|
||||
flushAssistantDelta();
|
||||
messages.push(message);
|
||||
});
|
||||
flushAssistantDelta();
|
||||
if (completedOutput && !messages.some(function(message) {
|
||||
return message.role === 'assistant' && String(message.content || '').trim() === completedOutput;
|
||||
})) {
|
||||
messages.push({ role: 'assistant', content: completedOutput });
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!messages.length) messages = storedMessages;
|
||||
var acpSessionId = '';
|
||||
events.forEach(function(event) {
|
||||
var eventType = String(event && (event.eventType || event.event_type || event.event) || '').trim();
|
||||
var payload = event && event.payload && typeof event.payload === 'object' ? event.payload : event;
|
||||
if (eventType === 'session.info.updated') {
|
||||
var nextAcpSessionId = String(payload && (payload.acpSessionId || payload.acp_session_id) || '').trim();
|
||||
if (nextAcpSessionId) acpSessionId = nextAcpSessionId;
|
||||
}
|
||||
});
|
||||
var current = pageAiCurrentSession();
|
||||
if (latest && current) {
|
||||
Object.assign(current, latest);
|
||||
}
|
||||
if (current) {
|
||||
current.messages = messages.slice(-300);
|
||||
current.updatedAt = Math.max(Number(current.updatedAt || 0), Date.now());
|
||||
if (acpSessionId) current.acpSessionId = acpSessionId;
|
||||
if (latest && latest.usage) current.usage = latest.usage;
|
||||
}
|
||||
pageAiApplyRuntimeState(payload && payload.runtime);
|
||||
pageUiState.pageAiMessages = messages.slice(-300);
|
||||
pageAiPersistSessions();
|
||||
}
|
||||
|
||||
async function pageAiLoadBackendSessionDetail(sessionId) {
|
||||
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
|
||||
if (!sessionId) return null;
|
||||
var response = await fetch('/api/page-ai/sessions/' + encodeURIComponent(sessionId) + '?' + pageAiBackendSessionQuery({ limit: 200 }), {
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'session_detail_failed_' + response.status));
|
||||
}
|
||||
pageAiApplyBackendSessionDetail(payload);
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function pageAiSearchBackendSessions(query) {
|
||||
var q = String(query || '').trim();
|
||||
pageUiState.pageAiSessionSearchQuery = q;
|
||||
if (!q) {
|
||||
pageUiState.pageAiSessionSearchResults = [];
|
||||
renderPageAiConversation();
|
||||
return [];
|
||||
}
|
||||
var response = await fetch('/api/page-ai/sessions/search?' + pageAiBackendSessionQuery({ q: q, limit: 20 }), {
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'session_search_failed_' + response.status));
|
||||
}
|
||||
pageUiState.pageAiSessionSearchResults = pageAiNormalizeArray(payload.results).map(function(row) {
|
||||
var normalized = pageAiNormalizeBackendSessionRow(row) || {};
|
||||
normalized.snippet = String(row && row.snippet || normalized.preview || '').trim();
|
||||
return normalized;
|
||||
}).filter(function(row) { return row.id; });
|
||||
renderPageAiConversation();
|
||||
return pageUiState.pageAiSessionSearchResults;
|
||||
}
|
||||
|
||||
async function pageAiExportBackendSession(sessionId) {
|
||||
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
|
||||
if (!sessionId) return null;
|
||||
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; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'session_export_failed_' + response.status));
|
||||
}
|
||||
var exported = payload.export && typeof payload.export === 'object' ? payload.export : {};
|
||||
var session = pageAiFindSessionById(sessionId) || pageAiCurrentSession();
|
||||
if (session) {
|
||||
session.exportedAt = Date.now();
|
||||
session.exportMarkdown = String(exported.markdown || '');
|
||||
session.updatedAt = Date.now();
|
||||
}
|
||||
doc.documentElement.setAttribute('data-mnote-page-ai-session-exported', sessionId);
|
||||
pageAiPersistSessions();
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
return payload;
|
||||
}
|
||||
|
||||
function pageAiPersistSessions() {
|
||||
pageAiSyncCurrentSessionMessages();
|
||||
doc.documentElement.setAttribute('data-mnote-page-ai-session-owner', 'hermes');
|
||||
doc.documentElement.setAttribute('data-mnote-page-ai-session-key', pageAiStorageKey());
|
||||
try {
|
||||
win.localStorage.setItem(pageAiStorageKey(), JSON.stringify({
|
||||
version: sessionStorageVersion,
|
||||
activeSessionId: pageUiState.pageAiActiveSessionId,
|
||||
activeProfileName: pageAiCurrentProfile(),
|
||||
activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
|
||||
sessions: pageAiNormalizeArray(pageUiState.pageAiSessions).slice(0, 20)
|
||||
}));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function pageAiEnsureHermesSession(forceCreate) {
|
||||
pageAiLoadSessions();
|
||||
var current = pageAiCurrentSession();
|
||||
var runProfile = pageAiRunProfile();
|
||||
if (!forceCreate && current && String(current.id || '').startsWith('mnote_') && current.profile === runProfile) return current;
|
||||
var response = await fetch('/api/page-ai/sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
workspaceId: resolveWorkspaceId(doc.body),
|
||||
documentId: currentDocumentId(),
|
||||
sourceKind: currentSourceKind(),
|
||||
rootUri: currentRootUri(),
|
||||
traceId: 'page-ai-' + Date.now().toString(36),
|
||||
profile: runProfile,
|
||||
agentId: pageAiCurrentAgentId(),
|
||||
profileId: pageUiState.pageAiAcpRuntime === 'hermes' ? runProfile : '',
|
||||
title: current && current.title ? current.title : '当前页问答'
|
||||
})
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'hermes_session_failed_' + response.status));
|
||||
}
|
||||
var session = {
|
||||
id: String(payload.sessionId || '').trim(),
|
||||
title: String(payload.title || '当前页问答'),
|
||||
agentId: pageAiCurrentAgentId(),
|
||||
profileId: pageUiState.pageAiAcpRuntime === 'hermes' ? runProfile : '',
|
||||
profile: String(payload.profile || runProfile).trim() || 'default',
|
||||
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
|
||||
persistence: String(payload.persistence || '').trim(),
|
||||
sessionStorage: String(payload.sessionStorage || '').trim(),
|
||||
permissionLevel: String(payload.permissionLevel || '').trim(),
|
||||
shareId: String(payload.shareId || '').trim(),
|
||||
acpSessionId: String(payload.acpSessionId || payload.acp_session_id || '').trim(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
messages: pageUiState.pageAiMessages.slice()
|
||||
};
|
||||
var previousSessionId = String(current && current.id || '').trim();
|
||||
var retainedSessions = pageAiNormalizeArray(pageUiState.pageAiSessions).filter(function(item) {
|
||||
var itemId = String(item && item.id || '').trim();
|
||||
return itemId && itemId !== session.id && itemId !== previousSessionId;
|
||||
});
|
||||
pageUiState.pageAiSessions = pageAiNormalizeSessions([session].concat(retainedSessions));
|
||||
pageUiState.pageAiActiveSessionId = session.id;
|
||||
pageAiPersistSessions();
|
||||
renderPageAiConversation();
|
||||
return session;
|
||||
}
|
||||
|
||||
async function pageAiRestoreHermesSession() {
|
||||
var current = pageAiCurrentSession();
|
||||
if (!current || !String(current.id || '').startsWith('mnote_')) return;
|
||||
var response = await fetch('/api/page-ai/sessions/' + encodeURIComponent(current.id) + '/resume?' + pageAiBackendSessionQuery({}), {
|
||||
method: 'POST',
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
if (!response.ok) return;
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (payload && payload.persistence === 'convex_acp_runtime_store') {
|
||||
pageAiApplyBackendSessionDetail(payload);
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
return;
|
||||
}
|
||||
var session = payload && (payload.session || (payload.upstream && payload.upstream.session) || payload);
|
||||
var messages = session && Array.isArray(session.messages) ? session.messages : [];
|
||||
pageAiApplyRuntimeState(payload && payload.runtime);
|
||||
if (session && (session.profile || session.profileName)) {
|
||||
current.profile = String(session.profile || session.profileName || current.profile || pageAiCurrentProfile());
|
||||
}
|
||||
if (!messages.length) {
|
||||
renderPageAiControls();
|
||||
return;
|
||||
}
|
||||
pageUiState.pageAiMessages = messages.slice(-300).map(function(message) {
|
||||
return {
|
||||
role: message.role === 'user' ? 'user' : (message.role === 'tool' ? 'tool' : 'assistant'),
|
||||
content: String(message.content || '')
|
||||
};
|
||||
});
|
||||
current.messages = pageUiState.pageAiMessages.slice();
|
||||
current.updatedAt = Date.now();
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
function pageAiCurrentSession() {
|
||||
return pageUiState.pageAiSessions.find(function(session) {
|
||||
return session.id === pageUiState.pageAiActiveSessionId;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function pageAiSelectedSessionIds() {
|
||||
if (!pageUiState.pageAiSelectedSessionIds || typeof pageUiState.pageAiSelectedSessionIds !== 'object') {
|
||||
pageUiState.pageAiSelectedSessionIds = {};
|
||||
}
|
||||
return pageUiState.pageAiSelectedSessionIds;
|
||||
}
|
||||
|
||||
function pageAiSessionSelected(sessionId) {
|
||||
sessionId = String(sessionId || '').trim();
|
||||
return Boolean(sessionId && pageAiSelectedSessionIds()[sessionId]);
|
||||
}
|
||||
|
||||
function pageAiSelectedSessionList() {
|
||||
var selected = pageAiSelectedSessionIds();
|
||||
return Object.keys(selected).filter(function(sessionId) {
|
||||
return selected[sessionId] === true;
|
||||
});
|
||||
}
|
||||
|
||||
function pageAiSelectedSessionCount() {
|
||||
return pageAiSelectedSessionList().length;
|
||||
}
|
||||
|
||||
function pageAiToggleSessionSelection(sessionId, selected) {
|
||||
sessionId = String(sessionId || '').trim();
|
||||
if (!sessionId) return;
|
||||
var selectedMap = Object.assign({}, pageAiSelectedSessionIds());
|
||||
if (selected === false) {
|
||||
delete selectedMap[sessionId];
|
||||
} else {
|
||||
selectedMap[sessionId] = true;
|
||||
}
|
||||
pageUiState.pageAiSelectedSessionIds = selectedMap;
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
function pageAiClearSessionSelection() {
|
||||
pageUiState.pageAiSelectedSessionIds = {};
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
function pageAiSyncCurrentSessionMessages() {
|
||||
var session = pageAiCurrentSession();
|
||||
if (!session) return;
|
||||
var runProfile = pageAiRunProfile();
|
||||
session.messages = Array.isArray(pageUiState.pageAiMessages) ? pageUiState.pageAiMessages.slice(-300) : [];
|
||||
session.agentId = pageAiCurrentAgentId();
|
||||
session.profileId = pageUiState.pageAiAcpRuntime === 'hermes' ? runProfile : '';
|
||||
session.profile = runProfile;
|
||||
session.acpRuntime = pageUiState.pageAiAcpRuntime || 'reasonix';
|
||||
if (String(session.source || '') === 'board') {
|
||||
session.schema = session.schema || 'mnote.page_ai.session.v2';
|
||||
session.workerPresetId = pageUiState.pageAiBoardWorkerId || session.workerPresetId || '';
|
||||
session.workflowId = pageUiState.pageAiBoardWorkflowId || session.workflowId || '';
|
||||
session.modelOverride = pageUiState.pageAiBoardModelOverride || session.modelOverride || '';
|
||||
}
|
||||
session.updatedAt = Date.now();
|
||||
}
|
||||
|
||||
function pageAiSetActiveSession(sessionId) {
|
||||
var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
|
||||
if (!session) return;
|
||||
pageUiState.pageAiActiveSessionId = session.id;
|
||||
if (session.agentId) pageUiState.pageAiAgentId = pageAiNormalizeAgentId(session.agentId);
|
||||
if (session.source === 'board') {
|
||||
if (session.workerPresetId) pageUiState.pageAiBoardWorkerId = session.workerPresetId;
|
||||
if (session.workflowId) pageUiState.pageAiBoardWorkflowId = session.workflowId;
|
||||
if (session.modelOverride) pageUiState.pageAiBoardModelOverride = session.modelOverride;
|
||||
if (session.boardRuns && typeof session.boardRuns === 'object') {
|
||||
pageUiState.pageAiBoardRunDetails = Object.assign({}, pageUiState.pageAiBoardRunDetails || {}, session.boardRuns);
|
||||
}
|
||||
}
|
||||
if (session.acpRuntime) pageUiState.pageAiAcpRuntime = session.acpRuntime;
|
||||
if (session.profileId || session.profile) pageAiSetActiveProfile(session.profileId || session.profile);
|
||||
pageUiState.pageAiMessages = Array.isArray(session.messages) ? session.messages.slice() : [];
|
||||
pageUiState.pageAiPage = 'chat';
|
||||
pageAiPersistSessions();
|
||||
renderPageAiControls();
|
||||
renderPageAiConversation();
|
||||
if (session.source === 'acp' || String(session.id || '').startsWith('mnote_')) {
|
||||
void pageAiLoadBackendSessionDetail(session.id).catch(function(error) {
|
||||
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
|
||||
renderPageAiControls();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function pageAiStartNewSession() {
|
||||
var session = pageAiNewSession();
|
||||
pageUiState.pageAiSessions = pageAiNormalizeSessions([session].concat(pageUiState.pageAiSessions));
|
||||
pageUiState.pageAiActiveSessionId = session.id;
|
||||
pageUiState.pageAiMessages = [];
|
||||
pageUiState.pageAiPage = 'chat';
|
||||
pageAiPersistSessions();
|
||||
renderPageAiControls();
|
||||
renderPageAiConversation();
|
||||
}
|
||||
|
||||
async function pageAiRenameBackendSession(sessionId) {
|
||||
var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
|
||||
if (!session) return;
|
||||
var title = win.prompt('重命名 AI 会话', session.title || '当前页问答');
|
||||
if (title === null) return;
|
||||
title = String(title || '').trim();
|
||||
if (!title) return;
|
||||
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 })
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'session_rename_failed_' + response.status));
|
||||
}
|
||||
var nextTitle = String((payload.result && payload.result.title) || title);
|
||||
[pageUiState.pageAiSessions, pageUiState.pageAiSessionSearchResults].forEach(function(list) {
|
||||
pageAiNormalizeArray(list).forEach(function(item) {
|
||||
if (String(item && item.id || '').trim() === sessionId) {
|
||||
item.title = nextTitle;
|
||||
item.updatedAt = Date.now();
|
||||
}
|
||||
});
|
||||
});
|
||||
pageAiPersistSessions();
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
function pageAiSessionRequiresBackendDelete(session) {
|
||||
var id = String(session && session.id || '').trim();
|
||||
var source = String(session && session.source || '').trim();
|
||||
return Boolean(id && (
|
||||
id.startsWith('mnote_')
|
||||
|| source === 'acp'
|
||||
|| String(session && session.persistence || '').trim()
|
||||
|| String(session && (session.sessionStorage || session.session_storage) || '').trim()
|
||||
));
|
||||
}
|
||||
|
||||
function pageAiFindSessionById(sessionId) {
|
||||
sessionId = String(sessionId || '').trim();
|
||||
if (!sessionId) return null;
|
||||
return pageAiNormalizeArray(pageUiState.pageAiSessions).find(function(item) {
|
||||
return String(item && item.id || '').trim() === sessionId;
|
||||
}) || pageAiNormalizeArray(pageUiState.pageAiSessionSearchResults).find(function(item) {
|
||||
return String(item && item.id || '').trim() === sessionId;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function pageAiApplyDeletedSessionIds(sessionIds) {
|
||||
var deleted = {};
|
||||
pageAiNormalizeArray(sessionIds).forEach(function(sessionId) {
|
||||
sessionId = String(sessionId || '').trim();
|
||||
if (sessionId) deleted[sessionId] = true;
|
||||
});
|
||||
pageUiState.pageAiSessions = pageAiNormalizeArray(pageUiState.pageAiSessions).filter(function(item) {
|
||||
return !deleted[String(item && item.id || '').trim()];
|
||||
});
|
||||
pageUiState.pageAiSessionSearchResults = pageAiNormalizeArray(pageUiState.pageAiSessionSearchResults).filter(function(item) {
|
||||
return !deleted[String(item && item.id || '').trim()];
|
||||
});
|
||||
var selected = Object.assign({}, pageAiSelectedSessionIds());
|
||||
Object.keys(deleted).forEach(function(sessionId) { delete selected[sessionId]; });
|
||||
pageUiState.pageAiSelectedSessionIds = selected;
|
||||
if (deleted[pageUiState.pageAiActiveSessionId]) {
|
||||
var next = pageUiState.pageAiSessions[0] || pageAiNewSession();
|
||||
if (!pageUiState.pageAiSessions.length) pageUiState.pageAiSessions = [next];
|
||||
pageUiState.pageAiActiveSessionId = next.id;
|
||||
pageUiState.pageAiMessages = Array.isArray(next.messages) ? next.messages.slice() : [];
|
||||
}
|
||||
}
|
||||
|
||||
async function pageAiDeleteBackendSessions(sessionIds) {
|
||||
var ids = pageAiNormalizeArray(sessionIds).map(function(sessionId) {
|
||||
return String(sessionId || '').trim();
|
||||
}).filter(Boolean);
|
||||
if (!ids.length) return;
|
||||
var sessions = ids.map(pageAiFindSessionById).filter(Boolean);
|
||||
if (!sessions.length) return;
|
||||
var confirmText = sessions.length === 1
|
||||
? '确定删除 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/page-ai/sessions/' + encodeURIComponent(session.id) + '?' + pageAiBackendSessionQuery({}), {
|
||||
method: 'DELETE',
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'session_delete_failed_' + response.status));
|
||||
}
|
||||
}
|
||||
pageAiApplyDeletedSessionIds(sessions.map(function(session) { return session.id; }));
|
||||
pageAiPersistSessions();
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
async function pageAiDeleteBackendSession(sessionId) {
|
||||
return pageAiDeleteBackendSessions([sessionId]);
|
||||
}
|
||||
|
||||
async function pageAiDeleteSelectedBackendSessions() {
|
||||
return pageAiDeleteBackendSessions(pageAiSelectedSessionList());
|
||||
}
|
||||
|
||||
async function pageAiResumeBackendSession(sessionId) {
|
||||
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
|
||||
if (!sessionId) return;
|
||||
pageAiSetActiveSession(sessionId);
|
||||
var response = await fetch('/api/page-ai/sessions/' + encodeURIComponent(sessionId) + '/resume?' + pageAiBackendSessionQuery({}), {
|
||||
method: 'POST',
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'session_resume_failed_' + response.status));
|
||||
}
|
||||
pageAiApplyBackendSessionDetail(payload);
|
||||
pageUiState.pageAiPage = 'chat';
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
async function pageAiCheckActiveRun(sessionId) {
|
||||
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
|
||||
if (!sessionId) return null;
|
||||
var response = await fetch('/api/page-ai/sessions/' + encodeURIComponent(sessionId) + '/active-run?' + pageAiBackendSessionQuery({}), {
|
||||
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, 'active_run_failed_' + response.status));
|
||||
}
|
||||
if (!payload.active || !payload.run) return null;
|
||||
var run = payload.run;
|
||||
var hostRunId = String(run.hostRunId || run.runId || '').trim();
|
||||
var status = String(run.status || '').trim();
|
||||
var current = pageAiCurrentSession();
|
||||
if (current && current.id === sessionId) {
|
||||
current.runId = hostRunId;
|
||||
current.status = status || current.status || 'running';
|
||||
current.updatedAt = Date.now();
|
||||
}
|
||||
pageAiApplyRuntimeState(Object.assign({}, run.runtime || {}, {
|
||||
status: status || 'running',
|
||||
runId: hostRunId
|
||||
}));
|
||||
doc.documentElement.setAttribute('data-mnote-page-ai-active-run-checked', 'true');
|
||||
if (hostRunId) doc.documentElement.setAttribute('data-mnote-page-ai-active-host-run-id', hostRunId);
|
||||
pageAiPersistSessions();
|
||||
renderPageAiControls();
|
||||
return run;
|
||||
}
|
||||
|
||||
return {
|
||||
pageAiStorageKey,
|
||||
pageAiBackendSessionQuery,
|
||||
pageAiNewSession,
|
||||
pageAiNormalizeSessions,
|
||||
pageAiNormalizeBackendSessionRow,
|
||||
pageAiMergeSessions,
|
||||
pageAiSessionStorageLabel,
|
||||
pageAiLoadSessions,
|
||||
pageAiLoadBackendSessions,
|
||||
pageAiMessageFromRuntimeEvent,
|
||||
pageAiApplyBackendSessionDetail,
|
||||
pageAiLoadBackendSessionDetail,
|
||||
pageAiSearchBackendSessions,
|
||||
pageAiPersistSessions,
|
||||
pageAiEnsureHermesSession,
|
||||
pageAiRestoreHermesSession,
|
||||
pageAiCurrentSession,
|
||||
pageAiSessionSelected,
|
||||
pageAiToggleSessionSelection,
|
||||
pageAiClearSessionSelection,
|
||||
pageAiSelectedSessionCount,
|
||||
pageAiSyncCurrentSessionMessages,
|
||||
pageAiSetActiveSession,
|
||||
pageAiStartNewSession,
|
||||
pageAiRenameBackendSession,
|
||||
pageAiExportBackendSession,
|
||||
pageAiDeleteBackendSession,
|
||||
pageAiDeleteSelectedBackendSessions,
|
||||
pageAiResumeBackendSession,
|
||||
pageAiCheckActiveRun
|
||||
};
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
export function createSidebarPageAiSkillRuntime(context) {
|
||||
const {
|
||||
pageAiCurrentAgentId,
|
||||
pageAiCurrentProfile,
|
||||
pageAiLoadSkills,
|
||||
pageAiNormalizeArray,
|
||||
pageAiPersistAiPreference,
|
||||
pageAiPersistRawAiPreference,
|
||||
pageAiProfileValue,
|
||||
pageUiState,
|
||||
renderPageAiControls,
|
||||
} = context;
|
||||
|
||||
function pageAiSkillSourceOptions() {
|
||||
var options = [
|
||||
{ value: 'mnote', group: 'mnote', label: 'MNote 公共能力', profile: '' },
|
||||
{ value: 'reasonix', group: 'reasonix', label: 'Reasonix skill(查看)', profile: '', readonly: true }
|
||||
];
|
||||
pageAiNormalizeArray(pageUiState.pageAiProfiles).forEach(function(profile) {
|
||||
var profileId = pageAiProfileValue(profile);
|
||||
if (!profileId || pageAiProfileIsChatOnlySkillSource(profile)) return;
|
||||
var label = profile.kind === 'shared' ? 'Hermes 共享 skill(查看)' : 'Hermes skill(查看)';
|
||||
var alias = String(profile.alias || profile.displayName || '').trim();
|
||||
options.push({
|
||||
value: 'hermes:' + profileId,
|
||||
group: 'hermes',
|
||||
profile: profileId,
|
||||
label: alias && alias !== label ? label + ' · ' + alias : label,
|
||||
readonly: true
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
function pageAiProfileIsChatOnlySkillSource(profile) {
|
||||
var profileId = String(pageAiProfileValue(profile) || '').trim().toLowerCase();
|
||||
var baseProfile = String(profile && profile.baseProfile || '').trim().toLowerCase();
|
||||
var label = String(profile && (profile.displayName || profile.alias || profile.name) || '').trim().toLowerCase();
|
||||
var providerKind = String(profile && profile.providerKind || '').trim().toLowerCase();
|
||||
return profileId.indexOf('chat') >= 0
|
||||
|| baseProfile.indexOf('chat') >= 0
|
||||
|| label.indexOf('chat') >= 0
|
||||
|| providerKind.indexOf('chat') >= 0
|
||||
|| profileId === 'shared_lite'
|
||||
|| baseProfile === 'lite';
|
||||
}
|
||||
|
||||
function pageAiDefaultSkillSource() {
|
||||
return 'mnote';
|
||||
}
|
||||
|
||||
function pageAiNormalizeSkillSource(source) {
|
||||
var value = String(source || '').trim();
|
||||
var options = pageAiSkillSourceOptions();
|
||||
if (options.some(function(option) { return option.value === value; })) return value;
|
||||
if (value === 'hermes') return 'hermes:' + pageAiCurrentProfile();
|
||||
if (value === 'mnote_builtin') return 'mnote';
|
||||
var fallback = pageAiDefaultSkillSource();
|
||||
if (options.some(function(option) { return option.value === fallback; })) return fallback;
|
||||
return options.length ? options[0].value : 'mnote';
|
||||
}
|
||||
|
||||
function pageAiCurrentSkillSource() {
|
||||
var normalized = pageAiNormalizeSkillSource(pageUiState.pageAiActiveSkillSource);
|
||||
pageUiState.pageAiActiveSkillSource = normalized;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function pageAiSetSkillSource(source) {
|
||||
var normalized = pageAiNormalizeSkillSource(source);
|
||||
pageUiState.pageAiActiveSkillSource = normalized;
|
||||
pageAiPersistAiPreference('skills.active_source', normalized);
|
||||
pageUiState.pageAiSkills = { categories: [], archived: [] };
|
||||
pageUiState.pageAiSkillError = '';
|
||||
void pageAiLoadSkills();
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
function pageAiSkillSourceParts(source) {
|
||||
var normalized = pageAiNormalizeSkillSource(source);
|
||||
if (normalized.indexOf('hermes:') === 0) {
|
||||
return { group: 'hermes', profile: normalized.slice('hermes:'.length), source: normalized };
|
||||
}
|
||||
if (normalized === 'reasonix') return { group: 'reasonix', profile: '', source: normalized };
|
||||
return { group: 'mnote', profile: '', source: 'mnote' };
|
||||
}
|
||||
|
||||
function pageAiCurrentSkillSourceLabel() {
|
||||
var source = pageAiCurrentSkillSource();
|
||||
var option = pageAiSkillSourceOptions().find(function(item) { return item.value === source; });
|
||||
return option ? option.label : source;
|
||||
}
|
||||
|
||||
function pageAiSkillOriginLabel(skill) {
|
||||
var origin = String(skill && skill.origin || '').trim();
|
||||
if (origin === 'generated' || String(skill && skill.createdBy || '') === 'agent') return '生成';
|
||||
if (origin === 'installed') return '安装';
|
||||
if (origin === 'builtin') return '内置';
|
||||
if (origin === 'copied') return '本地';
|
||||
var source = String(skill && skill.source || '').trim();
|
||||
if (source === 'hub') return '安装';
|
||||
if (source === 'builtin') return '内置';
|
||||
if (source === 'reasonix') {
|
||||
if (origin === 'project') return 'Reasonix 项目';
|
||||
if (origin === 'global') return 'Reasonix 全局';
|
||||
return 'Reasonix';
|
||||
}
|
||||
return '本地';
|
||||
}
|
||||
|
||||
function pageAiSkillPreferenceKey(group, profile) {
|
||||
var groupName = String(group || '').trim();
|
||||
if (groupName === 'mnote') return 'ai.agent.mnote_builtin.skills.enabled';
|
||||
if (groupName === 'reasonix') return 'ai.agent.reasonix.skills.enabled';
|
||||
if (groupName === 'hermes') {
|
||||
var nextProfile = String(profile || pageAiCurrentProfile() || 'default').trim() || 'default';
|
||||
return 'ai.agent.hermes.profile.' + nextProfile + '.skills.enabled';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function pageAiSkillPreferenceTable(group, profile) {
|
||||
var key = pageAiSkillPreferenceKey(group, profile);
|
||||
var preferences = pageUiState.pageAiSkillPreferences || {};
|
||||
var value = preferences[key];
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
||||
return {};
|
||||
}
|
||||
|
||||
function pageAiHermesHideBuiltinPreferenceKey(profile) {
|
||||
return 'ai.agent.hermes.skills.hide_builtin';
|
||||
}
|
||||
|
||||
function pageAiHideHermesBuiltinSkills(profile) {
|
||||
var preferences = pageUiState.pageAiSkillPreferences || {};
|
||||
return preferences[pageAiHermesHideBuiltinPreferenceKey(profile)] === true;
|
||||
}
|
||||
|
||||
function pageAiReasonixMemoryEnabled() {
|
||||
var preferences = pageUiState.pageAiSkillPreferences || {};
|
||||
return preferences['ai.agent.reasonix.memory_enabled'] === true;
|
||||
}
|
||||
|
||||
function pageAiSetReasonixMemoryEnabled(enabled) {
|
||||
var preferences = Object.assign({}, pageUiState.pageAiSkillPreferences || {});
|
||||
preferences['ai.agent.reasonix.memory_enabled'] = Boolean(enabled);
|
||||
pageUiState.pageAiSkillPreferences = preferences;
|
||||
pageAiPersistRawAiPreference('ai.agent.reasonix.memory_enabled', Boolean(enabled));
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
function pageAiSetHideHermesBuiltinSkills(enabled, profile) {
|
||||
var key = pageAiHermesHideBuiltinPreferenceKey(profile);
|
||||
var preferences = Object.assign({}, pageUiState.pageAiSkillPreferences || {});
|
||||
preferences[key] = Boolean(enabled);
|
||||
pageUiState.pageAiSkillPreferences = preferences;
|
||||
pageAiPersistRawAiPreference(key, Boolean(enabled));
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
function pageAiSkillIsBuiltin(skill) {
|
||||
var origin = String(skill && skill.origin || '').trim();
|
||||
var source = String(skill && skill.source || '').trim();
|
||||
return origin === 'builtin' || source === 'builtin';
|
||||
}
|
||||
|
||||
function pageAiToggleableSkillEntries(group, profile) {
|
||||
var catalogKey = group === 'hermes' && profile ? 'hermes:' + profile : group;
|
||||
var catalog = pageUiState.pageAiSkillCatalogs && pageUiState.pageAiSkillCatalogs[catalogKey]
|
||||
? pageUiState.pageAiSkillCatalogs[catalogKey]
|
||||
: pageUiState.pageAiSkillCatalogs && pageUiState.pageAiSkillCatalogs[group]
|
||||
? pageUiState.pageAiSkillCatalogs[group]
|
||||
: { categories: [], archived: [] };
|
||||
var overrides = pageAiSkillPreferenceTable(group, profile);
|
||||
var result = [];
|
||||
pageAiNormalizeArray(catalog.categories).forEach(function(category) {
|
||||
pageAiNormalizeArray(category.skills).forEach(function(skill) {
|
||||
var id = String(skill.id || skill.name || '').trim();
|
||||
if (!id) return;
|
||||
result.push({
|
||||
group: group,
|
||||
profile: profile || '',
|
||||
category: category.name,
|
||||
categoryTitle: skill.categoryTitle || category.title || category.name || '',
|
||||
id: id,
|
||||
name: skill.name || id,
|
||||
title: skill.title || skill.name || id,
|
||||
description: skill.description || '',
|
||||
enabled: group === 'mnote' ? (skill.enabled !== false && overrides[id] !== false) : skill.enabled !== false,
|
||||
toggleable: group === 'mnote' && skill.toggleable !== false,
|
||||
source: skill.source || group,
|
||||
origin: skill.origin || '',
|
||||
readOnly: group !== 'mnote' || skill.readOnly === true || skill.readonly === true || skill.configurable === false,
|
||||
builtin: pageAiSkillIsBuiltin(skill),
|
||||
configurable: skill.configurable !== false,
|
||||
configScope: skill.configScope || '',
|
||||
skillKind: skill.skillKind || '',
|
||||
profileId: skill.profileId || '',
|
||||
toolNames: skill.toolNames || [],
|
||||
tools: skill.tools || [],
|
||||
toolCount: Number(skill.toolCount || 0),
|
||||
disabledToolCount: Number(skill.disabledToolCount || 0),
|
||||
status: skill.status || '',
|
||||
capabilityId: skill.capabilityId || '',
|
||||
capabilityKind: skill.capabilityKind || '',
|
||||
uiKind: skill.uiKind || '',
|
||||
requiresContextRefs: skill.requiresContextRefs || []
|
||||
});
|
||||
});
|
||||
});
|
||||
pageAiNormalizeArray(catalog.archived).forEach(function(skill) {
|
||||
var id = String(skill.id || skill.name || '').trim();
|
||||
if (!id) return;
|
||||
result.push({
|
||||
group: group,
|
||||
profile: profile || '',
|
||||
category: 'archived',
|
||||
categoryTitle: skill.categoryTitle || 'archived',
|
||||
id: id,
|
||||
name: skill.name || id,
|
||||
title: skill.title || skill.name || id,
|
||||
description: skill.description || '',
|
||||
enabled: group === 'mnote' ? (skill.enabled !== false && overrides[id] !== false) : skill.enabled !== false,
|
||||
toggleable: group === 'mnote' && skill.toggleable !== false,
|
||||
source: skill.source || group,
|
||||
origin: skill.origin || '',
|
||||
readOnly: group !== 'mnote' || skill.readOnly === true || skill.readonly === true || skill.configurable === false,
|
||||
builtin: pageAiSkillIsBuiltin(skill),
|
||||
configurable: skill.configurable !== false,
|
||||
configScope: skill.configScope || '',
|
||||
skillKind: skill.skillKind || '',
|
||||
profileId: skill.profileId || '',
|
||||
toolNames: skill.toolNames || [],
|
||||
tools: skill.tools || [],
|
||||
toolCount: Number(skill.toolCount || 0),
|
||||
disabledToolCount: Number(skill.disabledToolCount || 0),
|
||||
status: skill.status || '',
|
||||
capabilityId: skill.capabilityId || '',
|
||||
capabilityKind: skill.capabilityKind || '',
|
||||
uiKind: skill.uiKind || '',
|
||||
requiresContextRefs: skill.requiresContextRefs || []
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function pageAiAllSkillEntries() {
|
||||
return []
|
||||
.concat(pageAiToggleableSkillEntries('mnote', ''))
|
||||
.concat(pageAiToggleableSkillEntries('reasonix', ''))
|
||||
.concat(pageAiToggleableSkillEntries('hermes', pageAiCurrentProfile()));
|
||||
}
|
||||
|
||||
function pageAiSkillGroupCollapsed(group) {
|
||||
var table = pageUiState.pageAiCollapsedSkillGroups || {};
|
||||
return table[String(group || '').trim()] === true;
|
||||
}
|
||||
|
||||
function pageAiToggleSkillGroup(group) {
|
||||
var normalized = String(group || '').trim();
|
||||
if (!normalized) return;
|
||||
var table = Object.assign({}, pageUiState.pageAiCollapsedSkillGroups || {});
|
||||
table[normalized] = table[normalized] !== true;
|
||||
pageUiState.pageAiCollapsedSkillGroups = table;
|
||||
pageAiPersistRawAiPreference('ai.common.skills.groups.collapsed', table);
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
function pageAiSetSkillPreference(group, skillId, enabled, profile) {
|
||||
var key = pageAiSkillPreferenceKey(group, profile);
|
||||
if (!key || !skillId) return;
|
||||
var preferences = Object.assign({}, pageUiState.pageAiSkillPreferences || {});
|
||||
var current = preferences[key] && typeof preferences[key] === 'object' && !Array.isArray(preferences[key])
|
||||
? Object.assign({}, preferences[key])
|
||||
: {};
|
||||
current[skillId] = Boolean(enabled);
|
||||
preferences[key] = current;
|
||||
pageUiState.pageAiSkillPreferences = preferences;
|
||||
pageAiPersistRawAiPreference(key, current);
|
||||
}
|
||||
|
||||
function pageAiSkillEnabled(skill) {
|
||||
return skill.enabled !== false;
|
||||
}
|
||||
|
||||
return {
|
||||
pageAiSkillSourceOptions,
|
||||
pageAiDefaultSkillSource,
|
||||
pageAiNormalizeSkillSource,
|
||||
pageAiCurrentSkillSource,
|
||||
pageAiSetSkillSource,
|
||||
pageAiSkillSourceParts,
|
||||
pageAiCurrentSkillSourceLabel,
|
||||
pageAiSkillOriginLabel,
|
||||
pageAiSkillPreferenceKey,
|
||||
pageAiSkillPreferenceTable,
|
||||
pageAiHermesHideBuiltinPreferenceKey,
|
||||
pageAiHideHermesBuiltinSkills,
|
||||
pageAiReasonixMemoryEnabled,
|
||||
pageAiSetReasonixMemoryEnabled,
|
||||
pageAiSetHideHermesBuiltinSkills,
|
||||
pageAiSkillIsBuiltin,
|
||||
pageAiToggleableSkillEntries,
|
||||
pageAiAllSkillEntries,
|
||||
pageAiSkillGroupCollapsed,
|
||||
pageAiToggleSkillGroup,
|
||||
pageAiSetSkillPreference,
|
||||
pageAiSkillEnabled
|
||||
};
|
||||
}
|
||||
@@ -1,773 +0,0 @@
|
||||
export function createSidebarPageAiTargetRuntime(context) {
|
||||
const {
|
||||
currentDocumentId,
|
||||
currentRootUri,
|
||||
currentSourceKind,
|
||||
currentPageOptions,
|
||||
documentRef,
|
||||
escapeHtml,
|
||||
pageAiEnsureContextRefState,
|
||||
pageUiState,
|
||||
resolveWorkspaceId,
|
||||
searchText,
|
||||
pageAiNormalizeArray,
|
||||
} = context;
|
||||
|
||||
function pageAiCloneJson(value) {
|
||||
if (value == null) return null;
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function localMarkdownRelativePathFromPageAiDocumentId(documentId) {
|
||||
var value = String(documentId || '').trim();
|
||||
if (!value.startsWith('local-md:')) return '';
|
||||
return value.slice('local-md:'.length).replace(/~2F/g, '/');
|
||||
}
|
||||
|
||||
function localMarkdownDocumentIdFromPageAiRelativePath(relativePath) {
|
||||
var normalized = String(relativePath || '').trim().replace(/\\/g, '/').replace(/^\/+/, '');
|
||||
if (!normalized) return '';
|
||||
return 'local-md:' + normalized.split('/').map(function(segment) {
|
||||
return encodeURIComponent(segment).replace(/%20/g, '~20');
|
||||
}).join('~2F');
|
||||
}
|
||||
|
||||
function pageAiWorkspacePathForDocument(documentId, seed) {
|
||||
var relativePath = String(seed && seed.relativePath || localMarkdownRelativePathFromPageAiDocumentId(documentId) || '').trim().replace(/\\/g, '/').replace(/^\/+/, '');
|
||||
var resolvedDocumentId = String(documentId || seed && seed.documentId || '').trim()
|
||||
|| localMarkdownDocumentIdFromPageAiRelativePath(relativePath);
|
||||
return {
|
||||
schema: 'mnote.workspace_path.v1',
|
||||
workspaceId: String(seed && seed.workspaceId || resolveWorkspaceId(documentRef.body) || '').trim(),
|
||||
sourceKind: String(seed && seed.sourceKind || currentSourceKind() || '').trim(),
|
||||
rootUri: String(seed && seed.rootUri || currentRootUri() || '').trim(),
|
||||
relativePath: relativePath,
|
||||
documentId: resolvedDocumentId,
|
||||
objectIdentity: seed && seed.objectIdentity && typeof seed.objectIdentity === 'object'
|
||||
? seed.objectIdentity
|
||||
: String(seed && seed.objectIdentity || resolvedDocumentId || '').trim(),
|
||||
assetId: String(seed && seed.assetId || '').trim(),
|
||||
resourceKind: String(seed && seed.resourceKind || 'markdown_page').trim()
|
||||
};
|
||||
}
|
||||
|
||||
function pageAiResourceKindForTarget(entry) {
|
||||
var kind = String(entry && (entry.editorKind || entry.kind) || '').trim().toLowerCase();
|
||||
var assetId = String(entry && entry.assetId || '').trim();
|
||||
var path = String(entry && entry.path || '').trim().toLowerCase();
|
||||
var workspacePath = entry && entry.workspacePath && typeof entry.workspacePath === 'object' ? entry.workspacePath : {};
|
||||
var workspaceResourceKind = String(workspacePath.resourceKind || '').trim().toLowerCase();
|
||||
var officeOpenMode = String(entry && entry.officeOpenMode || '').trim().toLowerCase();
|
||||
var onlyofficeSessionId = String(entry && (entry.onlyofficeSessionId || entry.bridgeSessionId) || '').trim();
|
||||
if (kind === 'page' || kind === 'markdown_page') return 'markdown_page';
|
||||
if (kind === 'mindmap' || path.endsWith('.mindmap.json')) return 'mindmap';
|
||||
if (kind === 'office' || kind === 'onlyoffice' || kind === 'only_office' || workspaceResourceKind === 'only_office' || /\.(doc|docx|ppt|pptx|xls|xlsx)$/.test(path)) {
|
||||
return officeOpenMode === 'onlyoffice_live' || onlyofficeSessionId ? 'only_office' : 'attachment';
|
||||
}
|
||||
if (kind === 'resource' && assetId) return 'resource';
|
||||
return kind || 'markdown_page';
|
||||
}
|
||||
|
||||
function pageAiIsOnlyOfficeLiveTarget(editorTarget) {
|
||||
var workspacePath = editorTarget && editorTarget.workspacePath && typeof editorTarget.workspacePath === 'object' ? editorTarget.workspacePath : {};
|
||||
var resourceKind = String(editorTarget && (editorTarget.resourceKind || editorTarget.editorKind) || workspacePath.resourceKind || '').trim().toLowerCase();
|
||||
var officeOpenMode = String(editorTarget && editorTarget.officeOpenMode || '').trim().toLowerCase();
|
||||
return resourceKind === 'only_office' || resourceKind === 'onlyoffice' || officeOpenMode === 'onlyoffice_live';
|
||||
}
|
||||
|
||||
function pageAiTargetId(entry) {
|
||||
if (!entry || typeof entry !== 'object') return '';
|
||||
var workspacePath = entry.workspacePath && typeof entry.workspacePath === 'object' ? entry.workspacePath : {};
|
||||
var entryIdentity = typeof entry.objectIdentity === 'string' ? entry.objectIdentity : '';
|
||||
var workspaceIdentity = typeof workspacePath.objectIdentity === 'string' ? workspacePath.objectIdentity : '';
|
||||
return String(entryIdentity || workspaceIdentity || entry.documentId || entry.assetId || entry.path || '').trim();
|
||||
}
|
||||
|
||||
function pageAiWorkspacePathForTarget(entry) {
|
||||
var seed = Object.assign({}, entry && entry.workspacePath && typeof entry.workspacePath === 'object' ? entry.workspacePath : {});
|
||||
var resourceKind = pageAiResourceKindForTarget(entry);
|
||||
if (!seed.relativePath && entry && entry.path) seed.relativePath = entry.path;
|
||||
if (!seed.assetId && entry && entry.assetId) seed.assetId = entry.assetId;
|
||||
var seedResourceKind = String(seed.resourceKind || '').trim();
|
||||
if (!seedResourceKind || seedResourceKind === 'page' || seedResourceKind === 'office') seed.resourceKind = resourceKind;
|
||||
if (!seed.objectIdentity && entry && entry.objectIdentity) seed.objectIdentity = entry.objectIdentity;
|
||||
if (!seed.workspaceId && entry && entry.workspaceId) seed.workspaceId = entry.workspaceId;
|
||||
return pageAiWorkspacePathForDocument(entry && entry.documentId || currentDocumentId(), seed);
|
||||
}
|
||||
|
||||
function pageAiTargetFromOpenEditor(entry, source) {
|
||||
if (!entry || typeof entry !== 'object') return null;
|
||||
var workspacePath = pageAiWorkspacePathForTarget(entry);
|
||||
var targetId = pageAiTargetId(entry) || workspacePath.objectIdentity || workspacePath.documentId;
|
||||
if (!targetId) return null;
|
||||
var objectIdentity = typeof entry.objectIdentity === 'string'
|
||||
? entry.objectIdentity
|
||||
: (typeof workspacePath.objectIdentity === 'string' ? workspacePath.objectIdentity : targetId);
|
||||
return {
|
||||
schema: 'mnote.ai_editor_target.v1',
|
||||
source: source || 'open_editors_snapshot',
|
||||
targetId: targetId,
|
||||
objectIdentity: objectIdentity,
|
||||
workspacePath: workspacePath,
|
||||
paneRole: entry.paneRole || 'primary',
|
||||
documentId: entry.documentId || workspacePath.documentId,
|
||||
workspaceId: entry.workspaceId || workspacePath.workspaceId || resolveWorkspaceId(documentRef.body),
|
||||
editorKind: entry.editorKind || entry.kind || workspacePath.resourceKind,
|
||||
resourceKind: workspacePath.resourceKind,
|
||||
title: entry.title || '',
|
||||
active: entry.active === true,
|
||||
dirtyState: entry.dirtyState || '',
|
||||
preview: entry.preview === true,
|
||||
pinned: entry.pinned === true,
|
||||
lastActiveAt: entry.lastActiveAt || 0,
|
||||
assetId: entry.assetId || workspacePath.assetId || '',
|
||||
path: entry.path || workspacePath.relativePath || '',
|
||||
officeOpenMode: entry.officeOpenMode || '',
|
||||
onlyofficeSessionId: entry.onlyofficeSessionId || entry.bridgeSessionId || '',
|
||||
bridgeSessionId: entry.bridgeSessionId || entry.onlyofficeSessionId || '',
|
||||
bridgeSessionReady: entry.bridgeSessionReady === true
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeOpenEditorEntry(entry) {
|
||||
if (!entry || typeof entry !== 'object') return null;
|
||||
return {
|
||||
objectIdentity: String(entry.objectIdentity || '').trim(),
|
||||
workspacePath: entry.workspacePath && typeof entry.workspacePath === 'object' ? entry.workspacePath : null,
|
||||
paneRole: String(entry.paneRole || 'primary').trim() === 'secondary' ? 'secondary' : 'primary',
|
||||
documentId: String(entry.documentId || '').trim(),
|
||||
workspaceId: String(entry.workspaceId || '').trim(),
|
||||
title: String(entry.title || '').trim(),
|
||||
kind: String(entry.kind || entry.editorKind || '').trim(),
|
||||
editorKind: String(entry.editorKind || entry.kind || '').trim(),
|
||||
active: entry.active === true,
|
||||
dirtyState: String(entry.dirtyState || entry.dirtyGuard || '').trim(),
|
||||
preview: entry.preview === true,
|
||||
pinned: entry.pinned === true,
|
||||
lastActiveAt: Number(entry.lastActiveAt || 0) || 0,
|
||||
assetId: String(entry.assetId || '').trim(),
|
||||
path: String(entry.path || '').trim(),
|
||||
officeOpenMode: String(entry.officeOpenMode || '').trim(),
|
||||
onlyofficeSessionId: String(entry.onlyofficeSessionId || entry.bridgeSessionId || '').trim(),
|
||||
bridgeSessionId: String(entry.bridgeSessionId || entry.onlyofficeSessionId || '').trim(),
|
||||
bridgeSessionReady: entry.bridgeSessionReady === true
|
||||
};
|
||||
}
|
||||
|
||||
function currentPageAiOpenEditorsSnapshot() {
|
||||
var snapshot = null;
|
||||
try {
|
||||
if (window.__mnoteDocumentPaneRuntime && typeof window.__mnoteDocumentPaneRuntime.getOpenEditorsSnapshot === 'function') {
|
||||
snapshot = window.__mnoteDocumentPaneRuntime.getOpenEditorsSnapshot();
|
||||
}
|
||||
} catch (_) {}
|
||||
if (!snapshot || typeof snapshot !== 'object') snapshot = window.__mnoteOpenEditorsSnapshot || null;
|
||||
if (!snapshot || typeof snapshot !== 'object') return null;
|
||||
var editors = Array.isArray(snapshot.editors)
|
||||
? snapshot.editors.map(normalizeOpenEditorEntry).filter(Boolean)
|
||||
: [];
|
||||
var resources = Array.isArray(snapshot.resourceEditors)
|
||||
? snapshot.resourceEditors.map(normalizeOpenEditorEntry).filter(Boolean)
|
||||
: editors.filter(function(entry) { return entry.kind !== 'page'; });
|
||||
var normalizeGroup = function(group, paneRole) {
|
||||
var groupEditors = group && Array.isArray(group.editors)
|
||||
? group.editors.map(normalizeOpenEditorEntry).filter(Boolean)
|
||||
: editors.filter(function(entry) { return entry.paneRole === paneRole; });
|
||||
var groupResources = group && Array.isArray(group.resourceEditors)
|
||||
? group.resourceEditors.map(normalizeOpenEditorEntry).filter(Boolean)
|
||||
: resources.filter(function(entry) { return entry.paneRole === paneRole; });
|
||||
var groupActive = groupEditors.find(function(entry) { return entry.active; }) || null;
|
||||
return {
|
||||
paneRole: paneRole,
|
||||
activeObjectIdentity: String(group && group.activeObjectIdentity || (groupActive ? groupActive.objectIdentity : '') || '').trim(),
|
||||
editors: groupEditors,
|
||||
resourceEditors: groupResources
|
||||
};
|
||||
};
|
||||
var groups = {
|
||||
primary: normalizeGroup(snapshot.groups && snapshot.groups.primary, 'primary'),
|
||||
secondary: normalizeGroup(snapshot.groups && snapshot.groups.secondary, 'secondary')
|
||||
};
|
||||
var activeObjectIdentity = String(snapshot.activeObjectIdentity || '').trim();
|
||||
var allTargets = editors.concat(resources);
|
||||
var activeEditor = allTargets.find(function(entry) {
|
||||
return entry.active && (!activeObjectIdentity || entry.objectIdentity === activeObjectIdentity);
|
||||
}) || groups.primary.editors.find(function(entry) {
|
||||
return entry.active;
|
||||
}) || groups.primary.resourceEditors.find(function(entry) {
|
||||
return entry.active;
|
||||
}) || groups.secondary.editors.find(function(entry) {
|
||||
return entry.active;
|
||||
}) || groups.secondary.resourceEditors.find(function(entry) {
|
||||
return entry.active;
|
||||
}) || null;
|
||||
return {
|
||||
schema: String(snapshot.schema || 'mnote.open_editors_snapshot.v1'),
|
||||
generatedAt: Number(snapshot.generatedAt || 0) || Date.now(),
|
||||
activeObjectIdentity: activeObjectIdentity || (activeEditor ? activeEditor.objectIdentity : ''),
|
||||
activeEditor: activeEditor,
|
||||
editors: editors,
|
||||
resourceEditors: resources,
|
||||
groups: groups
|
||||
};
|
||||
}
|
||||
|
||||
function pageAiFallbackEditorTarget() {
|
||||
var fallbackDocumentId = currentDocumentId();
|
||||
var fallbackWorkspacePath = pageAiWorkspacePathForDocument(fallbackDocumentId, null);
|
||||
var fallbackTargetId = fallbackWorkspacePath.objectIdentity || fallbackDocumentId || '';
|
||||
return {
|
||||
schema: 'mnote.ai_editor_target.v1',
|
||||
source: 'fallback_current_document',
|
||||
targetId: fallbackTargetId,
|
||||
objectIdentity: fallbackTargetId,
|
||||
workspacePath: fallbackWorkspacePath,
|
||||
paneRole: 'primary',
|
||||
documentId: fallbackDocumentId,
|
||||
workspaceId: resolveWorkspaceId(documentRef.body),
|
||||
editorKind: 'page',
|
||||
active: true,
|
||||
dirtyState: '',
|
||||
preview: false,
|
||||
pinned: true,
|
||||
lastActiveAt: Date.now(),
|
||||
assetId: '',
|
||||
path: ''
|
||||
};
|
||||
}
|
||||
|
||||
function pageAiEditorTargetCandidates() {
|
||||
var snapshot = currentPageAiOpenEditorsSnapshot();
|
||||
var entries = [];
|
||||
if (snapshot) {
|
||||
entries = pageAiNormalizeArray(snapshot.editors).concat(pageAiNormalizeArray(snapshot.resourceEditors));
|
||||
}
|
||||
var seen = {};
|
||||
var targets = entries.map(function(entry) {
|
||||
return pageAiTargetFromOpenEditor(entry, 'open_editors_snapshot');
|
||||
}).filter(function(target) {
|
||||
var id = String(target && target.targetId || '').trim();
|
||||
if (!id || seen[id]) return false;
|
||||
seen[id] = true;
|
||||
return true;
|
||||
});
|
||||
if (!targets.length) targets.push(pageAiFallbackEditorTarget());
|
||||
return targets;
|
||||
}
|
||||
|
||||
function currentPageAiEditorTarget() {
|
||||
var targets = pageAiEditorTargetCandidates();
|
||||
var selectedId = String(pageUiState.pageAiSelectedTargetId || '').trim();
|
||||
var selected = selectedId ? targets.find(function(target) { return target.targetId === selectedId; }) : null;
|
||||
return selected
|
||||
|| targets.find(function(target) { return target.active === true; })
|
||||
|| targets[0]
|
||||
|| pageAiFallbackEditorTarget();
|
||||
}
|
||||
|
||||
function currentPageAiPageEditorTarget() {
|
||||
var snapshot = currentPageAiOpenEditorsSnapshot();
|
||||
var documentId = currentDocumentId();
|
||||
var workspaceId = resolveWorkspaceId(documentRef.body);
|
||||
var sourceKind = currentSourceKind();
|
||||
var rootUri = currentRootUri();
|
||||
var relativePath = localMarkdownRelativePathFromPageAiDocumentId(documentId);
|
||||
var fallbackWorkspacePath = pageAiWorkspacePathForDocument(documentId, {
|
||||
relativePath: relativePath
|
||||
});
|
||||
var pageEditor = snapshot && Array.isArray(snapshot.editors)
|
||||
? snapshot.editors.find(function(entry) {
|
||||
return entry
|
||||
&& String(entry.editorKind || entry.kind || '') === 'page'
|
||||
&& String(entry.documentId || '').trim() === String(documentId || '').trim();
|
||||
})
|
||||
: null;
|
||||
var workspacePath = pageEditor && pageEditor.workspacePath
|
||||
? Object.assign({}, pageEditor.workspacePath, {
|
||||
workspaceId: workspaceId,
|
||||
sourceKind: sourceKind,
|
||||
rootUri: rootUri,
|
||||
relativePath: relativePath,
|
||||
documentId: documentId,
|
||||
resourceKind: pageEditor.workspacePath.resourceKind || 'markdown_page',
|
||||
objectIdentity: pageEditor.workspacePath.objectIdentity || fallbackWorkspacePath.objectIdentity
|
||||
})
|
||||
: fallbackWorkspacePath;
|
||||
var objectIdentity = String(pageEditor && pageEditor.objectIdentity || workspacePath.objectIdentity || documentId || '').trim();
|
||||
return {
|
||||
schema: 'mnote.ai_editor_target.v1',
|
||||
source: pageEditor ? 'current_page_from_open_editors_snapshot' : 'current_page_fallback',
|
||||
targetId: objectIdentity,
|
||||
objectIdentity: objectIdentity,
|
||||
workspacePath: Object.assign({}, workspacePath, { objectIdentity: objectIdentity }),
|
||||
paneRole: String(pageEditor && pageEditor.paneRole || 'primary').trim() === 'secondary' ? 'secondary' : 'primary',
|
||||
documentId: documentId,
|
||||
workspaceId: workspaceId,
|
||||
editorKind: 'page',
|
||||
active: pageEditor ? pageEditor.active === true : true,
|
||||
dirtyState: String(pageEditor && pageEditor.dirtyState || '').trim(),
|
||||
preview: pageEditor ? pageEditor.preview === true : false,
|
||||
pinned: true,
|
||||
lastActiveAt: Number(pageEditor && pageEditor.lastActiveAt || 0) || Date.now(),
|
||||
assetId: '',
|
||||
path: relativePath
|
||||
};
|
||||
}
|
||||
|
||||
function currentPageAiScopedEditorTarget() {
|
||||
var selected = pageAiEnsureContextRefState();
|
||||
return selected.active_editor ? currentPageAiEditorTarget() : currentPageAiPageEditorTarget();
|
||||
}
|
||||
|
||||
function pageAiSetRunTargetSnapshot(snapshot) {
|
||||
pageUiState.pageAiCurrentRunTargetSnapshot = snapshot || null;
|
||||
var target = snapshot && snapshot.editorTarget ? snapshot.editorTarget : null;
|
||||
var documentId = String(target && target.documentId || snapshot && snapshot.documentId || '').trim();
|
||||
var workspaceId = String(target && target.workspaceId || snapshot && snapshot.workspaceId || '').trim();
|
||||
var rootUri = String(target && target.workspacePath && target.workspacePath.rootUri || snapshot && snapshot.rootUri || '').trim();
|
||||
if (documentId) documentRef.documentElement.setAttribute('data-mnote-page-ai-run-target-document-id', documentId);
|
||||
if (workspaceId) documentRef.documentElement.setAttribute('data-mnote-page-ai-run-target-workspace-id', workspaceId);
|
||||
if (rootUri) documentRef.documentElement.setAttribute('data-mnote-page-ai-run-target-root-uri', rootUri);
|
||||
}
|
||||
|
||||
function assertPageAiTargetInCurrentWorkspace(editorTarget) {
|
||||
var target = editorTarget && typeof editorTarget === 'object' ? editorTarget : {};
|
||||
var workspacePath = target.workspacePath && typeof target.workspacePath === 'object' ? target.workspacePath : {};
|
||||
var targetSourceKind = String(workspacePath.sourceKind || '').trim();
|
||||
var currentKind = String(currentSourceKind() || '').trim();
|
||||
if (targetSourceKind && currentKind && targetSourceKind !== currentKind) {
|
||||
var sourceError = new Error('AI target 与当前页面 sourceKind 不一致,请重新选择当前工作区内的目标。');
|
||||
sourceError.code = 'page_ai_target_workspace_mismatch';
|
||||
throw sourceError;
|
||||
}
|
||||
var targetRootUri = String(workspacePath.rootUri || '').trim();
|
||||
var currentRoot = String(currentRootUri() || '').trim();
|
||||
if (targetRootUri && currentRoot && targetRootUri !== currentRoot) {
|
||||
var rootError = new Error('AI target 与当前本地工作区 rootUri 不一致,请重新选择当前工作区内的目标。');
|
||||
rootError.code = 'page_ai_target_workspace_mismatch';
|
||||
throw rootError;
|
||||
}
|
||||
var targetWorkspaceId = String(target.workspaceId || workspacePath.workspaceId || '').trim();
|
||||
var currentWorkspaceId = String(resolveWorkspaceId(documentRef.body) || '').trim();
|
||||
if (targetWorkspaceId && currentWorkspaceId && targetWorkspaceId !== currentWorkspaceId) {
|
||||
var workspaceError = new Error('AI target 与当前 workspaceId 不一致,请重新选择当前工作区内的目标。');
|
||||
workspaceError.code = 'page_ai_target_workspace_mismatch';
|
||||
throw workspaceError;
|
||||
}
|
||||
}
|
||||
|
||||
function pageAiBuildContextRefs(scopedContext, runTargetSnapshot) {
|
||||
var selected = pageAiEnsureContextRefState();
|
||||
var refs = [];
|
||||
var documentId = currentDocumentId();
|
||||
var rootUri = currentRootUri();
|
||||
var workspaceId = resolveWorkspaceId(documentRef.body);
|
||||
var editorTarget = scopedContext && scopedContext.editorTarget ? scopedContext.editorTarget : currentPageAiEditorTarget();
|
||||
if (selected.current_page) {
|
||||
refs.push({
|
||||
kind: 'current_page',
|
||||
documentId: documentId,
|
||||
rootUri: rootUri,
|
||||
workspaceId: workspaceId
|
||||
});
|
||||
}
|
||||
if (selected.selection && scopedContext && scopedContext.selectedText) {
|
||||
refs.push({
|
||||
kind: 'selection',
|
||||
documentId: documentId,
|
||||
rootUri: rootUri,
|
||||
selectedBlockId: scopedContext.selectedBlockId || ''
|
||||
});
|
||||
}
|
||||
if (selected.active_editor && editorTarget) {
|
||||
var workspacePath = editorTarget.workspacePath && typeof editorTarget.workspacePath === 'object' ? editorTarget.workspacePath : {};
|
||||
refs.push({
|
||||
kind: 'active_editor',
|
||||
documentId: editorTarget.documentId || documentId,
|
||||
workspaceId: editorTarget.workspaceId || workspacePath.workspaceId || workspaceId,
|
||||
rootUri: workspacePath.rootUri || rootUri,
|
||||
relativePath: workspacePath.relativePath || '',
|
||||
editorKind: editorTarget.editorKind || '',
|
||||
resourceKind: editorTarget.resourceKind || workspacePath.resourceKind || '',
|
||||
targetId: editorTarget.targetId || workspacePath.objectIdentity || '',
|
||||
objectIdentity: editorTarget.objectIdentity || workspacePath.objectIdentity || '',
|
||||
assetId: editorTarget.assetId || workspacePath.assetId || '',
|
||||
onlyofficeSessionId: editorTarget.onlyofficeSessionId || editorTarget.bridgeSessionId || '',
|
||||
bridgeSessionId: editorTarget.bridgeSessionId || editorTarget.onlyofficeSessionId || ''
|
||||
});
|
||||
}
|
||||
if (selected.file) {
|
||||
refs.push({
|
||||
kind: 'file',
|
||||
documentId: documentId,
|
||||
rootUri: rootUri,
|
||||
relativePath: localMarkdownRelativePathFromPageAiDocumentId(documentId)
|
||||
});
|
||||
}
|
||||
if (selected.folder) {
|
||||
refs.push({
|
||||
kind: 'folder',
|
||||
rootUri: rootUri,
|
||||
relativePath: ''
|
||||
});
|
||||
}
|
||||
if (selected.changed_files) {
|
||||
refs.push({
|
||||
kind: 'changed_files',
|
||||
rootUri: rootUri,
|
||||
sinceRunTargetSnapshot: runTargetSnapshot && runTargetSnapshot.frozenAt || null
|
||||
});
|
||||
}
|
||||
return refs.filter(function(ref) {
|
||||
return ref && String(ref.kind || '').trim();
|
||||
});
|
||||
}
|
||||
|
||||
function pageAiBuildAllowedRoots() {
|
||||
return pageAiNormalizeArray(pageUiState.pageAiAllowedRoots).map(function(root) {
|
||||
return {
|
||||
rootUri: root.rootUri,
|
||||
permission: root.permission === 'write' || root.permission === 'read_write' ? 'write' : 'read',
|
||||
recursive: root.recursive !== false,
|
||||
source: root.source === 'auto' || root.source === 'user' || root.source === 'admin'
|
||||
? 'sqlite_directory_grant'
|
||||
: (root.source || 'sqlite_directory_grant'),
|
||||
grantId: root.id || ''
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function pageAiBuildRunTargetSnapshot(scopedContext, prompt) {
|
||||
var aiContext = scopedContext && scopedContext.pageContext && scopedContext.pageContext.aiContext
|
||||
? scopedContext.pageContext.aiContext
|
||||
: {};
|
||||
var editorTarget = scopedContext && scopedContext.editorTarget ? scopedContext.editorTarget : currentPageAiEditorTarget();
|
||||
return {
|
||||
schema: 'mnote.page_ai_run_target_snapshot.v1',
|
||||
source: 'open_editors_snapshot',
|
||||
frozenAt: Date.now(),
|
||||
workspaceId: resolveWorkspaceId(documentRef.body),
|
||||
documentId: currentDocumentId(),
|
||||
sourceKind: currentSourceKind(),
|
||||
rootUri: currentRootUri(),
|
||||
contextScope: pageUiState.pageAiContextScope || 'page',
|
||||
promptPreview: searchText(prompt || '').slice(0, 160),
|
||||
editorTarget: pageAiCloneJson(editorTarget),
|
||||
activeEditorTarget: pageAiCloneJson(aiContext.activeEditorTarget || editorTarget),
|
||||
openEditorsSnapshot: pageAiCloneJson(aiContext.openEditorsSnapshot || currentPageAiOpenEditorsSnapshot())
|
||||
};
|
||||
}
|
||||
|
||||
function pageAiContextKindsFromRefs(contextRefs) {
|
||||
var kinds = {};
|
||||
pageAiNormalizeArray(contextRefs).forEach(function(ref) {
|
||||
var kind = String(ref && ref.kind || '').trim();
|
||||
if (kind) kinds[kind] = true;
|
||||
});
|
||||
return kinds;
|
||||
}
|
||||
|
||||
function pageAiPageContextForRefs(pageContext, contextRefs) {
|
||||
var cloned = pageAiCloneJson(pageContext) || {};
|
||||
var aiContext = cloned.aiContext && typeof cloned.aiContext === 'object' ? cloned.aiContext : {};
|
||||
var kinds = pageAiContextKindsFromRefs(contextRefs);
|
||||
delete cloned.documentBlocks;
|
||||
delete cloned.evidence;
|
||||
delete aiContext.contextBlocks;
|
||||
delete aiContext.pageText;
|
||||
delete aiContext.pageXml;
|
||||
delete aiContext.truncated;
|
||||
delete aiContext.warnings;
|
||||
if (!kinds.selection) {
|
||||
delete aiContext.selectedText;
|
||||
delete aiContext.selectedBlockIds;
|
||||
delete aiContext.selectedBlocks;
|
||||
delete aiContext.allowedTargetBlockIds;
|
||||
}
|
||||
cloned.aiContext = aiContext;
|
||||
return cloned;
|
||||
}
|
||||
|
||||
function pageAiBuildAgentTargetPackage(scopedContext, runTargetSnapshot) {
|
||||
var target = scopedContext && scopedContext.editorTarget ? scopedContext.editorTarget : currentPageAiEditorTarget();
|
||||
var workspacePath = target && target.workspacePath && typeof target.workspacePath === 'object'
|
||||
? pageAiWorkspacePathForDocument(target.documentId || currentDocumentId(), target.workspacePath)
|
||||
: pageAiWorkspacePathForDocument(target && target.documentId || currentDocumentId(), null);
|
||||
var relativePath = String(workspacePath.relativePath || '').trim();
|
||||
var allowedFiles = relativePath ? [relativePath] : [];
|
||||
var writable = pageAiBuildAllowedRoots().some(function(root) {
|
||||
return String(root && root.rootUri || '').trim() === String(workspacePath.rootUri || '').trim()
|
||||
&& String(root && root.permission || '').trim() === 'write';
|
||||
});
|
||||
var primaryTargetId = String(target && target.targetId || workspacePath.objectIdentity || workspacePath.documentId || '').trim();
|
||||
var onlyofficeSessionId = String(target && (target.onlyofficeSessionId || target.bridgeSessionId) || '').trim();
|
||||
var targetEntry = {
|
||||
targetId: primaryTargetId,
|
||||
objectIdentity: primaryTargetId,
|
||||
documentId: workspacePath.documentId,
|
||||
workspaceId: workspacePath.workspaceId,
|
||||
sourceKind: workspacePath.sourceKind,
|
||||
rootUri: workspacePath.rootUri,
|
||||
relativePath: relativePath,
|
||||
resourceKind: workspacePath.resourceKind,
|
||||
assetId: workspacePath.assetId || target && target.assetId || '',
|
||||
onlyofficeSessionId: onlyofficeSessionId,
|
||||
bridgeSessionId: onlyofficeSessionId,
|
||||
paneRole: target && target.paneRole || 'primary',
|
||||
title: target && target.title || '',
|
||||
policy: {
|
||||
permission: allowedFiles.length && writable ? 'read_write' : 'read',
|
||||
writeRequiresCleanBuffer: true,
|
||||
conflictPolicy: 'fail_on_dirty_or_stale'
|
||||
}
|
||||
};
|
||||
return {
|
||||
schema: 'mnote.agent_target_package.v1',
|
||||
source: 'page_ai_run_target_snapshot',
|
||||
frozenAt: runTargetSnapshot && runTargetSnapshot.frozenAt || Date.now(),
|
||||
primaryTargetId: primaryTargetId,
|
||||
onlyofficeSessionId: onlyofficeSessionId,
|
||||
bridgeSessionId: onlyofficeSessionId,
|
||||
workspaceId: workspacePath.workspaceId,
|
||||
sourceKind: workspacePath.sourceKind,
|
||||
rootUri: workspacePath.rootUri,
|
||||
documentId: workspacePath.documentId,
|
||||
objectIdentity: primaryTargetId,
|
||||
resourceKind: workspacePath.resourceKind,
|
||||
workspacePath: workspacePath,
|
||||
currentFile: relativePath ? {
|
||||
rootUri: workspacePath.rootUri,
|
||||
relativePath: relativePath,
|
||||
documentId: workspacePath.documentId,
|
||||
objectIdentity: primaryTargetId,
|
||||
resourceKind: workspacePath.resourceKind
|
||||
} : null,
|
||||
allowedFiles: allowedFiles,
|
||||
targets: [targetEntry],
|
||||
policy: {
|
||||
writeRequiresExplicitTarget: true,
|
||||
allowedFilesSource: 'selected_page_ai_target',
|
||||
conflictPolicy: 'fail_on_dirty_or_stale'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function pageAiBlockingDirtyState(dirtyState) {
|
||||
var state = String(dirtyState || '').trim();
|
||||
var normalized = state.toLowerCase();
|
||||
if (normalized === 'dirty') return 'Dirty';
|
||||
if (normalized === 'stale') return 'Stale';
|
||||
if (normalized === 'deleted') return 'Deleted';
|
||||
if (normalized === 'externalmodified' || normalized === 'external-change-conflict' || normalized === 'hasexternalconflict') return 'ExternalModified';
|
||||
return '';
|
||||
}
|
||||
|
||||
async function fetchPageAiTargetBufferState(editorTarget) {
|
||||
if (currentSourceKind() !== 'local_folder') return null;
|
||||
var target = editorTarget && typeof editorTarget === 'object' ? editorTarget : {};
|
||||
var workspacePath = target.workspacePath && typeof target.workspacePath === 'object' ? target.workspacePath : {};
|
||||
var resourceKind = String(target.resourceKind || target.editorKind || workspacePath.resourceKind || '').trim();
|
||||
if (resourceKind === 'office' || resourceKind === 'only_office' || resourceKind === 'onlyoffice') return null;
|
||||
var documentId = String(target.documentId || currentDocumentId() || '').trim();
|
||||
var rootUri = String(workspacePath.rootUri || currentRootUri() || '').trim();
|
||||
if (!documentId || !rootUri) return null;
|
||||
var relativePath = String(workspacePath.relativePath || '').trim()
|
||||
|| localMarkdownRelativePathFromPageAiDocumentId(documentId);
|
||||
var url = new URL('/api/documents/buffer-state', window.location.origin);
|
||||
url.searchParams.set('documentId', documentId);
|
||||
url.searchParams.set('sourceKind', 'local_folder');
|
||||
url.searchParams.set('rootUri', rootUri);
|
||||
var workspaceId = String(target.workspaceId || resolveWorkspaceId(documentRef.body) || '').trim();
|
||||
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
|
||||
if (relativePath) url.searchParams.set('relativePath', relativePath);
|
||||
try {
|
||||
var response = await fetch(url.toString(), { cache: 'no-store', headers: { accept: 'application/json' } });
|
||||
var payload = await response.json().catch(function() { return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) return null;
|
||||
return payload.result || null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function assertPageAiTargetWritable(editorTarget) {
|
||||
var workspacePath = editorTarget && editorTarget.workspacePath && typeof editorTarget.workspacePath === 'object' ? editorTarget.workspacePath : {};
|
||||
var resourceKind = String(editorTarget && (editorTarget.resourceKind || editorTarget.editorKind) || workspacePath.resourceKind || '').trim();
|
||||
var onlyofficeSessionId = String(editorTarget && (editorTarget.onlyofficeSessionId || editorTarget.bridgeSessionId) || '').trim();
|
||||
if (pageAiIsOnlyOfficeLiveTarget(editorTarget) && !onlyofficeSessionId) {
|
||||
var sessionError = new Error('ONLYOFFICE 资源仍在连接 MNote bridge,请等待 Office 页面加载完成后再让 AI 操作。');
|
||||
sessionError.code = 'page_ai_onlyoffice_bridge_not_ready';
|
||||
throw sessionError;
|
||||
}
|
||||
var snapshotState = pageAiBlockingDirtyState(editorTarget && editorTarget.dirtyState);
|
||||
var bufferState = await fetchPageAiTargetBufferState(editorTarget);
|
||||
var bufferDirtyState = pageAiBlockingDirtyState(bufferState && bufferState.dirtyState);
|
||||
var blockedState = bufferDirtyState || snapshotState;
|
||||
if (!blockedState) return bufferState;
|
||||
var documentId = String(editorTarget && editorTarget.documentId || currentDocumentId() || '').trim();
|
||||
var error = new Error('目标文档存在未保存或外部变更状态(' + blockedState + '),请先保存、解决冲突或刷新后再让 AI 写入。');
|
||||
error.code = 'page_ai_target_buffer_not_writable';
|
||||
error.documentId = documentId;
|
||||
error.dirtyState = blockedState;
|
||||
throw error;
|
||||
}
|
||||
|
||||
function currentPageAiSelectedText() {
|
||||
try {
|
||||
var selection = window.getSelection ? window.getSelection() : null;
|
||||
return selection ? searchText(selection.toString() || '') : '';
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function pageAiProjectionBlocks(aggregate) {
|
||||
var blocks = aggregate && aggregate.body && aggregate.body.blockDocument && aggregate.body.blockDocument.blocks;
|
||||
return Array.isArray(blocks) ? blocks : [];
|
||||
}
|
||||
|
||||
function pageAiBlockText(block) {
|
||||
return searchText(block && (block.text || block.title || block.content) || '');
|
||||
}
|
||||
|
||||
function pageAiSelectedBlockIdsFromSelection() {
|
||||
try {
|
||||
var selection = window.getSelection ? window.getSelection() : null;
|
||||
if (!selection || selection.rangeCount === 0 || searchText(selection.toString() || '') === '') return [];
|
||||
var range = selection.getRangeAt(0);
|
||||
var editor = documentRef.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
|
||||
if (!(editor instanceof HTMLElement)) return [];
|
||||
return Array.from(editor.children).filter(function(node) {
|
||||
if (!(node instanceof HTMLElement)) return false;
|
||||
try {
|
||||
return range.intersectsNode(node);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}).map(function(node) {
|
||||
return searchText(node.getAttribute('data-id') || node.id || '');
|
||||
}).filter(Boolean);
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function pageAiBlocksToPageXml(blocks, aggregate) {
|
||||
var revision = aggregate && aggregate.body ? String(aggregate.body.revision || '') : '';
|
||||
var pageId = currentDocumentId() || 'current-page';
|
||||
var lines = ['<page id="' + escapeHtml(pageId) + '" revision="' + escapeHtml(revision) + '">'];
|
||||
blocks.forEach(function(block) {
|
||||
var blockId = String(block && (block.blockId || block.id) || '');
|
||||
var type = String(block && block.type || 'paragraph');
|
||||
var revisionRef = String(block && block.revisionRef || '');
|
||||
var level = block && block.attrs && block.attrs.level ? ' level="' + escapeHtml(block.attrs.level) + '"' : '';
|
||||
lines.push(' <block id="' + escapeHtml(blockId) + '" type="' + escapeHtml(type) + '" revisionRef="' + escapeHtml(revisionRef) + '"' + level + '>' + escapeHtml(pageAiBlockText(block)) + '</block>');
|
||||
});
|
||||
lines.push('</page>');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function buildPageAiContext(contextSnapshot, scope, selectedText) {
|
||||
var aggregate = contextSnapshot.aggregate || {};
|
||||
var body = aggregate.body || {};
|
||||
var allBlocks = pageAiProjectionBlocks(aggregate);
|
||||
var selectedBlockIds = scope === 'selection' ? pageAiSelectedBlockIdsFromSelection() : [];
|
||||
var selectedSet = {};
|
||||
selectedBlockIds.forEach(function(id) { selectedSet[id] = true; });
|
||||
var selectedBlocks = selectedBlockIds.length
|
||||
? allBlocks.filter(function(block) { return selectedSet[String(block.blockId || block.id || '')]; })
|
||||
: [];
|
||||
var contextBlocks = selectedBlocks.length ? selectedBlocks : allBlocks.slice(0, 120);
|
||||
var truncated = !selectedBlocks.length && allBlocks.length > contextBlocks.length;
|
||||
return {
|
||||
schema: 'mnote.page_ai_context.v1',
|
||||
workspaceId: resolveWorkspaceId(documentRef.body),
|
||||
documentId: currentDocumentId(),
|
||||
activeEditorTarget: currentPageAiScopedEditorTarget(),
|
||||
openEditorsSnapshot: currentPageAiOpenEditorsSnapshot(),
|
||||
scope: scope,
|
||||
revision: body.revision || null,
|
||||
conflictDetectionKey: body.conflictDetectionKey || null,
|
||||
selectedText: selectedText || '',
|
||||
selectedBlockIds: selectedBlockIds,
|
||||
allowedTargetBlockIds: selectedBlockIds,
|
||||
selectedBlocks: selectedBlocks,
|
||||
contextBlocks: contextBlocks,
|
||||
pageText: contextBlocks.map(pageAiBlockText).filter(Boolean).join('\n'),
|
||||
pageXml: pageAiBlocksToPageXml(contextBlocks, aggregate),
|
||||
truncated: truncated,
|
||||
warnings: truncated ? [{ code: 'page_ai_context_truncated', message: '页面 AI context 已按前 120 个块裁剪' }] : []
|
||||
};
|
||||
}
|
||||
|
||||
function pageAiScopedPageContext(contextSnapshot) {
|
||||
var aggregate = contextSnapshot.aggregate || {};
|
||||
var body = contextSnapshot.body || {};
|
||||
var subtree = contextSnapshot.subtree || null;
|
||||
var outline = subtree && subtree.outline ? subtree.outline : null;
|
||||
var scope = pageUiState.pageAiContextScope || 'page';
|
||||
var title = aggregate.head && aggregate.head.title ? aggregate.head.title : '';
|
||||
var selectedText = scope === 'selection' ? currentPageAiSelectedText() : '';
|
||||
var aiContext = buildPageAiContext(contextSnapshot, scope, selectedText);
|
||||
var editorTarget = aiContext.activeEditorTarget || currentPageAiScopedEditorTarget();
|
||||
return {
|
||||
pageContext: {
|
||||
contextScope: scope,
|
||||
documentBlocks: null,
|
||||
node: {
|
||||
documentId: currentDocumentId(),
|
||||
title: title
|
||||
},
|
||||
subtree: null,
|
||||
outline: null,
|
||||
pageSubtreeSource: scope === 'page' ? (contextSnapshot.pageSubtreeSource || 'none') : 'scope:' + scope,
|
||||
evidence: selectedText ? [{ kind: 'selection', text: selectedText }] : null,
|
||||
pageOptions: scope === 'options' || scope === 'page' ? currentPageOptions() : null,
|
||||
contentAccess: 'mnote.context.read_current_page',
|
||||
aiContext: aiContext
|
||||
},
|
||||
editorTarget: editorTarget,
|
||||
selectedText: selectedText,
|
||||
selectedBlockId: aiContext.selectedBlockIds[0] || null
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
currentPageAiSelectedText,
|
||||
currentPageAiEditorTarget,
|
||||
currentPageAiPageEditorTarget,
|
||||
currentPageAiScopedEditorTarget,
|
||||
currentPageAiOpenEditorsSnapshot,
|
||||
pageAiBlockText,
|
||||
pageAiBlockingDirtyState,
|
||||
pageAiBlocksToPageXml,
|
||||
pageAiBuildAgentTargetPackage,
|
||||
pageAiBuildAllowedRoots,
|
||||
pageAiBuildContextRefs,
|
||||
pageAiBuildRunTargetSnapshot,
|
||||
pageAiCloneJson,
|
||||
pageAiContextKindsFromRefs,
|
||||
pageAiEditorTargetCandidates,
|
||||
pageAiFallbackEditorTarget,
|
||||
pageAiPageContextForRefs,
|
||||
pageAiProjectionBlocks,
|
||||
pageAiResourceKindForTarget,
|
||||
pageAiSelectedBlockIdsFromSelection,
|
||||
pageAiSetRunTargetSnapshot,
|
||||
pageAiScopedPageContext,
|
||||
pageAiTargetFromOpenEditor,
|
||||
pageAiTargetId,
|
||||
pageAiWorkspacePathForDocument,
|
||||
pageAiWorkspacePathForTarget,
|
||||
assertPageAiTargetInCurrentWorkspace,
|
||||
assertPageAiTargetWritable,
|
||||
buildPageAiContext,
|
||||
fetchPageAiTargetBufferState,
|
||||
localMarkdownDocumentIdFromPageAiRelativePath,
|
||||
localMarkdownRelativePathFromPageAiDocumentId,
|
||||
};
|
||||
}
|
||||
@@ -599,7 +599,6 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'<div class="mnote-extensions-subtabs" role="tablist" aria-label="扩展子面板">' +
|
||||
'<button type="button" class="mnote-extensions-subtab is-active" role="tab" aria-selected="true" data-extensions-subtab="knowledge">知识库</button>' +
|
||||
'<button type="button" class="mnote-extensions-subtab" role="tab" aria-selected="false" data-extensions-subtab="skills">Skills</button>' +
|
||||
'<button type="button" class="mnote-extensions-subtab" role="tab" aria-selected="false" data-extensions-subtab="mcp">MCP</button>' +
|
||||
'<button type="button" class="mnote-extensions-subtab" role="tab" aria-selected="false" data-extensions-subtab="tools">Agent 工具</button>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-extensions-subpanel" data-extensions-panel="knowledge">' +
|
||||
@@ -608,9 +607,6 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'<div class="mnote-extensions-subpanel" data-extensions-panel="skills" hidden>' +
|
||||
'<div class="mnote-extensions-status" data-extensions-status="skills"><span class="mnote-extensions-loading">加载中...</span></div>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-extensions-subpanel" data-extensions-panel="mcp" hidden>' +
|
||||
'<div class="mnote-extensions-status" data-extensions-status="mcp"><span class="mnote-extensions-loading">加载中...</span></div>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-extensions-subpanel" data-extensions-panel="tools" hidden>' +
|
||||
'<div class="mnote-extensions-status" data-extensions-status="tools"><span class="mnote-extensions-loading">加载中...</span></div>' +
|
||||
'</div>' +
|
||||
@@ -625,17 +621,12 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'<div class="mnote-dashboard-card" data-dashboard-card="skills">' +
|
||||
'<div class="mnote-dashboard-card-title">Skills</div>' +
|
||||
'<div class="mnote-dashboard-card-value" data-dashboard-value="skills">--</div>' +
|
||||
'<div class="mnote-dashboard-card-desc">已安装 / 可用</div>' +
|
||||
'<div class="mnote-dashboard-card-desc">工具清单中的 skill 能力</div>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-dashboard-card" data-dashboard-card="mcp">' +
|
||||
'<div class="mnote-dashboard-card-title">MCP</div>' +
|
||||
'<div class="mnote-dashboard-card-value" data-dashboard-value="mcp">--</div>' +
|
||||
'<div class="mnote-dashboard-card-desc">服务器 / 已连接</div>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-dashboard-card" data-dashboard-card="agent">' +
|
||||
'<div class="mnote-dashboard-card-title">Agent Runs</div>' +
|
||||
'<div class="mnote-dashboard-card-value" data-dashboard-value="agent">--</div>' +
|
||||
'<div class="mnote-dashboard-card-desc">最近 7 天运行次数</div>' +
|
||||
'<div class="mnote-dashboard-card" data-dashboard-card="tools">' +
|
||||
'<div class="mnote-dashboard-card-title">Agent 工具</div>' +
|
||||
'<div class="mnote-dashboard-card-value" data-dashboard-value="tools">--</div>' +
|
||||
'<div class="mnote-dashboard-card-desc">/api/mnote/tools/manifest</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="wolai-page-settings-index-add" style="margin-top:10px" data-dashboard-action="refresh">刷新仪表盘</button>' +
|
||||
@@ -3003,10 +2994,29 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
// 切换时懒加载对应面板数据
|
||||
if (subtabName === 'knowledge') refreshKnowledgeExtensionsPanel();
|
||||
else if (subtabName === 'skills') refreshSkillsExtensionsPanel();
|
||||
else if (subtabName === 'mcp') refreshMcpExtensionsPanel();
|
||||
else if (subtabName === 'tools') refreshToolsExtensionsPanel();
|
||||
}
|
||||
|
||||
async function fetchAgentToolsManifest() {
|
||||
var resp = await fetch('/api/mnote/tools/manifest');
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
var data = await resp.json();
|
||||
var tools = Array.isArray(data && data.tools) ? data.tools : [];
|
||||
var capabilities = data && data.capabilities && typeof data.capabilities === 'object'
|
||||
? data.capabilities
|
||||
: {};
|
||||
return { data: data, tools: tools, capabilities: capabilities };
|
||||
}
|
||||
|
||||
function isSkillRelatedTool(tool) {
|
||||
if (!tool || typeof tool !== 'object') return false;
|
||||
var name = String(tool.name || tool.function_name || '');
|
||||
if (/skill/i.test(name)) return true;
|
||||
var scopes = tool.capabilityScope || tool.capabilities || [];
|
||||
if (Array.isArray(scopes) && scopes.some(function(s) { return /skill/i.test(String(s)); })) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 当切换到扩展 tab 时刷新当前子面板
|
||||
function refreshExtensionsPanel() {
|
||||
var popover = ensurePageSettingsPopover();
|
||||
@@ -3051,26 +3061,30 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Skills 子面板 ---
|
||||
// --- Skills 子面板(来自 /api/mnote/tools/manifest,非历史 agent 网关) ---
|
||||
async function refreshSkillsExtensionsPanel() {
|
||||
var statusEl = document.querySelector('[data-extensions-status="skills"]');
|
||||
if (!statusEl) return;
|
||||
statusEl.innerHTML = '<span class="mnote-extensions-loading">加载中...</span>';
|
||||
try {
|
||||
var resp = await fetch('/api/page-ai/agent-descriptors');
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
var data = await resp.json();
|
||||
var descriptors = data.descriptors || data || [];
|
||||
// 从 agent descriptor 中收集 skills 信息
|
||||
var pack = await fetchAgentToolsManifest();
|
||||
var allSkills = [];
|
||||
descriptors.forEach(function(desc) {
|
||||
var caps = desc.capabilities || desc.capabilityStates || {};
|
||||
if (caps.skills && Array.isArray(caps.skills)) {
|
||||
caps.skills.forEach(function(s) { allSkills.push({ name: s, source: desc.name || desc.id || 'unknown' }); });
|
||||
}
|
||||
pack.tools.forEach(function(tool) {
|
||||
if (!isSkillRelatedTool(tool)) return;
|
||||
allSkills.push({
|
||||
name: tool.name || tool.function_name || 'skill',
|
||||
source: 'mnote-agent-tools'
|
||||
});
|
||||
});
|
||||
var caps = pack.capabilities;
|
||||
Object.keys(caps).forEach(function(key) {
|
||||
if (!/skill/i.test(key)) return;
|
||||
var entry = caps[key];
|
||||
var label = entry && (entry.name || entry.id) ? (entry.name || entry.id) : key;
|
||||
allSkills.push({ name: String(label), source: 'capability' });
|
||||
});
|
||||
if (!allSkills.length) {
|
||||
allSkills.push({ name: 'skill-read (内置)', source: 'hermes' });
|
||||
allSkills.push({ name: 'mnote.skill.read', source: 'builtin' });
|
||||
}
|
||||
var html = '';
|
||||
allSkills.forEach(function(skill) {
|
||||
@@ -3078,63 +3092,29 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'<div class="mnote-extensions-item-row"><span class="mnote-extensions-label">' + escapeHtml(skill.name) + '</span><span class="mnote-extensions-source">' + escapeHtml(skill.source) + '</span></div>' +
|
||||
'</div>';
|
||||
});
|
||||
if (!html) html = '<div class="mnote-extensions-item"><span>暂无已安装 Skills</span></div>';
|
||||
if (!html) html = '<div class="mnote-extensions-item"><span>暂无 Skills</span></div>';
|
||||
statusEl.innerHTML = html;
|
||||
} catch (err) {
|
||||
statusEl.innerHTML = '<div class="mnote-extensions-item"><span class="mnote-extensions-error">无法加载 Skills 信息</span></div>';
|
||||
}
|
||||
}
|
||||
|
||||
// --- MCP 子面板 ---
|
||||
async function refreshMcpExtensionsPanel() {
|
||||
var statusEl = document.querySelector('[data-extensions-status="mcp"]');
|
||||
if (!statusEl) return;
|
||||
statusEl.innerHTML = '<span class="mnote-extensions-loading">加载中...</span>';
|
||||
try {
|
||||
var resp = await fetch('/api/hermes/mcp/servers');
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
var servers = await resp.json();
|
||||
var list = Array.isArray(servers) ? servers : (servers.servers || []);
|
||||
var html = '';
|
||||
if (list.length === 0) {
|
||||
html = '<div class="mnote-extensions-item"><span>暂无已配置 MCP 服务器</span></div>';
|
||||
} else {
|
||||
list.forEach(function(srv) {
|
||||
var name = srv.name || srv.id || '未命名';
|
||||
var url = srv.url || srv.command || '';
|
||||
var connected = srv.connected ? '已连接' : '未连接';
|
||||
var connClass = srv.connected ? 'mnote-status-ready' : 'mnote-status-pending';
|
||||
html += '<div class="mnote-extensions-item">' +
|
||||
'<div class="mnote-extensions-item-row"><span class="mnote-extensions-label">' + escapeHtml(name) + '</span><span class="' + connClass + '">' + connected + '</span></div>' +
|
||||
(url ? '<div class="mnote-extensions-item-row"><span class="mnote-extensions-detail">' + escapeHtml(String(url)) + '</span></div>' : '') +
|
||||
'</div>';
|
||||
});
|
||||
}
|
||||
statusEl.innerHTML = html;
|
||||
} catch (err) {
|
||||
statusEl.innerHTML = '<div class="mnote-extensions-item"><span class="mnote-extensions-error">无法加载 MCP 配置</span></div>';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Agent 工具子面板 ---
|
||||
async function refreshToolsExtensionsPanel() {
|
||||
var statusEl = document.querySelector('[data-extensions-status="tools"]');
|
||||
if (!statusEl) return;
|
||||
statusEl.innerHTML = '<span class="mnote-extensions-loading">加载中...</span>';
|
||||
try {
|
||||
var resp = await fetch('/api/page-ai/agent-descriptors');
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
var data = await resp.json();
|
||||
var descriptors = data.descriptors || data || [];
|
||||
var pack = await fetchAgentToolsManifest();
|
||||
var html = '';
|
||||
descriptors.forEach(function(desc) {
|
||||
var tools = desc.tools || [];
|
||||
var name = desc.name || desc.id || 'unknown';
|
||||
if (!tools.length) return;
|
||||
if (!pack.tools.length) {
|
||||
html = '<div class="mnote-extensions-item"><span>暂无 Agent 工具信息</span></div>';
|
||||
} else {
|
||||
html += '<div class="mnote-extensions-item mnote-extensions-agent-group">' +
|
||||
'<div class="mnote-extensions-agent-name">' + escapeHtml(name) + '</div>';
|
||||
tools.forEach(function(tool) {
|
||||
'<div class="mnote-extensions-agent-name">mnote agent tools</div>';
|
||||
pack.tools.forEach(function(tool) {
|
||||
var toolName = tool.name || tool.function_name || '';
|
||||
if (!toolName) return;
|
||||
var disabled = tool.disabled ? ' (已禁用)' : '';
|
||||
html += '<div class="mnote-extensions-tool-item">' +
|
||||
'<span class="mnote-extensions-tool-name">' + escapeHtml(toolName) + '</span>' +
|
||||
@@ -3142,8 +3122,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
});
|
||||
if (!html) html = '<div class="mnote-extensions-item"><span>暂无 Agent 工具信息</span></div>';
|
||||
}
|
||||
statusEl.innerHTML = html;
|
||||
} catch (err) {
|
||||
statusEl.innerHTML = '<div class="mnote-extensions-item"><span class="mnote-extensions-error">无法加载 Agent 工具信息</span></div>';
|
||||
@@ -3371,14 +3350,9 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
// --- 仪表盘 ---
|
||||
function refreshDashboardPanel() {
|
||||
var popover = ensurePageSettingsPopover();
|
||||
// 知识库状态
|
||||
refreshDashboardKnowledgeCard(popover);
|
||||
// Skills
|
||||
refreshDashboardSkillsCard(popover);
|
||||
// MCP
|
||||
refreshDashboardMcpCard(popover);
|
||||
// Agent runs
|
||||
refreshDashboardAgentCard(popover);
|
||||
refreshDashboardToolsCard(popover);
|
||||
}
|
||||
|
||||
async function refreshDashboardKnowledgeCard(popover) {
|
||||
@@ -3408,49 +3382,22 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
if (!el) return;
|
||||
el.textContent = '加载中...';
|
||||
try {
|
||||
var resp = await fetch('/api/page-ai/agent-descriptors');
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
var data = await resp.json();
|
||||
var descriptors = data.descriptors || data || [];
|
||||
var count = 0;
|
||||
descriptors.forEach(function(d) {
|
||||
var caps = d.capabilities || d.capabilityStates || {};
|
||||
if (caps.skills && Array.isArray(caps.skills)) count += caps.skills.length;
|
||||
});
|
||||
if (!count) count = 1; // skill-read 内置
|
||||
var pack = await fetchAgentToolsManifest();
|
||||
var count = pack.tools.filter(isSkillRelatedTool).length;
|
||||
if (!count) count = 1; // mnote.skill.read 内置
|
||||
el.textContent = count + ' 个';
|
||||
} catch (_) {
|
||||
el.textContent = '--';
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshDashboardMcpCard(popover) {
|
||||
var el = popover.querySelector('[data-dashboard-value="mcp"]');
|
||||
async function refreshDashboardToolsCard(popover) {
|
||||
var el = popover.querySelector('[data-dashboard-value="tools"]');
|
||||
if (!el) return;
|
||||
el.textContent = '加载中...';
|
||||
try {
|
||||
var resp = await fetch('/api/hermes/mcp/servers');
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
var servers = await resp.json();
|
||||
var list = Array.isArray(servers) ? servers : (servers.servers || []);
|
||||
var connected = list.filter(function(s) { return s.connected; }).length;
|
||||
el.textContent = list.length + ' / ' + connected + ' 已连接';
|
||||
} catch (_) {
|
||||
el.textContent = '--';
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshDashboardAgentCard(popover) {
|
||||
var el = popover.querySelector('[data-dashboard-value="agent"]');
|
||||
if (!el) return;
|
||||
el.textContent = '加载中...';
|
||||
try {
|
||||
var resp = await fetch('/api/hermes/client/runs?limit=100');
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
var data = await resp.json();
|
||||
var runs = Array.isArray(data) ? data : (data.runs || []);
|
||||
var recentCount = runs.length;
|
||||
el.textContent = recentCount + ' 次';
|
||||
var pack = await fetchAgentToolsManifest();
|
||||
el.textContent = pack.tools.length + ' 个';
|
||||
} catch (_) {
|
||||
el.textContent = '--';
|
||||
}
|
||||
|
||||
@@ -2982,61 +2982,9 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
openSearchModal();
|
||||
}, true);
|
||||
|
||||
const sidebarPageAi = createSidebarPageAiRuntime({
|
||||
buildLocalFileOpenUrl,
|
||||
currentDocumentId,
|
||||
currentPageAggregate,
|
||||
currentPageOptions,
|
||||
currentRootUri,
|
||||
currentSourceKind,
|
||||
escapeHtml,
|
||||
cssEscape,
|
||||
openLocalResourceInActiveTab,
|
||||
pageUiState,
|
||||
resolveWorkspaceId,
|
||||
searchText,
|
||||
});
|
||||
// Page AI 产品入口仅 Pi Lab;facade 只桥接 open / trigger / no-op delegates。
|
||||
const sidebarPageAi = createSidebarPageAiRuntime({ pageUiState });
|
||||
const openPageAiDrawer = (...args) => sidebarPageAi.openPageAiDrawer(...args);
|
||||
const closePageAiDrawer = (...args) => sidebarPageAi.closePageAiDrawer(...args);
|
||||
const isPageAiDrawerOpen = (...args) => sidebarPageAi.isPageAiDrawerOpen(...args);
|
||||
const ensurePageAiDrawer = (...args) => sidebarPageAi.ensurePageAiDrawer(...args);
|
||||
const sendPageAiMessage = (...args) => sidebarPageAi.sendPageAiMessage(...args);
|
||||
const pageAiOpenHermesSettings = (...args) => sidebarPageAi.pageAiOpenHermesSettings(...args);
|
||||
const pageAiStopRun = (...args) => sidebarPageAi.pageAiStopRun(...args);
|
||||
const pageAiLoadGatewayHealth = (...args) => sidebarPageAi.pageAiLoadGatewayHealth(...args);
|
||||
const renderPageAiControls = (...args) => sidebarPageAi.renderPageAiControls(...args);
|
||||
const renderPageAiConversation = (...args) => sidebarPageAi.renderPageAiConversation(...args);
|
||||
const renderPageAiProviderButtons = (...args) => sidebarPageAi.renderPageAiProviderButtons(...args);
|
||||
const renderPageAiSuggestions = (...args) => sidebarPageAi.renderPageAiSuggestions(...args);
|
||||
const pageAiSaveProfileMemory = (...args) => sidebarPageAi.pageAiSaveProfileMemory(...args);
|
||||
const pageAiToggleSkill = (...args) => sidebarPageAi.pageAiToggleSkill(...args);
|
||||
const pageAiToggleTool = (...args) => sidebarPageAi.pageAiToggleTool(...args);
|
||||
const pageAiResumeBackendSession = (...args) => sidebarPageAi.pageAiResumeBackendSession(...args);
|
||||
const pageAiRenameBackendSession = (...args) => sidebarPageAi.pageAiRenameBackendSession(...args);
|
||||
const pageAiDeleteBackendSession = (...args) => sidebarPageAi.pageAiDeleteBackendSession(...args);
|
||||
const pageAiResolvePermission = (...args) => sidebarPageAi.pageAiResolvePermission(...args);
|
||||
const pageAiOpenLocation = (...args) => sidebarPageAi.pageAiOpenLocation(...args);
|
||||
const pageAiSetActiveSession = (...args) => sidebarPageAi.pageAiSetActiveSession(...args);
|
||||
const pageAiStartNewSession = (...args) => sidebarPageAi.pageAiStartNewSession(...args);
|
||||
const pageAiLoadSessions = (...args) => sidebarPageAi.pageAiLoadSessions(...args);
|
||||
const pageAiLoadBackendSessions = (...args) => sidebarPageAi.pageAiLoadBackendSessions(...args);
|
||||
const pageAiCancelQueuedRun = (...args) => sidebarPageAi.pageAiCancelQueuedRun(...args);
|
||||
const pageAiSearchBackendSessions = (...args) => sidebarPageAi.pageAiSearchBackendSessions(...args);
|
||||
const pageAiPersistSessions = (...args) => sidebarPageAi.pageAiPersistSessions(...args);
|
||||
const pageAiLoadProfiles = (...args) => sidebarPageAi.pageAiLoadProfiles(...args);
|
||||
const pageAiLoadProfileMemory = (...args) => sidebarPageAi.pageAiLoadProfileMemory(...args);
|
||||
const pageAiLoadSkills = (...args) => sidebarPageAi.pageAiLoadSkills(...args);
|
||||
const pageAiSwitchProfile = (...args) => sidebarPageAi.pageAiSwitchProfile(...args);
|
||||
const pageAiSetSkillSource = (...args) => sidebarPageAi.pageAiSetSkillSource(...args);
|
||||
const pageAiToggleSkillGroup = (...args) => sidebarPageAi.pageAiToggleSkillGroup(...args);
|
||||
const pageAiSetReasonixMemoryEnabled = (...args) => sidebarPageAi.pageAiSetReasonixMemoryEnabled(...args);
|
||||
const pageAiSetHideHermesBuiltinSkills = (...args) => sidebarPageAi.pageAiSetHideHermesBuiltinSkills(...args);
|
||||
const pageAiSetContextScope = (...args) => sidebarPageAi.pageAiSetContextScope(...args);
|
||||
const pageAiSetAgentId = (...args) => sidebarPageAi.pageAiSetAgentId(...args);
|
||||
const pageAiToggleContextRef = (...args) => sidebarPageAi.pageAiToggleContextRef(...args);
|
||||
const pageAiSetAgentPopoverOpen = (...args) => sidebarPageAi.pageAiSetAgentPopoverOpen(...args);
|
||||
const pageAiSetTargetPopoverOpen = (...args) => sidebarPageAi.pageAiSetTargetPopoverOpen(...args);
|
||||
const pageAiSelectTarget = (...args) => sidebarPageAi.pageAiSelectTarget(...args);
|
||||
const updatePageAiTriggerState = (...args) => sidebarPageAi.updatePageAiTriggerState(...args);
|
||||
|
||||
const sidebarAttachmentOpen = createSidebarAttachmentOpenRuntime({
|
||||
@@ -3374,7 +3322,11 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
var pageAiTrigger = closestAction(e.target, '[data-mnote-action="open-page-ai"]');
|
||||
if (pageAiTrigger) {
|
||||
e.preventDefault();
|
||||
openPageAiDrawer();
|
||||
// 产品面唯一 Page AI:Pi Lab(不再打开 Hermes/OpenCode legacy drawer)
|
||||
if (window.createSidebarPageAiPiLabRuntime) {
|
||||
window.createSidebarPageAiPiLabRuntime({});
|
||||
}
|
||||
window.postMessage({ source: 'mnote-sidebar', type: 'mnote:pi-lab-show' }, window.location.origin);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -163,12 +163,17 @@
|
||||
|
||||
function ensureFormUi() {
|
||||
if (!state._formUi) {
|
||||
state._formUi = { openAccountIds: {}, expandedSecretPanels: {} };
|
||||
state._formUi = {
|
||||
openAccountIds: {},
|
||||
expandedSecretPanels: {},
|
||||
folderPickerOpen: {},
|
||||
};
|
||||
}
|
||||
if (!state._formUi.openAccountIds) state._formUi.openAccountIds = {};
|
||||
if (!state._formUi.expandedSecretPanels) {
|
||||
state._formUi.expandedSecretPanels = {};
|
||||
}
|
||||
if (!state._formUi.folderPickerOpen) state._formUi.folderPickerOpen = {};
|
||||
return state._formUi;
|
||||
}
|
||||
|
||||
@@ -892,6 +897,8 @@
|
||||
kind: (s && s.kind) || 'apikey',
|
||||
label: (s && s.label) || '',
|
||||
value: (s && s.value) || { state: 'absent' },
|
||||
// Draft-only flag from reRenderFormKeepingSlots; absent on server items.
|
||||
valueClear: !!(s && s.valueClear),
|
||||
accountId: (s && (s.accountId || s.account_id)) || '',
|
||||
};
|
||||
}
|
||||
@@ -924,6 +931,96 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 将 ISO 时间格式化为本地可读短串;无效则原样返回。 */
|
||||
function formatVaultDateTime(raw) {
|
||||
var s = String(raw || '').trim();
|
||||
if (!s) return '';
|
||||
var d = new Date(s);
|
||||
if (isNaN(d.getTime())) return s;
|
||||
try {
|
||||
return d.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
} catch (_e) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号级登录态展示(扩展 Cookie session)。
|
||||
* 优先账号槽位字段;合成单账号时回退条目级 hasLoginSession / sessionUpdatedAt。
|
||||
*/
|
||||
function accountSessionView(acc, item, isSyntheticSingle) {
|
||||
var has =
|
||||
acc && typeof acc.hasLoginSession === 'boolean'
|
||||
? acc.hasLoginSession
|
||||
: isSyntheticSingle && item
|
||||
? !!item.hasLoginSession
|
||||
: false;
|
||||
var savedAt =
|
||||
(acc && (acc.lastLoginAt || acc.sessionUpdatedAt)) ||
|
||||
(isSyntheticSingle && item
|
||||
? item.lastLoginAt || item.sessionUpdatedAt || ''
|
||||
: '') ||
|
||||
'';
|
||||
var expiresAt =
|
||||
(acc && acc.sessionExpiresAt) ||
|
||||
(isSyntheticSingle && item ? item.sessionExpiresAt || '' : '') ||
|
||||
'';
|
||||
return {
|
||||
hasLoginSession: !!has,
|
||||
savedAt: String(savedAt || '').trim(),
|
||||
expiresAt: String(expiresAt || '').trim(),
|
||||
source: (acc && acc.sessionSource) || '',
|
||||
};
|
||||
}
|
||||
|
||||
/** 详情页:账号下「是否保存登录态 + 保存日期」行。 */
|
||||
function renderAccountSessionRows(sess, showEmpty) {
|
||||
if (!sess.hasLoginSession && !showEmpty) {
|
||||
// 未保存时也简短展示一行,避免用户以为功能缺失
|
||||
return (
|
||||
'<div class="mnote-vault-field-row mnote-vault-session-row" data-testid="vault-account-session">' +
|
||||
'<label>登录态</label>' +
|
||||
'<div><span class="mnote-vault-session-badge is-absent">未保存</span></div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
var badge = sess.hasLoginSession
|
||||
? '<span class="mnote-vault-session-badge is-saved" data-testid="vault-session-saved">已保存</span>'
|
||||
: '<span class="mnote-vault-session-badge is-absent" data-testid="vault-session-absent">未保存</span>';
|
||||
var dateText = sess.hasLoginSession
|
||||
? formatVaultDateTime(sess.savedAt) || '—'
|
||||
: '—';
|
||||
var html =
|
||||
'<div class="mnote-vault-field-row mnote-vault-session-row" data-testid="vault-account-session">' +
|
||||
'<label>登录态</label>' +
|
||||
'<div class="mnote-vault-session-meta">' +
|
||||
badge +
|
||||
'</div></div>' +
|
||||
'<div class="mnote-vault-field-row mnote-vault-session-row" data-testid="vault-account-session-date">' +
|
||||
'<label>保存日期</label>' +
|
||||
'<div>' +
|
||||
(sess.hasLoginSession
|
||||
? escapeHtml(dateText)
|
||||
: '<span class="mnote-vault-muted">—</span>') +
|
||||
'</div></div>';
|
||||
if (sess.hasLoginSession && sess.expiresAt) {
|
||||
html +=
|
||||
'<div class="mnote-vault-field-row mnote-vault-session-row" data-testid="vault-account-session-expires">' +
|
||||
'<label>过期时间</label>' +
|
||||
'<div>' +
|
||||
escapeHtml(formatVaultDateTime(sess.expiresAt) || sess.expiresAt) +
|
||||
'</div></div>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function normalizeAccountsFromItem(item, forEdit) {
|
||||
var topSecrets = normalizeSecretsFromItem(item);
|
||||
var accounts = (item && item.accounts) || [];
|
||||
@@ -941,6 +1038,25 @@
|
||||
return s.accountId && s.accountId === a.id;
|
||||
});
|
||||
}
|
||||
// 多账号时:若槽位未挂 session 元数据,首账号可回退条目级(兼容旧投影)
|
||||
var hasSess =
|
||||
typeof a.hasLoginSession === 'boolean'
|
||||
? a.hasLoginSession
|
||||
: aidx === 0
|
||||
? !!(item && item.hasLoginSession)
|
||||
: false;
|
||||
var lastLogin =
|
||||
a.lastLoginAt ||
|
||||
(aidx === 0 && item ? item.lastLoginAt : null) ||
|
||||
null;
|
||||
var sessUpdated =
|
||||
a.sessionUpdatedAt ||
|
||||
(aidx === 0 && item ? item.sessionUpdatedAt : null) ||
|
||||
null;
|
||||
var sessExpires =
|
||||
a.sessionExpiresAt ||
|
||||
(aidx === 0 && item ? item.sessionExpiresAt : null) ||
|
||||
null;
|
||||
return {
|
||||
id: a.id || '',
|
||||
label: a.label || '',
|
||||
@@ -952,7 +1068,14 @@
|
||||
: a.email || '',
|
||||
passwordHint: a.passwordHint || '',
|
||||
password: a.password || { state: 'absent' },
|
||||
// Draft-only flag from reRenderFormKeepingSlots; absent on server items.
|
||||
passwordClear: !!a.passwordClear,
|
||||
secrets: nested,
|
||||
hasLoginSession: hasSess,
|
||||
lastLoginAt: lastLogin,
|
||||
sessionUpdatedAt: sessUpdated,
|
||||
sessionExpiresAt: sessExpires,
|
||||
sessionSource: a.sessionSource || '',
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -974,6 +1097,13 @@
|
||||
passwordHint: (item && item.passwordHint) || '',
|
||||
password: (item && item.password) || { state: 'absent' },
|
||||
secrets: topSecrets,
|
||||
// 合成单账号:登录态取条目级
|
||||
hasLoginSession: !!(item && item.hasLoginSession),
|
||||
lastLoginAt: (item && item.lastLoginAt) || null,
|
||||
sessionUpdatedAt: (item && item.sessionUpdatedAt) || null,
|
||||
sessionExpiresAt: (item && item.sessionExpiresAt) || null,
|
||||
sessionSource: '',
|
||||
_syntheticFromItem: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -986,6 +1116,12 @@
|
||||
passwordHint: '',
|
||||
password: { state: 'absent' },
|
||||
secrets: [],
|
||||
hasLoginSession: !!(item && item.hasLoginSession),
|
||||
lastLoginAt: (item && item.lastLoginAt) || null,
|
||||
sessionUpdatedAt: (item && item.sessionUpdatedAt) || null,
|
||||
sessionExpiresAt: (item && item.sessionExpiresAt) || null,
|
||||
sessionSource: '',
|
||||
_syntheticFromItem: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1157,6 +1293,11 @@
|
||||
acc.username ||
|
||||
acc.email ||
|
||||
'账号 ' + (idx + 1);
|
||||
var sess = accountSessionView(
|
||||
acc,
|
||||
item,
|
||||
!!acc._syntheticFromItem || accounts.length === 1
|
||||
);
|
||||
var body =
|
||||
fieldRow('标签名', acc.label, showEmpty) +
|
||||
fieldRow('用户名', acc.username, showEmpty) +
|
||||
@@ -1164,7 +1305,8 @@
|
||||
secretRow('密码', 'password', acc.password, showEmpty, {
|
||||
accountId: acc.id || '',
|
||||
}) +
|
||||
fieldRow('密码提示', acc.passwordHint, showEmpty);
|
||||
fieldRow('密码提示', acc.passwordHint, showEmpty) +
|
||||
renderAccountSessionRows(sess, showEmpty);
|
||||
var secList = acc.secrets || [];
|
||||
var nestedSecretsHtml = secList
|
||||
.map(function (sec, sidx) {
|
||||
@@ -1220,6 +1362,9 @@
|
||||
(secList.length
|
||||
? '<span class="mnote-vault-muted">密钥 ' + secList.length + '</span>'
|
||||
: '') +
|
||||
(sess.hasLoginSession
|
||||
? '<span class="mnote-vault-session-badge is-saved is-compact" title="已保存登录态">登录态</span>'
|
||||
: '') +
|
||||
'</summary>' +
|
||||
'<div class="mnote-vault-slot-body">' +
|
||||
body +
|
||||
@@ -1329,7 +1474,7 @@
|
||||
);
|
||||
}
|
||||
|
||||
/** Unique folderPath values from current list (for select / datalist). */
|
||||
/** Unique folderPath values from current list (for picker / datalist). */
|
||||
function collectFolderPaths() {
|
||||
var set = {};
|
||||
(state.items || []).forEach(function (item) {
|
||||
@@ -1346,47 +1491,152 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Nested tree of known folderPath segments for the expand-style picker.
|
||||
* @returns {{ name: string, path: string, children: Array }}
|
||||
*/
|
||||
function buildFolderPickerTree(paths) {
|
||||
var root = { name: '', path: '', children: [] };
|
||||
var byPath = { '': root };
|
||||
(paths || []).forEach(function (fp) {
|
||||
var parts = String(fp || '')
|
||||
.split('/')
|
||||
.filter(Boolean);
|
||||
var acc = [];
|
||||
var parent = root;
|
||||
parts.forEach(function (part) {
|
||||
acc.push(part);
|
||||
var p = acc.join('/');
|
||||
if (!byPath[p]) {
|
||||
var node = { name: part, path: p, children: [] };
|
||||
byPath[p] = node;
|
||||
parent.children.push(node);
|
||||
}
|
||||
parent = byPath[p];
|
||||
});
|
||||
});
|
||||
function sortNodes(nodes) {
|
||||
nodes.sort(function (a, b) {
|
||||
return a.name.localeCompare(b.name, 'zh');
|
||||
});
|
||||
nodes.forEach(function (n) {
|
||||
if (n.children && n.children.length) sortNodes(n.children);
|
||||
});
|
||||
}
|
||||
sortNodes(root.children);
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a picker branch starts expanded.
|
||||
* Default: all collapsed. Only expand ancestors of the current selection
|
||||
* (so the selected path stays visible), or branches the user toggled open.
|
||||
*/
|
||||
function isFolderPickerBranchOpen(path, depth, current) {
|
||||
var ui = ensureFormUi();
|
||||
if (!ui.folderPickerOpen) ui.folderPickerOpen = {};
|
||||
if (Object.prototype.hasOwnProperty.call(ui.folderPickerOpen, path)) {
|
||||
return !!ui.folderPickerOpen[path];
|
||||
}
|
||||
// Only auto-open ancestors of the selected path (not the leaf itself unless it has kids under selection).
|
||||
if (current && path && current.indexOf(path + '/') === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function renderFolderPickerNode(node, depth, current) {
|
||||
var kids = node.children || [];
|
||||
var hasKids = kids.length > 0;
|
||||
var selected = current === node.path;
|
||||
var open = hasKids && isFolderPickerBranchOpen(node.path, depth, current);
|
||||
var html = '';
|
||||
html +=
|
||||
'<div class="mnote-vault-folder-picker-node" data-vault-picker-path="' +
|
||||
escapeHtml(node.path) +
|
||||
'" style="--vault-picker-depth:' +
|
||||
depth +
|
||||
'">';
|
||||
html += '<div class="mnote-vault-folder-picker-row' + (selected ? ' is-selected' : '') + '">';
|
||||
if (hasKids) {
|
||||
html +=
|
||||
'<button type="button" class="mnote-vault-folder-picker-chevron" data-vault-folder-picker-toggle="' +
|
||||
escapeHtml(node.path) +
|
||||
'" aria-expanded="' +
|
||||
(open ? 'true' : 'false') +
|
||||
'" title="' +
|
||||
(open ? '收起' : '展开') +
|
||||
'">' +
|
||||
(open ? '▼' : '▶') +
|
||||
'</button>';
|
||||
} else {
|
||||
html += '<span class="mnote-vault-folder-picker-chevron is-leaf" aria-hidden="true"></span>';
|
||||
}
|
||||
html +=
|
||||
'<button type="button" class="mnote-vault-folder-picker-label" data-vault-folder-pick="' +
|
||||
escapeHtml(node.path) +
|
||||
'" data-testid="vault-folder-pick-' +
|
||||
escapeHtml(node.path || 'root') +
|
||||
'">' +
|
||||
escapeHtml(node.name || node.path) +
|
||||
'</button>';
|
||||
html += '</div>';
|
||||
if (hasKids) {
|
||||
html +=
|
||||
'<div class="mnote-vault-folder-picker-children"' +
|
||||
(open ? '' : ' hidden') +
|
||||
'>';
|
||||
kids.forEach(function (child) {
|
||||
html += renderFolderPickerNode(child, depth + 1, current);
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function formFolderPathField(value) {
|
||||
var current = (value || '').trim();
|
||||
var current = normalizeFolderPath(value || '');
|
||||
var paths = collectFolderPaths();
|
||||
var seen = {};
|
||||
paths.forEach(function (p) {
|
||||
seen[p] = true;
|
||||
});
|
||||
var options =
|
||||
'<option value=""' +
|
||||
(!current ? ' selected' : '') +
|
||||
'>(无分组)</option>';
|
||||
paths.forEach(function (p) {
|
||||
options +=
|
||||
'<option value="' +
|
||||
escapeHtml(p) +
|
||||
'"' +
|
||||
(p === current ? ' selected' : '') +
|
||||
'>' +
|
||||
escapeHtml(p) +
|
||||
'</option>';
|
||||
});
|
||||
// 当前值若不在列表中,仍挂到树上便于高亮/再选。
|
||||
var treePaths = paths.slice();
|
||||
if (current && !seen[current]) {
|
||||
options +=
|
||||
'<option value="' +
|
||||
escapeHtml(current) +
|
||||
'" selected>' +
|
||||
escapeHtml(current) +
|
||||
'</option>';
|
||||
treePaths.push(current);
|
||||
var parts = current.split('/').filter(Boolean);
|
||||
for (var i = 1; i < parts.length; i++) {
|
||||
var prefix = parts.slice(0, i).join('/');
|
||||
if (treePaths.indexOf(prefix) < 0) treePaths.push(prefix);
|
||||
}
|
||||
}
|
||||
var tree = buildFolderPickerTree(treePaths);
|
||||
var treeHtml = '';
|
||||
treeHtml +=
|
||||
'<button type="button" class="mnote-vault-folder-picker-none' +
|
||||
(!current ? ' is-selected' : '') +
|
||||
'" data-vault-folder-pick="" data-testid="vault-folder-pick-none">(无分组)</button>';
|
||||
tree.children.forEach(function (child) {
|
||||
treeHtml += renderFolderPickerNode(child, 0, current);
|
||||
});
|
||||
if (!tree.children.length && !current) {
|
||||
treeHtml +=
|
||||
'<div class="mnote-vault-folder-picker-empty mnote-vault-muted">暂无已有分组,可在下方输入新建</div>';
|
||||
}
|
||||
return (
|
||||
'<div class="mnote-vault-field-row mnote-vault-folder-field">' +
|
||||
'<label for="vault-f-folderPath">分组</label>' +
|
||||
'<div class="mnote-vault-folder-controls">' +
|
||||
'<select id="vault-f-folderSelect" data-vault-folder-select data-testid="vault-folder-select" aria-label="选择分组">' +
|
||||
options +
|
||||
'</select>' +
|
||||
'<div class="mnote-vault-folder-picker" data-vault-folder-picker data-testid="vault-folder-select" role="listbox" aria-label="选择分组">' +
|
||||
treeHtml +
|
||||
'</div>' +
|
||||
'<input id="vault-f-folderPath" name="folderPath" type="text" ' +
|
||||
'value="' +
|
||||
escapeHtml(current) +
|
||||
'" placeholder="新分组可直接输入,用 / 分层" ' +
|
||||
'data-vault-folder-input data-testid="vault-folder-path" list="vault-folder-path-list" />' +
|
||||
'" placeholder="点上方分组选择,或直接输入;用 / 分层" ' +
|
||||
'data-vault-folder-input data-testid="vault-folder-path" list="vault-folder-path-list" autocomplete="off" />' +
|
||||
'<datalist id="vault-folder-path-list">' +
|
||||
paths
|
||||
.map(function (p) {
|
||||
@@ -1394,39 +1644,75 @@
|
||||
})
|
||||
.join('') +
|
||||
'</datalist>' +
|
||||
'<p class="mnote-vault-form-hint mnote-vault-folder-hint">分组默认折叠;点 ▶ 展开,点名称即可选中。也可在下方直接输入路径。</p>' +
|
||||
'</div></div>'
|
||||
);
|
||||
}
|
||||
|
||||
function syncFolderPickerSelection(form, path) {
|
||||
if (!form) return;
|
||||
var picker = qs('[data-vault-folder-picker]', form);
|
||||
if (!picker) return;
|
||||
var normalized = normalizeFolderPath(path || '');
|
||||
qsa('[data-vault-folder-pick]', picker).forEach(function (btn) {
|
||||
var p = btn.getAttribute('data-vault-folder-pick');
|
||||
if (p == null) p = '';
|
||||
var row = btn.closest('.mnote-vault-folder-picker-row');
|
||||
var selected = p === normalized;
|
||||
btn.classList.toggle('is-selected', selected);
|
||||
if (row) row.classList.toggle('is-selected', selected);
|
||||
if (btn.classList.contains('mnote-vault-folder-picker-none')) {
|
||||
btn.classList.toggle('is-selected', selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bindFolderPathControls(form) {
|
||||
if (!form) return;
|
||||
var select = qs('[data-vault-folder-select]', form);
|
||||
var input = qs('[data-vault-folder-input]', form);
|
||||
if (select && input) {
|
||||
select.addEventListener('change', function () {
|
||||
input.value = select.value || '';
|
||||
state.dirty = true;
|
||||
var picker = qs('[data-vault-folder-picker]', form);
|
||||
if (picker) {
|
||||
picker.addEventListener('click', function (ev) {
|
||||
var t = ev.target;
|
||||
if (!(t instanceof Element)) return;
|
||||
var toggle = t.closest('[data-vault-folder-picker-toggle]');
|
||||
if (toggle && picker.contains(toggle)) {
|
||||
ev.preventDefault();
|
||||
var tpath = toggle.getAttribute('data-vault-folder-picker-toggle') || '';
|
||||
var nodeEl = toggle.closest('.mnote-vault-folder-picker-node');
|
||||
// First matching descendants is the direct children panel for this node.
|
||||
var kidsEl =
|
||||
nodeEl && nodeEl.querySelector('.mnote-vault-folder-picker-children');
|
||||
var open = toggle.getAttribute('aria-expanded') === 'true';
|
||||
var next = !open;
|
||||
ensureFormUi().folderPickerOpen = ensureFormUi().folderPickerOpen || {};
|
||||
ensureFormUi().folderPickerOpen[tpath] = next;
|
||||
toggle.setAttribute('aria-expanded', next ? 'true' : 'false');
|
||||
toggle.textContent = next ? '▼' : '▶';
|
||||
toggle.setAttribute('title', next ? '收起' : '展开');
|
||||
if (kidsEl) kidsEl.hidden = !next;
|
||||
return;
|
||||
}
|
||||
var pick = t.closest('[data-vault-folder-pick]');
|
||||
if (pick && picker.contains(pick)) {
|
||||
ev.preventDefault();
|
||||
var chosen = pick.getAttribute('data-vault-folder-pick');
|
||||
if (chosen == null) chosen = '';
|
||||
chosen = normalizeFolderPath(chosen);
|
||||
if (input) input.value = chosen;
|
||||
syncFolderPickerSelection(form, chosen);
|
||||
state.dirty = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (input) {
|
||||
input.addEventListener('input', function () {
|
||||
var n = normalizeFolderPath(input.value);
|
||||
// keep select in sync when user picks a known path via typing
|
||||
if (select) {
|
||||
var match = false;
|
||||
for (var i = 0; i < select.options.length; i++) {
|
||||
if (select.options[i].value === n) {
|
||||
select.selectedIndex = i;
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!match) select.selectedIndex = 0;
|
||||
}
|
||||
syncFolderPickerSelection(form, input.value);
|
||||
});
|
||||
input.addEventListener('change', function () {
|
||||
var n = normalizeFolderPath(input.value);
|
||||
input.value = n;
|
||||
syncFolderPickerSelection(form, n);
|
||||
state.dirty = true;
|
||||
});
|
||||
}
|
||||
@@ -1439,24 +1725,55 @@
|
||||
var urls = normalizeUrlsFromItem(item);
|
||||
if (!urls.length) urls = [''];
|
||||
state._formUrls = urls.slice();
|
||||
/**
|
||||
* Rebuild draft slots from item projection.
|
||||
* reRenderFormKeepingSlots injects in-progress plaintext as
|
||||
* `{ state: 'revealed', value }` — must become valueText/passwordValue,
|
||||
* not be wiped to '' (that made the 1st new secret disappear after +密钥).
|
||||
* valueClear/passwordClear on the pseudo-item are also preserved.
|
||||
*/
|
||||
state._formAccounts = accounts.map(function (a) {
|
||||
var pwd = a.password || { state: 'absent' };
|
||||
var passwordClear = !!a.passwordClear;
|
||||
var passwordValue = '';
|
||||
var passwordState = pwd;
|
||||
if (passwordClear) {
|
||||
passwordState = { state: 'absent' };
|
||||
passwordValue = '';
|
||||
} else if (pwd && pwd.state === 'revealed') {
|
||||
// Draft plaintext typed in this session (not yet saved / mid re-render).
|
||||
passwordValue = String(pwd.value || '');
|
||||
// Treat as "no stored secret yet" for keep-logic; save uses passwordValue.
|
||||
passwordState = { state: 'absent' };
|
||||
}
|
||||
return {
|
||||
id: a.id || newClientId('acc'),
|
||||
label: a.label || '',
|
||||
username: a.username || '',
|
||||
email: a.email || '',
|
||||
passwordHint: a.passwordHint || '',
|
||||
passwordState: a.password || { state: 'absent' },
|
||||
passwordClear: false,
|
||||
passwordValue: '',
|
||||
passwordState: passwordState,
|
||||
passwordClear: passwordClear,
|
||||
passwordValue: passwordValue,
|
||||
secrets: (a.secrets || []).map(function (s) {
|
||||
var v = s.value || { state: 'absent' };
|
||||
var valueClear = !!s.valueClear;
|
||||
var valueText = '';
|
||||
var valueState = v;
|
||||
if (valueClear) {
|
||||
valueState = { state: 'absent' };
|
||||
valueText = '';
|
||||
} else if (v && v.state === 'revealed') {
|
||||
valueText = String(v.value || '');
|
||||
valueState = { state: 'absent' };
|
||||
}
|
||||
return {
|
||||
id: s.id || newClientId('sec'),
|
||||
kind: s.kind || 'apikey',
|
||||
label: s.label || '',
|
||||
valueState: s.value || { state: 'absent' },
|
||||
valueClear: false,
|
||||
valueText: '',
|
||||
valueState: valueState,
|
||||
valueClear: valueClear,
|
||||
valueText: valueText,
|
||||
accountId: a.id || '',
|
||||
};
|
||||
}),
|
||||
@@ -1592,9 +1909,11 @@
|
||||
'<div class="mnote-vault-secret-edit">' +
|
||||
'<input type="text" autocomplete="off" spellcheck="false" data-acc-field="password" data-acc-id="' +
|
||||
escapeHtml(acc.id) +
|
||||
'" class="mnote-vault-secret-input-plain" placeholder="' +
|
||||
'" class="mnote-vault-secret-input-plain" value="' +
|
||||
escapeHtml(acc.passwordValue || '') +
|
||||
'" placeholder="' +
|
||||
escapeHtml(
|
||||
!isCreate && hasPwd
|
||||
!isCreate && hasPwd && !acc.passwordValue
|
||||
? '留空不改;输入新值覆盖'
|
||||
: '可选;可用 [Key] 密文片段'
|
||||
) +
|
||||
@@ -1602,7 +1921,11 @@
|
||||
(!isCreate && hasPwd
|
||||
? '<button type="button" data-acc-show-password="' +
|
||||
escapeHtml(acc.id) +
|
||||
'" data-testid="vault-acc-show-password">显示</button>' +
|
||||
'"' +
|
||||
(acc.passwordValue ? ' data-shown="1"' : '') +
|
||||
' data-testid="vault-acc-show-password">' +
|
||||
(acc.passwordValue ? '隐藏' : '显示') +
|
||||
'</button>' +
|
||||
'<button type="button" data-acc-clear-password="' +
|
||||
escapeHtml(acc.id) +
|
||||
'">清空</button>'
|
||||
@@ -1624,6 +1947,7 @@
|
||||
sec.kind === 'token' ? 'Token' : sec.kind === 'other' ? '其他' : 'API Key';
|
||||
var title = sec.label || kindLabel + ' ' + (idx + 1);
|
||||
var hasVal = sec.valueState && sec.valueState.state === 'masked';
|
||||
var draftText = sec.valueText || '';
|
||||
return (
|
||||
'<div class="mnote-vault-nested-secret" data-vault-secret-slot="' +
|
||||
escapeHtml(sec.id) +
|
||||
@@ -1672,15 +1996,21 @@
|
||||
escapeHtml(sec.id) +
|
||||
'" data-acc-id="' +
|
||||
escapeHtml(accountId) +
|
||||
'" value="" placeholder="' +
|
||||
escapeHtml(hasVal ? '留空不改;输入新值覆盖' : '密钥明文') +
|
||||
'" value="' +
|
||||
escapeHtml(draftText) +
|
||||
'" placeholder="' +
|
||||
escapeHtml(hasVal && !draftText ? '留空不改;输入新值覆盖' : '密钥明文') +
|
||||
'" />' +
|
||||
(hasVal
|
||||
? '<button type="button" data-sec-show-value="' +
|
||||
escapeHtml(sec.id) +
|
||||
'" data-acc-id="' +
|
||||
escapeHtml(accountId) +
|
||||
'" data-testid="vault-sec-show-value">显示</button>' +
|
||||
'"' +
|
||||
(draftText ? ' data-shown="1"' : '') +
|
||||
' data-testid="vault-sec-show-value">' +
|
||||
(draftText ? '隐藏' : '显示') +
|
||||
'</button>' +
|
||||
'<button type="button" data-sec-clear-value="' +
|
||||
escapeHtml(sec.id) +
|
||||
'" data-acc-id="' +
|
||||
@@ -1830,6 +2160,8 @@
|
||||
username: a.username,
|
||||
email: a.email,
|
||||
passwordHint: a.passwordHint,
|
||||
// Flags survive re-render so clear intent is not lost.
|
||||
passwordClear: !!a.passwordClear,
|
||||
password: a.passwordClear
|
||||
? { state: 'absent' }
|
||||
: a.passwordValue
|
||||
@@ -1841,6 +2173,7 @@
|
||||
kind: s.kind,
|
||||
label: s.label,
|
||||
accountId: a.id,
|
||||
valueClear: !!s.valueClear,
|
||||
value: s.valueClear
|
||||
? { state: 'absent' }
|
||||
: s.valueText
|
||||
@@ -2040,13 +2373,30 @@
|
||||
var folderPath = normalizeFolderPath(String(fd.get('folderPath') || '').trim());
|
||||
var accounts = (state._formAccounts || [])
|
||||
.map(function (acc) {
|
||||
// Prefer live input draft; fall back to revealed draft left by re-render inject.
|
||||
var raw = String(acc.passwordValue || '');
|
||||
if (
|
||||
!raw &&
|
||||
acc.passwordState &&
|
||||
acc.passwordState.state === 'revealed' &&
|
||||
acc.passwordState.value
|
||||
) {
|
||||
raw = String(acc.passwordState.value);
|
||||
}
|
||||
if (/^[•*·]+$/.test(raw) || raw === '••••••••') {
|
||||
toast('不能将掩码写回 password', 'error');
|
||||
throw new Error('mask');
|
||||
}
|
||||
var nestedSecrets = (acc.secrets || []).map(function (sec) {
|
||||
var sraw = String(sec.valueText || '');
|
||||
if (
|
||||
!sraw &&
|
||||
sec.valueState &&
|
||||
sec.valueState.state === 'revealed' &&
|
||||
sec.valueState.value
|
||||
) {
|
||||
sraw = String(sec.valueState.value);
|
||||
}
|
||||
if (/^[•*·]+$/.test(sraw) || sraw === '••••••••') {
|
||||
toast('不能将掩码写回 secret', 'error');
|
||||
throw new Error('mask');
|
||||
|
||||
Reference in New Issue
Block a user