chore: harden lightrag knowledge rag runtime
This commit is contained in:
@@ -690,11 +690,18 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const entry = resourceTabRegistry.get(activeResource);
|
||||
const documentId = String(entry?.ownerDocumentId || entry?.documentId || '').trim();
|
||||
if (documentId) url.pathname = `/documents/${encodeURIComponent(documentId)}`;
|
||||
const sourceKind = String(entry?.sourceKind || currentWebShellSourceKind() || '').trim();
|
||||
const rootUri = String(entry?.rootUri || currentWebShellRootUri() || '').trim();
|
||||
const resourcePath = String(entry?.path || entry?.session?.resourcePath || '').trim();
|
||||
if (sourceKind) url.searchParams.set('sourceKind', sourceKind);
|
||||
if (rootUri) url.searchParams.set('rootUri', rootUri);
|
||||
if (resourcePath) url.searchParams.set('resourcePath', resourcePath);
|
||||
url.searchParams.set('resourceTab', activeResource);
|
||||
} else {
|
||||
const documentId = currentWebShellDocumentId();
|
||||
if (documentId) url.pathname = `/documents/${encodeURIComponent(documentId)}`;
|
||||
url.searchParams.delete('resourceTab');
|
||||
url.searchParams.delete('resourcePath');
|
||||
}
|
||||
replaceUrlState(url);
|
||||
};
|
||||
@@ -1085,6 +1092,59 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
overlay.style.height = `${Math.max(1, bbox.y1 - bbox.y0)}px`;
|
||||
};
|
||||
|
||||
const applyPdfEvidenceOverlayToCanvas = (canvas, locator) => {
|
||||
if (!(canvas instanceof HTMLCanvasElement)) return null;
|
||||
const frame = canvas.parentElement instanceof HTMLElement && canvas.parentElement.hasAttribute('data-mnote-pdf-page-frame')
|
||||
? canvas.parentElement
|
||||
: null;
|
||||
if (!(frame instanceof HTMLElement)) return null;
|
||||
const bbox = normalizeEvidenceBBox(locator?.bbox);
|
||||
let overlay = frame.querySelector('[data-mnote-evidence-bbox-highlight]');
|
||||
if (!bbox) {
|
||||
if (overlay instanceof HTMLElement) overlay.hidden = true;
|
||||
return null;
|
||||
}
|
||||
const viewport = canvas.__mnotePdfViewport;
|
||||
if (!viewport || typeof viewport.convertToViewportRectangle !== 'function') return null;
|
||||
if (!(overlay instanceof HTMLElement)) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.className = 'mnote-resource-tab-bbox-highlight';
|
||||
overlay.setAttribute('data-mnote-evidence-bbox-highlight', 'true');
|
||||
frame.append(overlay);
|
||||
}
|
||||
const normalizedMineruBox = bbox.x0 >= 0 && bbox.y0 >= 0 && bbox.x1 <= 1000 && bbox.y1 <= 1000;
|
||||
let x;
|
||||
let y;
|
||||
let width;
|
||||
let height;
|
||||
if (normalizedMineruBox) {
|
||||
x = Math.min(bbox.x0, bbox.x1) / 1000 * viewport.width;
|
||||
y = Math.min(bbox.y0, bbox.y1) / 1000 * viewport.height;
|
||||
width = Math.max(1, Math.abs(bbox.x1 - bbox.x0) / 1000 * viewport.width);
|
||||
height = Math.max(1, Math.abs(bbox.y1 - bbox.y0) / 1000 * viewport.height);
|
||||
} else {
|
||||
const rect = viewport.convertToViewportRectangle([bbox.x0, bbox.y0, bbox.x1, bbox.y1]);
|
||||
x = Math.min(rect[0], rect[2]);
|
||||
y = Math.min(rect[1], rect[3]);
|
||||
width = Math.max(1, Math.abs(rect[2] - rect[0]));
|
||||
height = Math.max(1, Math.abs(rect[3] - rect[1]));
|
||||
}
|
||||
overlay.hidden = false;
|
||||
overlay.style.left = `${Math.max(0, x)}px`;
|
||||
overlay.style.top = `${Math.max(0, y)}px`;
|
||||
overlay.style.width = `${width}px`;
|
||||
overlay.style.height = `${height}px`;
|
||||
return overlay;
|
||||
};
|
||||
|
||||
const scrollPdfEvidenceTargetIntoView = (canvas, overlay) => {
|
||||
const target = overlay instanceof HTMLElement && !overlay.hidden
|
||||
? overlay
|
||||
: (canvas?.parentElement instanceof HTMLElement ? canvas.parentElement : canvas);
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
window.setTimeout(() => target.scrollIntoView({ block: 'center', inline: 'nearest' }), 0);
|
||||
};
|
||||
|
||||
const applyEvidenceLocatorToTextPanel = (entry, locator) => {
|
||||
if (!(entry?.panel instanceof HTMLElement) || !locator) return;
|
||||
const root = entry.panel.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
|
||||
@@ -1119,8 +1179,12 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
entry.panel.querySelectorAll('canvas.mnote-pdf-page[data-mnote-evidence-page="true"]').forEach((node) => {
|
||||
if (node instanceof HTMLElement) node.removeAttribute('data-mnote-evidence-page');
|
||||
});
|
||||
entry.panel.querySelectorAll('[data-mnote-evidence-bbox-highlight]').forEach((node) => {
|
||||
if (node instanceof HTMLElement) node.hidden = true;
|
||||
});
|
||||
canvas.setAttribute('data-mnote-evidence-page', 'true');
|
||||
canvas.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
const overlay = applyPdfEvidenceOverlayToCanvas(canvas, locator);
|
||||
scrollPdfEvidenceTargetIntoView(canvas, overlay);
|
||||
};
|
||||
|
||||
const applyEvidenceLocatorToEntry = (entry, input = {}) => {
|
||||
@@ -1328,16 +1392,23 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const scale = Math.max(0.6, Math.min(2.4, availableWidth / baseViewport.width));
|
||||
const viewport = page.getViewport({ scale });
|
||||
const outputScale = Math.min(2.5, Math.max(2, window.devicePixelRatio || 1));
|
||||
const frame = document.createElement('div');
|
||||
frame.setAttribute('data-mnote-pdf-page-frame', 'true');
|
||||
frame.style.position = 'relative';
|
||||
frame.style.width = `${Math.floor(viewport.width)}px`;
|
||||
frame.style.maxWidth = '100%';
|
||||
frame.style.margin = '0 auto 14px';
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.className = 'mnote-pdf-page';
|
||||
canvas.setAttribute('data-page-number', String(pageNumber));
|
||||
canvas.__mnotePdfViewport = viewport;
|
||||
canvas.width = Math.floor(viewport.width * outputScale);
|
||||
canvas.height = Math.floor(viewport.height * outputScale);
|
||||
canvas.style.display = 'block';
|
||||
canvas.style.maxWidth = '100%';
|
||||
canvas.style.width = `${Math.floor(viewport.width)}px`;
|
||||
canvas.style.height = `${Math.floor(viewport.height)}px`;
|
||||
canvas.style.margin = '0 auto 14px';
|
||||
canvas.style.margin = '0';
|
||||
canvas.style.background = '#fff';
|
||||
canvas.style.border = '1px solid #d8d8d2';
|
||||
canvas.style.boxShadow = '0 2px 10px rgba(25, 25, 22, .08)';
|
||||
@@ -1351,25 +1422,12 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const evidencePage = Number(evidenceLocator?.page || 0);
|
||||
if (evidencePage === pageNumber) {
|
||||
canvas.setAttribute('data-mnote-evidence-page', 'true');
|
||||
const bbox = normalizeEvidenceBBox(evidenceLocator?.bbox);
|
||||
if (bbox) {
|
||||
const rect = viewport.convertToViewportRectangle([bbox.x0, bbox.y0, bbox.x1, bbox.y1]);
|
||||
const x = Math.min(rect[0], rect[2]);
|
||||
const y = Math.min(rect[1], rect[3]);
|
||||
const width = Math.max(1, Math.abs(rect[2] - rect[0]));
|
||||
const height = Math.max(1, Math.abs(rect[3] - rect[1]));
|
||||
context.save();
|
||||
context.scale(outputScale, outputScale);
|
||||
context.fillStyle = 'rgba(37, 99, 235, 0.18)';
|
||||
context.strokeStyle = 'rgba(37, 99, 235, 0.9)';
|
||||
context.lineWidth = 2;
|
||||
context.fillRect(x, y, width, height);
|
||||
context.strokeRect(x, y, width, height);
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
if (entry.inlinePdfDocument === pdf) viewer.append(canvas);
|
||||
if (evidencePage === pageNumber) window.setTimeout(() => canvas.scrollIntoView({ block: 'center', inline: 'nearest' }), 0);
|
||||
frame.append(canvas);
|
||||
let evidenceOverlay = null;
|
||||
if (evidencePage === pageNumber) evidenceOverlay = applyPdfEvidenceOverlayToCanvas(canvas, evidenceLocator);
|
||||
if (entry.inlinePdfDocument === pdf) viewer.append(frame);
|
||||
if (evidencePage === pageNumber) scrollPdfEvidenceTargetIntoView(canvas, evidenceOverlay);
|
||||
};
|
||||
|
||||
const openInlinePdfResourceTab = async (entry, input) => {
|
||||
|
||||
@@ -41,6 +41,8 @@ export function createSidebarPageAiRenderRuntime(context) {
|
||||
pageAiSessionAgentFilterValue,
|
||||
pageAiSessionPreviewText,
|
||||
pageAiSessionStorageLabel,
|
||||
pageAiSessionSelected,
|
||||
pageAiSelectedSessionCount,
|
||||
pageAiSkillEnabled,
|
||||
pageAiSkillGroupCollapsed,
|
||||
pageAiSkillListEntries,
|
||||
@@ -601,6 +603,15 @@ export function createSidebarPageAiRenderRuntime(context) {
|
||||
sessionError.textContent = pageUiState.pageAiSessionError || '';
|
||||
sessionError.hidden = !pageUiState.pageAiSessionError;
|
||||
}
|
||||
var selectedSessionCount = pageAiSelectedSessionCount();
|
||||
var selectedSessionToolbar = drawer.querySelector('[data-page-ai-history-selection-toolbar]');
|
||||
if (selectedSessionToolbar instanceof HTMLElement) {
|
||||
selectedSessionToolbar.hidden = selectedSessionCount <= 0;
|
||||
}
|
||||
var selectedSessionCountNode = drawer.querySelector('[data-page-ai-history-selection-count]');
|
||||
if (selectedSessionCountNode instanceof HTMLElement) {
|
||||
selectedSessionCountNode.textContent = '已选 ' + String(selectedSessionCount) + ' 个';
|
||||
}
|
||||
var sessionSearch = drawer.querySelector('[data-page-ai-session-search]');
|
||||
if (sessionSearch instanceof HTMLInputElement && documentRef.activeElement !== sessionSearch) {
|
||||
sessionSearch.value = pageUiState.pageAiSessionSearchQuery || '';
|
||||
@@ -1009,6 +1020,11 @@ export function createSidebarPageAiRenderRuntime(context) {
|
||||
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="history" hidden>' +
|
||||
'<div class="wolai-page-ai-settings-head">' +
|
||||
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
|
||||
'<div class="wolai-page-ai-history-selection" data-page-ai-history-selection-toolbar hidden>' +
|
||||
'<span data-page-ai-history-selection-count>已选 0 个</span>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-clear-selection>取消</button>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost wolai-page-ai-ghost--danger" data-page-ai-session-delete-selected>删除</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<label class="wolai-page-ai-skill-search">' +
|
||||
'<span>搜索会话</span>' +
|
||||
@@ -1293,6 +1309,20 @@ export function createSidebarPageAiRenderRuntime(context) {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function pageAiHistoryTextKey(value) {
|
||||
return String(value || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/[.…]+$/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function pageAiHistoryPreviewIsDuplicate(title, preview) {
|
||||
var titleKey = pageAiHistoryTextKey(title);
|
||||
var previewKey = pageAiHistoryTextKey(preview);
|
||||
if (!titleKey || !previewKey) return false;
|
||||
return titleKey === previewKey || previewKey.indexOf(titleKey) === 0 || titleKey.indexOf(previewKey) === 0;
|
||||
}
|
||||
|
||||
function renderPageAiConversation() {
|
||||
var drawer = ensurePageAiDrawer();
|
||||
renderPageAiSuggestions();
|
||||
@@ -1308,22 +1338,28 @@ export function createSidebarPageAiRenderRuntime(context) {
|
||||
return;
|
||||
}
|
||||
conversation.innerHTML = historyRows.map(function(session) {
|
||||
var title = String(session.title || '新会话').trim() || '新会话';
|
||||
var preview = pageAiSessionPreviewText(session);
|
||||
var previewHtml = pageAiHistoryPreviewIsDuplicate(title, preview) ? '' : '<span>' + escapeHtml(preview) + '</span>';
|
||||
var active = session.id === pageUiState.pageAiActiveSessionId;
|
||||
var selected = pageAiSessionSelected(session.id);
|
||||
var usage = pageAiUsageSummary(session.usage);
|
||||
var agentLabel = pageAiSessionAgentLabel(session);
|
||||
var meta = [agentLabel, pageAiSessionStorageLabel(session), session.permissionLevel, session.shareId, session.status, usage].filter(Boolean).join(' · ');
|
||||
return '' +
|
||||
'<div class="wolai-page-ai-message wolai-page-ai-message--history' + (active ? ' is-active' : '') + '" data-page-ai-session-row="' + escapeHtml(session.id) + '" data-page-ai-session-agent="' + escapeHtml(pageAiSessionAgentFilterValue(session)) + '">' +
|
||||
'<button type="button" class="wolai-page-ai-message-text" data-page-ai-session="' + escapeHtml(session.id) + '">' +
|
||||
'<strong>' + escapeHtml(session.title || '新会话') + '</strong><br />' +
|
||||
'<span>' + escapeHtml(preview) + '</span>' +
|
||||
(meta ? '<br /><span class="wolai-page-ai-tool-meta">' + escapeHtml(meta) + '</span>' : '') +
|
||||
'<div class="wolai-page-ai-message wolai-page-ai-message--history' + (active ? ' is-active' : '') + (selected ? ' is-selected' : '') + '" data-page-ai-session-row="' + escapeHtml(session.id) + '" data-page-ai-session-agent="' + escapeHtml(pageAiSessionAgentFilterValue(session)) + '">' +
|
||||
'<button type="button" class="wolai-page-ai-session-check" data-page-ai-session-select="' + escapeHtml(session.id) + '" aria-checked="' + (selected ? 'true' : 'false') + '" aria-label="选择会话 ' + escapeHtml(session.title || session.id) + '">' +
|
||||
'<span>' + (selected ? '✓' : '') + '</span>' +
|
||||
'</button>' +
|
||||
'<div class="wolai-page-ai-message-actions">' +
|
||||
'<button type="button" class="wolai-page-ai-message-text wolai-page-ai-history-main" data-page-ai-session="' + escapeHtml(session.id) + '">' +
|
||||
'<strong>' + escapeHtml(title) + '</strong>' +
|
||||
previewHtml +
|
||||
(meta ? '<span class="wolai-page-ai-tool-meta">' + escapeHtml(meta) + '</span>' : '') +
|
||||
'</button>' +
|
||||
'<div class="wolai-page-ai-message-actions wolai-page-ai-history-actions">' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-resume="' + escapeHtml(session.id) + '">恢复</button>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-rename="' + escapeHtml(session.id) + '">重命名</button>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-delete="' + escapeHtml(session.id) + '">删除</button>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost wolai-page-ai-ghost--danger" data-page-ai-session-delete="' + escapeHtml(session.id) + '">删除</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
@@ -184,6 +184,7 @@ export function createSidebarPageAiRuntime(context) {
|
||||
pageAiSessionSearchResults: [],
|
||||
pageAiSessionSearchTimer: 0,
|
||||
pageAiSessionAgentFilter: 'all',
|
||||
pageAiSelectedSessionIds: {},
|
||||
pageAiSessionError: '',
|
||||
pageAiPermissionRequests: []
|
||||
};
|
||||
@@ -494,7 +495,12 @@ export function createSidebarPageAiRuntime(context) {
|
||||
const pageAiStartNewSession = (...args) => pageAiSessionRuntime.pageAiStartNewSession(...args);
|
||||
const pageAiRenameBackendSession = (...args) => pageAiSessionRuntime.pageAiRenameBackendSession(...args);
|
||||
const pageAiDeleteBackendSession = (...args) => pageAiSessionRuntime.pageAiDeleteBackendSession(...args);
|
||||
const pageAiDeleteSelectedBackendSessions = (...args) => pageAiSessionRuntime.pageAiDeleteSelectedBackendSessions(...args);
|
||||
const pageAiResumeBackendSession = (...args) => pageAiSessionRuntime.pageAiResumeBackendSession(...args);
|
||||
const pageAiSessionSelected = (...args) => pageAiSessionRuntime.pageAiSessionSelected(...args);
|
||||
const pageAiToggleSessionSelection = (...args) => pageAiSessionRuntime.pageAiToggleSessionSelection(...args);
|
||||
const pageAiClearSessionSelection = (...args) => pageAiSessionRuntime.pageAiClearSessionSelection(...args);
|
||||
const pageAiSelectedSessionCount = (...args) => pageAiSessionRuntime.pageAiSelectedSessionCount(...args);
|
||||
const pageAiCheckActiveRun = (...args) => pageAiSessionRuntime.pageAiCheckActiveRun(...args);
|
||||
const pageAiSkillRuntime = createSidebarPageAiSkillRuntime({
|
||||
pageAiCurrentAgentId,
|
||||
@@ -632,6 +638,8 @@ export function createSidebarPageAiRuntime(context) {
|
||||
pageAiSessionAgentLabel,
|
||||
pageAiSessionPreviewText,
|
||||
pageAiSessionStorageLabel,
|
||||
pageAiSessionSelected,
|
||||
pageAiSelectedSessionCount,
|
||||
pageAiSkillEnabled,
|
||||
pageAiSkillGroupCollapsed,
|
||||
pageAiSkillListEntries,
|
||||
@@ -3244,6 +3252,30 @@ export function createSidebarPageAiRuntime(context) {
|
||||
void pageAiToggleTool(pageAiToolToggle.getAttribute('data-page-ai-tool-toggle') || '', pageAiToolToggle.getAttribute('aria-pressed') !== 'true');
|
||||
return true;
|
||||
}
|
||||
var pageAiSessionSelect = closestAction(event.target, '[data-page-ai-session-select]');
|
||||
if (pageAiSessionSelect) {
|
||||
event.preventDefault();
|
||||
pageAiToggleSessionSelection(
|
||||
pageAiSessionSelect.getAttribute('data-page-ai-session-select') || '',
|
||||
pageAiSessionSelect.getAttribute('aria-checked') !== 'true'
|
||||
);
|
||||
return true;
|
||||
}
|
||||
var pageAiClearSelectedSessions = closestAction(event.target, '[data-page-ai-session-clear-selection]');
|
||||
if (pageAiClearSelectedSessions) {
|
||||
event.preventDefault();
|
||||
pageAiClearSessionSelection();
|
||||
return true;
|
||||
}
|
||||
var pageAiDeleteSelectedSessions = closestAction(event.target, '[data-page-ai-session-delete-selected]');
|
||||
if (pageAiDeleteSelectedSessions) {
|
||||
event.preventDefault();
|
||||
void pageAiDeleteSelectedBackendSessions().catch(function(error) {
|
||||
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
|
||||
renderPageAiControls();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
var pageAiSessionResume = closestAction(event.target, '[data-page-ai-session-resume]');
|
||||
if (pageAiSessionResume) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -519,6 +519,49 @@ export function createSidebarPageAiSessionRuntime(context) {
|
||||
}) || 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;
|
||||
@@ -585,30 +628,87 @@ export function createSidebarPageAiSessionRuntime(context) {
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
async function pageAiDeleteBackendSession(sessionId) {
|
||||
var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
|
||||
if (!session) return;
|
||||
if (!win.confirm('确定删除 AI 会话“' + (session.title || sessionId) + '”吗?')) return;
|
||||
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '?' + pageAiBackendSessionQuery({}), {
|
||||
method: 'DELETE',
|
||||
headers: { 'accept': 'application/json' }
|
||||
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;
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'session_delete_failed_' + response.status));
|
||||
}
|
||||
pageUiState.pageAiSessions = pageUiState.pageAiSessions.filter(function(item) { return item.id !== sessionId; });
|
||||
if (pageUiState.pageAiActiveSessionId === sessionId) {
|
||||
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) + '”吗?'
|
||||
: '确定删除选中的 ' + String(sessions.length) + ' 个 AI 会话吗?';
|
||||
if (!win.confirm(confirmText)) return;
|
||||
for (var index = 0; index < sessions.length; index += 1) {
|
||||
var session = sessions[index];
|
||||
if (!pageAiSessionRequiresBackendDelete(session)) continue;
|
||||
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(session.id) + '?' + pageAiBackendSessionQuery({}), {
|
||||
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;
|
||||
@@ -676,11 +776,16 @@ export function createSidebarPageAiSessionRuntime(context) {
|
||||
pageAiEnsureHermesSession,
|
||||
pageAiRestoreHermesSession,
|
||||
pageAiCurrentSession,
|
||||
pageAiSessionSelected,
|
||||
pageAiToggleSessionSelection,
|
||||
pageAiClearSessionSelection,
|
||||
pageAiSelectedSessionCount,
|
||||
pageAiSyncCurrentSessionMessages,
|
||||
pageAiSetActiveSession,
|
||||
pageAiStartNewSession,
|
||||
pageAiRenameBackendSession,
|
||||
pageAiDeleteBackendSession,
|
||||
pageAiDeleteSelectedBackendSessions,
|
||||
pageAiResumeBackendSession,
|
||||
pageAiCheckActiveRun
|
||||
};
|
||||
|
||||
@@ -30,6 +30,9 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
ppt: { mode: 'wide', resolvedMode: 'wide', cssMaxWidth: '1180px', source: 'system' },
|
||||
mindmap: { mode: 'full', resolvedMode: 'full', cssMaxWidth: 'none', source: 'system' }
|
||||
};
|
||||
var knowledgeRagPipelineEventSource = null;
|
||||
var knowledgeRagPipelineWatcherScopeKey = '';
|
||||
var knowledgeRagPipelineWatcherRefreshPending = false;
|
||||
|
||||
function defaultPageOptions() {
|
||||
return {
|
||||
@@ -71,11 +74,6 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
return pageUiState.knowledgeRagSummary || {};
|
||||
}
|
||||
|
||||
var knowledgeRagBridgeTimer = 0;
|
||||
var knowledgeRagBridgeGeneration = 0;
|
||||
var knowledgeRagBridgeAttempt = 0;
|
||||
var knowledgeRagBridgeDelays = [1200, 2000, 3200, 5000, 8000, 13000, 21000, 34000];
|
||||
|
||||
function pageWidthModeLabel(mode) {
|
||||
if (mode === 'inherit') return '继承默认';
|
||||
if (mode === 'readable') return '阅读';
|
||||
@@ -1099,21 +1097,85 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
}
|
||||
|
||||
function knowledgeRagStatusHasInFlight(status) {
|
||||
var pipeline = status && status.pipeline && typeof status.pipeline === 'object' ? status.pipeline : {};
|
||||
if (pipeline.busy === true || pipeline.scanning === true || pipeline.destructiveBusy === true || pipeline.requestPending === true || pipeline.pendingRequests === true || Number(pipeline.pendingEnqueues || 0) > 0) return true;
|
||||
var registry = status && status.registry && typeof status.registry === 'object' ? status.registry : {};
|
||||
var entries = Array.isArray(registry.entries) ? registry.entries : [];
|
||||
if (entries.some(function(entry) {
|
||||
var providerStatus = String(entry && entry.lightRagStatus || '').trim();
|
||||
return !entry?.deletedAtMs && !entry?.stale && !(entry?.indexedAtMs && entry?.lightRagDocId)
|
||||
&& ['submitted', 'pending', 'processing', 'parsed', ''].includes(providerStatus);
|
||||
&& ['submitted', 'pending', 'scanning', 'parsing', 'parse', 'parsed', 'analyzing', 'analyze', 'processing', 'process', ''].includes(providerStatus);
|
||||
})) return true;
|
||||
var groups = status && status.documents && status.documents.rawStatusGroups && typeof status.documents.rawStatusGroups === 'object'
|
||||
? status.documents.rawStatusGroups
|
||||
: {};
|
||||
return ['pending', 'parse', 'parsed', 'analyze', 'processing', 'process'].some(function(key) {
|
||||
return ['pending', 'scanning', 'parsing', 'parse', 'parsed', 'analyzing', 'analyze', 'processing', 'process'].some(function(key) {
|
||||
return Number(groups[key] || 0) > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function knowledgeRagPipelineHasInFlight(pipeline) {
|
||||
pipeline = pipeline && typeof pipeline === 'object' ? pipeline : {};
|
||||
return pipeline.busy === true
|
||||
|| pipeline.scanning === true
|
||||
|| pipeline.destructiveBusy === true
|
||||
|| pipeline.requestPending === true
|
||||
|| pipeline.pendingRequests === true
|
||||
|| Number(pipeline.pendingEnqueues || 0) > 0;
|
||||
}
|
||||
|
||||
function parseKnowledgeRagChunkProgress(message) {
|
||||
var text = String(message || '');
|
||||
var match = text.match(/Chunk\s+(\d+)\s+of\s+(\d+).*?\b(doc-[A-Za-z0-9_-]+?)(?=-(?:chunk|mm)-|\s|$)/i);
|
||||
if (!match) return null;
|
||||
var current = Number(match[1]);
|
||||
var total = Number(match[2]);
|
||||
if (!Number.isFinite(current) || !Number.isFinite(total) || current <= 0 || total <= 0) return null;
|
||||
return {
|
||||
current: Math.min(current, total),
|
||||
total: total,
|
||||
docId: String(match[3] || ''),
|
||||
latestMessage: text
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeKnowledgeRagPipelineEvent(payload) {
|
||||
var input = payload && typeof payload === 'object' ? payload : {};
|
||||
var latestMessage = String(input.latestMessage || input.latest_message || '');
|
||||
var historyMessages = Array.isArray(input.historyMessages)
|
||||
? input.historyMessages
|
||||
: (Array.isArray(input.history_messages) ? input.history_messages : []);
|
||||
var progress = input.progress && typeof input.progress === 'object'
|
||||
? input.progress
|
||||
: parseKnowledgeRagChunkProgress(latestMessage);
|
||||
if (!progress) {
|
||||
for (var index = historyMessages.length - 1; index >= 0; index -= 1) {
|
||||
progress = parseKnowledgeRagChunkProgress(historyMessages[index]);
|
||||
if (progress) break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: input.ok !== false,
|
||||
busy: input.busy === true,
|
||||
destructiveBusy: input.destructiveBusy === true || input.destructive_busy === true,
|
||||
scanning: input.scanning === true,
|
||||
scanningExclusive: input.scanningExclusive === true || input.scanning_exclusive === true,
|
||||
requestPending: input.requestPending === true || input.request_pending === true,
|
||||
pendingRequests: input.pendingRequests === true || input.pending_requests === true,
|
||||
pendingEnqueues: Number(input.pendingEnqueues ?? input.pending_enqueues ?? 0) || 0,
|
||||
docs: Number(input.docs || 0) || 0,
|
||||
batches: Number(input.batches ?? input.batchs ?? 0) || 0,
|
||||
currentBatch: Number(input.currentBatch ?? input.cur_batch ?? 0) || 0,
|
||||
jobName: String(input.jobName || input.job_name || ''),
|
||||
jobStart: input.jobStart || input.job_start || null,
|
||||
latestMessage: latestMessage,
|
||||
historyMessages: historyMessages.map(function(message) { return String(message || ''); }),
|
||||
cancellationRequested: input.cancellationRequested === true || input.cancellation_requested === true,
|
||||
cancellationReason: input.cancellationReason || input.cancellation_reason || null,
|
||||
progress: progress || null
|
||||
};
|
||||
}
|
||||
|
||||
function knowledgeRagPipelineProgress(status) {
|
||||
var pipeline = status && status.pipeline && typeof status.pipeline === 'object' ? status.pipeline : {};
|
||||
var progress = pipeline.progress && typeof pipeline.progress === 'object' ? pipeline.progress : null;
|
||||
@@ -1131,35 +1193,113 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
|
||||
function knowledgeRagSourceProgressHtml(progress) {
|
||||
if (!progress) return '';
|
||||
var percent = Math.max(0, Math.min(100, Math.round((progress.current / progress.total) * 100)));
|
||||
var label = progress.current + ' / ' + progress.total + ' · ' + percent + '%';
|
||||
var current = Number(progress.current || 0);
|
||||
var total = Number(progress.total || 0);
|
||||
var determinate = Number.isFinite(current) && Number.isFinite(total) && current > 0 && total > 0;
|
||||
var percent = determinate ? Math.max(0, Math.min(100, Math.round((current / total) * 100))) : 0;
|
||||
var label = determinate ? current + ' / ' + total + ' · ' + percent + '%' : String(progress.label || '处理中');
|
||||
var mode = determinate ? 'determinate' : 'indeterminate';
|
||||
var ariaValues = determinate
|
||||
? ' aria-valuemin="0" aria-valuemax="' + String(total) + '" aria-valuenow="' + String(current) + '"'
|
||||
: ' aria-valuetext="' + escapeHtml(label) + '"';
|
||||
return '' +
|
||||
'<div class="mnote-knowledge-rag-source-progress" role="progressbar" aria-valuemin="0" aria-valuemax="' + String(progress.total) + '" aria-valuenow="' + String(progress.current) + '" aria-label="' + escapeHtml('索引进度 ' + label) + '">' +
|
||||
'<div><i style="width:' + String(percent) + '%"></i></div>' +
|
||||
'<div class="mnote-knowledge-rag-source-progress" data-progress-mode="' + mode + '" role="progressbar"' + ariaValues + ' aria-label="' + escapeHtml('索引进度 ' + label) + '">' +
|
||||
'<div><i' + (determinate ? ' style="width:' + String(percent) + '%"' : '') + '></i></div>' +
|
||||
'<span>' + escapeHtml(label) + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function scheduleKnowledgeRagStatusBridge(reason) {
|
||||
if (!knowledgeRagIsAvailable()) return;
|
||||
if (knowledgeRagBridgeTimer) window.clearTimeout(knowledgeRagBridgeTimer);
|
||||
knowledgeRagBridgeGeneration += 1;
|
||||
knowledgeRagBridgeAttempt = 0;
|
||||
runKnowledgeRagStatusBridge(knowledgeRagBridgeGeneration, reason || 'source-updated');
|
||||
function knowledgeRagDocumentIsInFlight(doc) {
|
||||
var status = String(doc && (doc.statusGroup || doc.status) || '').toLowerCase();
|
||||
return ['pending', 'scanning', 'parsing', 'parse', 'parsed', 'analyzing', 'analyze', 'processing', 'process'].includes(status);
|
||||
}
|
||||
|
||||
function runKnowledgeRagStatusBridge(generation, reason) {
|
||||
if (generation !== knowledgeRagBridgeGeneration || !knowledgeRagIsAvailable()) return;
|
||||
var delay = knowledgeRagBridgeDelays[Math.min(knowledgeRagBridgeAttempt, knowledgeRagBridgeDelays.length - 1)];
|
||||
knowledgeRagBridgeTimer = window.setTimeout(async function() {
|
||||
if (generation !== knowledgeRagBridgeGeneration || !knowledgeRagIsAvailable()) return;
|
||||
knowledgeRagBridgeAttempt += 1;
|
||||
await loadKnowledgeRagStatus(true, { silentLoading: true, notifyUpdated: true, reason: reason || 'bridge' });
|
||||
var status = currentKnowledgeRagSummary().status || {};
|
||||
if (knowledgeRagStatusHasInFlight(status) && knowledgeRagBridgeAttempt < knowledgeRagBridgeDelays.length) {
|
||||
runKnowledgeRagStatusBridge(generation, reason || 'bridge');
|
||||
function knowledgeRagProgressLabel(status, doc, fallback) {
|
||||
var pipeline = status && status.pipeline && typeof status.pipeline === 'object' ? status.pipeline : {};
|
||||
var latest = String(pipeline.latestMessage || '').trim();
|
||||
if (latest) return '处理中';
|
||||
var providerStatus = String(doc && (doc.statusGroup || doc.status) || fallback || '').toLowerCase();
|
||||
if (providerStatus === 'pending') return '排队中';
|
||||
if (providerStatus === 'scanning') return '扫描中';
|
||||
if (providerStatus === 'parsing' || providerStatus === 'parse' || providerStatus === 'parsed') return '解析中';
|
||||
if (providerStatus === 'analyzing' || providerStatus === 'analyze') return '分析中';
|
||||
return '索引中';
|
||||
}
|
||||
|
||||
function knowledgeRagIndeterminateProgress(status, doc, fallback) {
|
||||
return { mode: 'indeterminate', label: knowledgeRagProgressLabel(status, doc, fallback) };
|
||||
}
|
||||
|
||||
function stopKnowledgeRagPipelineWatcher() {
|
||||
if (knowledgeRagPipelineEventSource && typeof knowledgeRagPipelineEventSource.close === 'function') {
|
||||
knowledgeRagPipelineEventSource.close();
|
||||
}
|
||||
knowledgeRagPipelineEventSource = null;
|
||||
knowledgeRagPipelineWatcherScopeKey = '';
|
||||
}
|
||||
|
||||
function handleKnowledgeRagPipelineEvent(payload) {
|
||||
var scopeKey = knowledgeRagScopeKey();
|
||||
if (knowledgeRagPipelineWatcherScopeKey && knowledgeRagPipelineWatcherScopeKey !== scopeKey) return;
|
||||
var current = currentKnowledgeRagSummary();
|
||||
var currentStatus = current.status && typeof current.status === 'object' ? current.status : {};
|
||||
var previousPipeline = currentStatus.pipeline && typeof currentStatus.pipeline === 'object' ? currentStatus.pipeline : {};
|
||||
var previousInFlight = knowledgeRagPipelineHasInFlight(previousPipeline);
|
||||
var pipeline = normalizeKnowledgeRagPipelineEvent(payload);
|
||||
var nextStatus = Object.assign({}, currentStatus, { pipeline: pipeline });
|
||||
pageUiState.knowledgeRagSummary = Object.assign({}, current, {
|
||||
scopeKey: scopeKey,
|
||||
loading: false,
|
||||
error: '',
|
||||
status: nextStatus
|
||||
});
|
||||
if (isKnowledgeRagSettingsOpen()) renderKnowledgeRagSettings();
|
||||
var currentInFlight = knowledgeRagPipelineHasInFlight(pipeline);
|
||||
if (currentInFlight) return;
|
||||
var latest = String(pipeline.latestMessage || '').toLowerCase();
|
||||
var finished = previousInFlight || latest.indexOf('completed') !== -1 || latest.indexOf('complete') !== -1;
|
||||
if (!finished || knowledgeRagPipelineWatcherRefreshPending) return;
|
||||
knowledgeRagPipelineWatcherRefreshPending = true;
|
||||
void loadKnowledgeRagStatus(true, {
|
||||
silentLoading: true,
|
||||
notifyUpdated: true,
|
||||
reason: 'pipeline-event-complete'
|
||||
}).finally(function() {
|
||||
knowledgeRagPipelineWatcherRefreshPending = false;
|
||||
if (!knowledgeRagStatusHasInFlight(currentKnowledgeRagSummary().status || {})) {
|
||||
stopKnowledgeRagPipelineWatcher();
|
||||
}
|
||||
}, delay);
|
||||
});
|
||||
}
|
||||
|
||||
function startKnowledgeRagPipelineWatcher(reason) {
|
||||
document.documentElement.setAttribute('data-mnote-knowledge-rag-watcher-required', String(reason || 'source-updated'));
|
||||
if (!knowledgeRagIsAvailable() || typeof window.EventSource !== 'function') return;
|
||||
var scopeKey = knowledgeRagScopeKey();
|
||||
if (knowledgeRagPipelineEventSource && knowledgeRagPipelineWatcherScopeKey === scopeKey) return;
|
||||
stopKnowledgeRagPipelineWatcher();
|
||||
var url = new URL('/api/knowledge-rag/pipeline-events', window.location.origin);
|
||||
url.searchParams.set('workspaceId', resolveWorkspaceId(document.body));
|
||||
url.searchParams.set('rootUri', currentRootUri());
|
||||
var source = new EventSource(url.toString());
|
||||
knowledgeRagPipelineEventSource = source;
|
||||
knowledgeRagPipelineWatcherScopeKey = scopeKey;
|
||||
source.addEventListener('pipeline_status', function(event) {
|
||||
var payload = null;
|
||||
try {
|
||||
payload = JSON.parse(event.data || '{}');
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
handleKnowledgeRagPipelineEvent(payload);
|
||||
});
|
||||
source.onerror = function() {
|
||||
document.documentElement.setAttribute('data-mnote-knowledge-rag-watcher-error', 'true');
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleKnowledgeRagStatusBridge(reason) {
|
||||
startKnowledgeRagPipelineWatcher(reason);
|
||||
}
|
||||
|
||||
function knowledgeRagUnmappedProviderRows(status, entries) {
|
||||
@@ -1245,6 +1385,15 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
return;
|
||||
}
|
||||
var pipelineProgress = knowledgeRagPipelineProgress(status || {});
|
||||
var providerDocs = knowledgeRagProviderDocuments(status || {});
|
||||
var providerDocsById = new Map();
|
||||
var providerDocsByPath = new Map();
|
||||
providerDocs.forEach(function(doc) {
|
||||
var docId = String(doc && doc.id || '').trim();
|
||||
var filePath = String(doc && doc.filePath || '').trim();
|
||||
if (docId) providerDocsById.set(docId, doc);
|
||||
if (filePath) providerDocsByPath.set(filePath, doc);
|
||||
});
|
||||
sourcesNode.innerHTML = items.map(function(row) {
|
||||
if (row.kind === 'provider') {
|
||||
var doc = row.doc || {};
|
||||
@@ -1252,6 +1401,9 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var docStatus = String(doc.status || doc.statusGroup || '');
|
||||
var failedDoc = row.category === 'unmapped_failed';
|
||||
var providerProgress = pipelineProgress && pipelineProgress.docId && pipelineProgress.docId === String(doc.id || '') ? pipelineProgress : null;
|
||||
if (!providerProgress && !failedDoc && knowledgeRagDocumentIsInFlight(doc)) {
|
||||
providerProgress = knowledgeRagIndeterminateProgress(status || {}, doc, docStatus);
|
||||
}
|
||||
return '' +
|
||||
'<div class="mnote-knowledge-rag-source-row" data-knowledge-rag-source-row="true" data-knowledge-rag-source-kind="provider" data-knowledge-rag-source-path="' + escapeHtml(providerPath) + '">' +
|
||||
'<span class="wolai-page-settings-index-status-dot" data-index-status="' + (failedDoc ? 'fault' : 'indexed') + '" title="' + escapeHtml(docStatus || 'LightRAG 文档') + '"></span>' +
|
||||
@@ -1286,6 +1438,10 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var canReindex = Boolean(sourcePath && !deleting);
|
||||
var canDelete = Boolean(sourcePath && !deleted && !deletedDone && !deleting);
|
||||
var entryProgress = pipelineProgress && pipelineProgress.docId && pipelineProgress.docId === docId && !indexed && !failed && !stale && !deleted && !deletedDone ? pipelineProgress : null;
|
||||
var providerDoc = (docId && providerDocsById.get(docId)) || providerDocsByPath.get(String(entry && entry.lightRagFilePath || '').trim()) || null;
|
||||
if (!entryProgress && !indexed && !failed && !stale && !deleted && !deletedDone && !deleting && (knowledgeRagDocumentIsInFlight(providerDoc) || docId || providerStatus)) {
|
||||
entryProgress = knowledgeRagIndeterminateProgress(status || {}, providerDoc, providerStatus);
|
||||
}
|
||||
return '' +
|
||||
'<div class="mnote-knowledge-rag-source-row" data-knowledge-rag-source-row="true" data-knowledge-rag-source-path="' + escapeHtml(sourcePath) + '">' +
|
||||
'<span class="wolai-page-settings-index-status-dot" data-index-status="' + kind + '" title="' + escapeHtml(statusLabel) + '"></span>' +
|
||||
@@ -1396,6 +1552,8 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
}
|
||||
var scopeKey = knowledgeRagScopeKey();
|
||||
var current = currentKnowledgeRagSummary();
|
||||
var previousStatus = current.status || {};
|
||||
var previousDigest = knowledgeRagRegistryStatusDigest(previousStatus);
|
||||
if (!force && current.scopeKey === scopeKey && !current.error && !current.loading) {
|
||||
renderKnowledgeRagSettings();
|
||||
return;
|
||||
@@ -1419,14 +1577,20 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(payload && payload.message ? payload.message : 'knowledge_rag_status_' + response.status);
|
||||
}
|
||||
var nextDigest = knowledgeRagRegistryStatusDigest(payload);
|
||||
pageUiState.knowledgeRagSummary = {
|
||||
scopeKey: scopeKey,
|
||||
loading: false,
|
||||
error: '',
|
||||
status: payload
|
||||
};
|
||||
if (options.notifyUpdated) {
|
||||
notifyKnowledgeRagSourceUpdated({ action: 'status-sync', reason: options.reason || 'bridge', status: payload });
|
||||
if (knowledgeRagStatusHasInFlight(payload)) {
|
||||
startKnowledgeRagPipelineWatcher(options.reason || 'status-in-flight');
|
||||
} else if (!knowledgeRagPipelineWatcherRefreshPending) {
|
||||
stopKnowledgeRagPipelineWatcher();
|
||||
}
|
||||
if (options.notifyUpdated || previousDigest !== nextDigest) {
|
||||
notifyKnowledgeRagSourceUpdated({ action: 'status-sync', reason: options.reason || 'registry-status-changed', status: payload });
|
||||
}
|
||||
} catch (error) {
|
||||
pageUiState.knowledgeRagSummary = {
|
||||
@@ -1495,6 +1659,21 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
}));
|
||||
}
|
||||
|
||||
function knowledgeRagRegistryStatusDigest(status) {
|
||||
var registry = status && status.registry && typeof status.registry === 'object' ? status.registry : {};
|
||||
var entries = Array.isArray(registry.entries) ? registry.entries : [];
|
||||
return entries.map(function(entry) {
|
||||
return [
|
||||
String(entry && entry.sourceRootRelativePath || ''),
|
||||
String(entry && entry.lightRagDocId || ''),
|
||||
String(entry && entry.lightRagStatus || ''),
|
||||
String(entry && entry.indexedAtMs || ''),
|
||||
String(entry && entry.deletedAtMs || ''),
|
||||
entry && entry.stale ? 'stale' : 'fresh'
|
||||
].join('|');
|
||||
}).sort().join('\n');
|
||||
}
|
||||
|
||||
function removeKnowledgeRagSourceInput(button) {
|
||||
var row = button && button.closest ? button.closest('[data-knowledge-rag-source-input-row]') : null;
|
||||
if (row instanceof HTMLElement) row.remove();
|
||||
|
||||
@@ -158,6 +158,14 @@ impl AcpSessionManager {
|
||||
.and_then(Value::as_str)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
params
|
||||
.get("toolCall")
|
||||
.and_then(|tool_call| tool_call.get("toolCallId"))
|
||||
.and_then(Value::as_str)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
})
|
||||
.unwrap_or_else(|| format!("acp_perm_{}", id));
|
||||
|
||||
let tool_name = params
|
||||
@@ -165,6 +173,16 @@ impl AcpSessionManager {
|
||||
.or_else(|| params.get("tool"))
|
||||
.or_else(|| params.get("name"))
|
||||
.or_else(|| params.get("method"))
|
||||
.or_else(|| {
|
||||
params
|
||||
.get("toolCall")
|
||||
.and_then(|tool_call| tool_call.get("title"))
|
||||
})
|
||||
.or_else(|| {
|
||||
params
|
||||
.get("toolCall")
|
||||
.and_then(|tool_call| tool_call.get("kind"))
|
||||
})
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("session/request_permission")
|
||||
.to_string();
|
||||
@@ -333,6 +351,18 @@ impl AcpSessionManager {
|
||||
.get("toolName")
|
||||
.or_else(|| pending.params.get("tool"))
|
||||
.or_else(|| pending.params.get("name"))
|
||||
.or_else(|| {
|
||||
pending
|
||||
.params
|
||||
.get("toolCall")
|
||||
.and_then(|tool_call| tool_call.get("title"))
|
||||
})
|
||||
.or_else(|| {
|
||||
pending
|
||||
.params
|
||||
.get("toolCall")
|
||||
.and_then(|tool_call| tool_call.get("kind"))
|
||||
})
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("session/request_permission")
|
||||
.to_string();
|
||||
@@ -827,8 +857,10 @@ fn permission_response_for_decision(
|
||||
let option_id = permission_option_id_by_decision(params, decision);
|
||||
match (decision, option_id) {
|
||||
("allow", Some(option_id)) | ("deny", Some(option_id)) => Some(Ok(json!({
|
||||
"outcome": "selected",
|
||||
"optionId": option_id
|
||||
"outcome": {
|
||||
"outcome": "selected",
|
||||
"optionId": option_id
|
||||
}
|
||||
}))),
|
||||
("deny", None) => Some(Err((-32000, "permission denied by user".into()))),
|
||||
_ => None,
|
||||
@@ -1271,13 +1303,44 @@ rl.on('line', (line) => {
|
||||
let allow = permission_response_for_decision(¶ms, "allow")
|
||||
.expect("allow response")
|
||||
.expect("allow should select an option");
|
||||
assert_eq!(allow["outcome"], "selected");
|
||||
assert_eq!(allow["optionId"], "allow_once");
|
||||
assert_eq!(allow["outcome"]["outcome"], "selected");
|
||||
assert_eq!(allow["outcome"]["optionId"], "allow_once");
|
||||
|
||||
let deny = permission_response_for_decision(¶ms, "deny")
|
||||
.expect("deny response")
|
||||
.expect("deny should select an option");
|
||||
assert_eq!(deny["outcome"], "selected");
|
||||
assert_eq!(deny["optionId"], "reject_once");
|
||||
assert_eq!(deny["outcome"]["outcome"], "selected");
|
||||
assert_eq!(deny["outcome"]["optionId"], "reject_once");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_response_matches_reasonix_v2_acp_result_shape() {
|
||||
let params = json!({
|
||||
"sessionId": "sess_1",
|
||||
"toolCall": {
|
||||
"toolCallId": "gate-call_1",
|
||||
"title": "bash",
|
||||
"kind": "execute",
|
||||
"rawInput": {"command": "reasonix --version"}
|
||||
},
|
||||
"options": [
|
||||
{"optionId": "allow_once", "name": "Allow", "kind": "allow_once"},
|
||||
{"optionId": "allow_always", "name": "Allow Bash(reasonix --version)", "kind": "allow_always"},
|
||||
{"optionId": "reject_once", "name": "Reject", "kind": "reject_once"}
|
||||
]
|
||||
});
|
||||
|
||||
let allow = permission_response_for_decision(¶ms, "allow")
|
||||
.expect("allow response")
|
||||
.expect("allow should select an option");
|
||||
assert_eq!(
|
||||
allow,
|
||||
json!({
|
||||
"outcome": {
|
||||
"outcome": "selected",
|
||||
"optionId": "allow_once"
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ async fn log_failed_response(request: Request, next: Next) -> Response {
|
||||
uri = %uri,
|
||||
status = %status,
|
||||
error_code = %error_code,
|
||||
"退役 Convex 兼容路径被请求"
|
||||
"退役 legacy cloud/Convex 兼容路径被请求"
|
||||
);
|
||||
} else {
|
||||
error!(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::app::{AppConfig, AppState};
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::transport::convex::{
|
||||
use crate::transport::legacy_cloud_guard::{
|
||||
execute_retired_command_plan, execute_retired_command_plan_with_artifacts,
|
||||
RetiredCloudCommandExecution,
|
||||
};
|
||||
|
||||
@@ -208,7 +208,7 @@ fn ok_response(context: &RequestContext, result: Value) -> (StatusCode, HeaderMa
|
||||
}
|
||||
|
||||
fn execution_artifacts_json(
|
||||
execution: &crate::transport::convex::RetiredCloudCommandExecution,
|
||||
execution: &crate::transport::legacy_cloud_guard::RetiredCloudCommandExecution,
|
||||
) -> Value {
|
||||
execution
|
||||
.artifacts
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::routes::web_shell::{
|
||||
render_local_file_tree_html_scoped, render_local_sidebar_tree_html,
|
||||
render_local_sidebar_tree_html_from_snapshot,
|
||||
};
|
||||
use crate::transport::convex::execute_retired_mutation_by_name;
|
||||
use crate::transport::legacy_cloud_guard::execute_retired_mutation_by_name;
|
||||
use crate::workspace_shell::{
|
||||
apply_active_page, build_workspace_shell_projection, render_workspace_shell_sidebar_html,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,9 @@ use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::hermes_tools::manifest;
|
||||
use crate::transport::convex::{execute_retired_mutation_by_name, execute_retired_query_by_name};
|
||||
use crate::transport::legacy_cloud_guard::{
|
||||
execute_retired_mutation_by_name, execute_retired_query_by_name,
|
||||
};
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Extension, Path, Query, State};
|
||||
use axum::http::{header, HeaderMap, HeaderName, HeaderValue, StatusCode};
|
||||
|
||||
@@ -3,11 +3,14 @@ use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::routes::evidence::{citation_markdown_for_locator, citation_url_for_locator};
|
||||
use crate::routes::local_folder_source;
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Extension, Json, Query, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::response::Response;
|
||||
use core_protocol::evidence::{
|
||||
EvidenceBBox, EvidenceLocator, EvidenceOpenAction, EvidenceResourceKind,
|
||||
};
|
||||
use futures_util::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
@@ -327,6 +330,59 @@ pub async fn status(
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn pipeline_events(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
) -> Result<Response, WebError> {
|
||||
let endpoint = lightrag_endpoint();
|
||||
let url = format!(
|
||||
"{}/documents/pipeline_status/events",
|
||||
endpoint.trim_end_matches('/')
|
||||
);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
.map_err(|error| {
|
||||
WebError::internal(format!("LightRAG SSE client 初始化失败: {error}"))
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let mut request = client.get(&url).header("accept", "text/event-stream");
|
||||
if let Some(api_key) = lightrag_api_key() {
|
||||
request = request.header("X-API-Key", api_key);
|
||||
}
|
||||
let upstream = request.send().await.map_err(|error| {
|
||||
WebError::bad_gateway_code(
|
||||
"knowledge_rag_lightrag_events_unreachable",
|
||||
format!("无法连接 LightRAG pipeline 事件流: {error}"),
|
||||
)
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let upstream_status = upstream.status();
|
||||
if !upstream_status.is_success() {
|
||||
let text = upstream.text().await.unwrap_or_default();
|
||||
return Err(WebError::bad_gateway_code(
|
||||
"knowledge_rag_lightrag_events_error",
|
||||
format!("LightRAG pipeline 事件流返回 HTTP {upstream_status}: {text}"),
|
||||
)
|
||||
.with_context(&context));
|
||||
}
|
||||
let stream = upstream.bytes_stream().map_err(|error| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("LightRAG pipeline 事件流读取失败: {error}"),
|
||||
)
|
||||
});
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "text/event-stream; charset=utf-8")
|
||||
.header(header::CACHE_CONTROL, "no-cache, no-transform")
|
||||
.header("x-accel-buffering", "no")
|
||||
.body(Body::from_stream(stream))
|
||||
.map_err(|error| {
|
||||
WebError::internal(format!("LightRAG pipeline 事件响应构造失败: {error}"))
|
||||
.with_context(&context)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn sync_registry_for_root(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
@@ -1447,13 +1503,29 @@ fn lightrag_pipeline_status_summary(value: &Value) -> Value {
|
||||
.get("latest_message")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let progress = parse_lightrag_chunk_progress(latest_message);
|
||||
let history_messages = lightrag_pipeline_history_messages(value);
|
||||
let progress = parse_lightrag_chunk_progress(latest_message).or_else(|| {
|
||||
history_messages
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|message| parse_lightrag_chunk_progress(message))
|
||||
});
|
||||
json!({
|
||||
"ok": true,
|
||||
"busy": value.get("busy").and_then(Value::as_bool).unwrap_or(false),
|
||||
"destructiveBusy": value.get("destructive_busy").and_then(Value::as_bool).unwrap_or(false),
|
||||
"scanning": value.get("scanning").and_then(Value::as_bool).unwrap_or(false),
|
||||
"scanningExclusive": value.get("scanning_exclusive").and_then(Value::as_bool).unwrap_or(false),
|
||||
"requestPending": value.get("request_pending").and_then(Value::as_bool).unwrap_or(false),
|
||||
"pendingEnqueues": value.get("pending_enqueues").and_then(Value::as_u64).unwrap_or(0),
|
||||
"pendingRequests": value.get("pending_requests").and_then(Value::as_bool).unwrap_or(false),
|
||||
"docs": value.get("docs").and_then(Value::as_u64).unwrap_or(0),
|
||||
"batches": value.get("batchs").and_then(Value::as_u64).unwrap_or(0),
|
||||
"currentBatch": value.get("cur_batch").and_then(Value::as_u64).unwrap_or(0),
|
||||
"jobName": value.get("job_name").and_then(Value::as_str).unwrap_or_default(),
|
||||
"jobStart": value.get("job_start").cloned().unwrap_or(Value::Null),
|
||||
"latestMessage": latest_message,
|
||||
"historyMessages": history_messages,
|
||||
"cancellationRequested": value.get("cancellation_requested").and_then(Value::as_bool).unwrap_or(false),
|
||||
"cancellationReason": value.get("cancellation_reason").cloned().unwrap_or(Value::Null),
|
||||
"progress": progress.map(|progress| json!({
|
||||
@@ -1464,6 +1536,25 @@ fn lightrag_pipeline_status_summary(value: &Value) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn lightrag_pipeline_history_messages(value: &Value) -> Vec<String> {
|
||||
value
|
||||
.get("history_messages")
|
||||
.and_then(Value::as_array)
|
||||
.map(|messages| {
|
||||
messages
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.rev()
|
||||
.take(80)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn lightrag_rerank_status_summary(value: &Value) -> Value {
|
||||
let configuration = value.get("configuration").unwrap_or(&Value::Null);
|
||||
let queue = value.get("rerank_queue_status").unwrap_or(&Value::Null);
|
||||
@@ -1510,13 +1601,15 @@ fn parse_lightrag_chunk_progress(message: &str) -> Option<LightRagChunkProgress>
|
||||
let current = current.trim().parse::<u64>().ok()?;
|
||||
let (total, _) = rest.split_once(' ')?;
|
||||
let total = total.trim().parse::<u64>().ok()?;
|
||||
let doc_token = message
|
||||
.split_whitespace()
|
||||
.find(|token| token.starts_with("doc-") && token.contains("-chunk-"))?;
|
||||
let doc_token = message.split_whitespace().find(|token| {
|
||||
token.starts_with("doc-") && (token.contains("-chunk-") || token.contains("-mm-"))
|
||||
})?;
|
||||
let doc_id = doc_token
|
||||
.rsplit_once("-chunk-")
|
||||
.or_else(|| doc_token.rsplit_once("-mm-"))
|
||||
.map(|(doc_id, _)| doc_id)
|
||||
.unwrap_or(doc_token)
|
||||
.trim_end_matches(|ch: char| !ch.is_ascii_alphanumeric())
|
||||
.to_string();
|
||||
if total == 0 || current == 0 || doc_id.is_empty() {
|
||||
return None;
|
||||
@@ -3630,6 +3723,11 @@ mod tests {
|
||||
let status = json!({
|
||||
"busy": true,
|
||||
"scanning": true,
|
||||
"destructive_busy": false,
|
||||
"pending_enqueues": 1,
|
||||
"docs": 1,
|
||||
"batchs": 2,
|
||||
"cur_batch": 1,
|
||||
"job_name": "book.docx",
|
||||
"latest_message": "Chunk 212 of 333 extracted 4 Ent + 0 Rel doc-ff0b60997a285a85e5704a114d7b3ffa-chunk-212"
|
||||
});
|
||||
@@ -3638,6 +3736,10 @@ mod tests {
|
||||
|
||||
assert_eq!(summary["busy"], true);
|
||||
assert_eq!(summary["scanning"], true);
|
||||
assert_eq!(summary["pendingEnqueues"], 1);
|
||||
assert_eq!(summary["docs"], 1);
|
||||
assert_eq!(summary["batches"], 2);
|
||||
assert_eq!(summary["currentBatch"], 1);
|
||||
assert_eq!(summary["progress"]["current"], 212);
|
||||
assert_eq!(summary["progress"]["total"], 333);
|
||||
assert_eq!(
|
||||
@@ -3646,6 +3748,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lightrag_pipeline_summary_extracts_chunk_progress_from_history() {
|
||||
let status = json!({
|
||||
"busy": true,
|
||||
"scanning": false,
|
||||
"job_name": "book.pdf",
|
||||
"latest_message": "Merging stage 1/1: book.pdf",
|
||||
"history_messages": [
|
||||
"Analyzing multimodal: doc-e51400269a2c41a2fcb22f1071193fad",
|
||||
"Chunk 5 of 21 extracted 2 Ent + 1 Rel doc-e51400269a2c41a2fcb22f1071193fad-mm-drawing-001",
|
||||
"Merging stage 1/1: book.pdf"
|
||||
]
|
||||
});
|
||||
|
||||
let summary = lightrag_pipeline_status_summary(&status);
|
||||
|
||||
assert_eq!(summary["progress"]["current"], 5);
|
||||
assert_eq!(summary["progress"]["total"], 21);
|
||||
assert_eq!(
|
||||
summary["progress"]["docId"],
|
||||
"doc-e51400269a2c41a2fcb22f1071193fad"
|
||||
);
|
||||
assert_eq!(summary["historyMessages"].as_array().unwrap().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_registry_json_defaults_indexed_roots() {
|
||||
let registry: KnowledgeRagSourceRegistry = serde_json::from_value(json!({
|
||||
|
||||
@@ -129,7 +129,7 @@ async fn resolve_mindmap_workspace_id(
|
||||
}
|
||||
|
||||
fn execution_artifacts_json(
|
||||
execution: &crate::transport::convex::RetiredCloudCommandExecution,
|
||||
execution: &crate::transport::legacy_cloud_guard::RetiredCloudCommandExecution,
|
||||
) -> Value {
|
||||
execution
|
||||
.artifacts
|
||||
|
||||
@@ -84,6 +84,10 @@ pub fn build_router(state: AppState) -> Router {
|
||||
.route("/api/evidence/read", post(evidence::read))
|
||||
.route("/api/evidence/open", post(evidence::open))
|
||||
.route("/api/knowledge-rag/status", get(knowledge_rag::status))
|
||||
.route(
|
||||
"/api/knowledge-rag/pipeline-events",
|
||||
get(knowledge_rag::pipeline_events),
|
||||
)
|
||||
.route("/api/knowledge-rag/ingest", post(knowledge_rag::ingest))
|
||||
.route("/api/knowledge-rag/search", post(knowledge_rag::search))
|
||||
.route("/api/knowledge-rag/query", post(knowledge_rag::query_rag))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::app::AppConfig;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::transport::convex::execute_retired_query_plan;
|
||||
use crate::transport::legacy_cloud_guard::execute_retired_query_plan;
|
||||
use bridge_runtime::{
|
||||
build_query_request, execute_runtime_input, execute_runtime_query, BridgeContext,
|
||||
RuntimeActorWire, RuntimeBridgeContextWire, RuntimeExecutionPlan, RuntimeInput,
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::routes::command_support::{
|
||||
use crate::routes::local_folder_source::{
|
||||
ensure_local_workspace_access, execute_local_tree_command,
|
||||
};
|
||||
use crate::transport::convex::{
|
||||
use crate::transport::legacy_cloud_guard::{
|
||||
execute_retired_mutation_by_name, execute_retired_query_by_name,
|
||||
persist_runtime_command_artifacts,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::routes::query_support::{
|
||||
fetch_documents_meta_via_legacy_cloud, resolve_effective_workspace_id,
|
||||
};
|
||||
use crate::routes::snapshot_support::{load_projection_snapshot, ProjectionSnapshotSpec};
|
||||
use crate::transport::convex::execute_retired_mutation_by_name;
|
||||
use crate::transport::legacy_cloud_guard::execute_retired_mutation_by_name;
|
||||
use crate::tree_shell::filetree_renderer::{
|
||||
render_initial_filetree_html, FileTreeInitialRenderInput, FileTreeRenderRow,
|
||||
};
|
||||
|
||||
@@ -79,8 +79,21 @@ pub async fn document_page_shell(
|
||||
)
|
||||
));
|
||||
}
|
||||
let primary_source_kind = normalize_source_kind(query.source_kind.as_deref());
|
||||
let mut primary_source_kind = normalize_source_kind(query.source_kind.as_deref());
|
||||
let primary_root_uri = normalize_optional_query_value(query.root_uri.as_deref());
|
||||
if primary_source_kind.is_none()
|
||||
&& document_id.trim().starts_with("local-md:")
|
||||
&& primary_root_uri.is_some()
|
||||
{
|
||||
primary_source_kind = Some("local_folder");
|
||||
}
|
||||
if primary_source_kind.is_none() && document_id.trim().starts_with("local-md:") {
|
||||
return redirect_response(&format!(
|
||||
"/?routeGuard={}&missingPage={}",
|
||||
query_escape("local_folder_source_required"),
|
||||
query_escape(&document_id),
|
||||
));
|
||||
}
|
||||
let aggregate = match build_page_aggregate_snapshot(
|
||||
&state,
|
||||
&context,
|
||||
@@ -2249,12 +2262,25 @@ pub async fn pdf_preview_page(Query(query): Query<PdfPreviewQuery>) -> Response
|
||||
if (viewer) viewer.append(canvas);
|
||||
if (evidencePage === pageNumber) {{
|
||||
canvas.setAttribute('data-mnote-evidence-page', 'true');
|
||||
let evidenceTargetY = null;
|
||||
if (evidenceBBox) {{
|
||||
const rect = viewport.convertToViewportRectangle([evidenceBBox.x0, evidenceBBox.y0, evidenceBBox.x1, evidenceBBox.y1]);
|
||||
const x = Math.min(rect[0], rect[2]);
|
||||
const y = Math.min(rect[1], rect[3]);
|
||||
const width = Math.max(1, Math.abs(rect[2] - rect[0]));
|
||||
const height = Math.max(1, Math.abs(rect[3] - rect[1]));
|
||||
const normalizedMineruBox = evidenceBBox.x0 >= 0 && evidenceBBox.y0 >= 0 && evidenceBBox.x1 <= 1000 && evidenceBBox.y1 <= 1000;
|
||||
let x;
|
||||
let y;
|
||||
let width;
|
||||
let height;
|
||||
if (normalizedMineruBox) {{
|
||||
x = Math.min(evidenceBBox.x0, evidenceBBox.x1) / 1000 * viewport.width;
|
||||
y = Math.min(evidenceBBox.y0, evidenceBBox.y1) / 1000 * viewport.height;
|
||||
width = Math.max(1, Math.abs(evidenceBBox.x1 - evidenceBBox.x0) / 1000 * viewport.width);
|
||||
height = Math.max(1, Math.abs(evidenceBBox.y1 - evidenceBBox.y0) / 1000 * viewport.height);
|
||||
}} else {{
|
||||
const rect = viewport.convertToViewportRectangle([evidenceBBox.x0, evidenceBBox.y0, evidenceBBox.x1, evidenceBBox.y1]);
|
||||
x = Math.min(rect[0], rect[2]);
|
||||
y = Math.min(rect[1], rect[3]);
|
||||
width = Math.max(1, Math.abs(rect[2] - rect[0]));
|
||||
height = Math.max(1, Math.abs(rect[3] - rect[1]));
|
||||
}}
|
||||
context.save();
|
||||
context.scale(outputScale, outputScale);
|
||||
context.fillStyle = 'rgba(37, 99, 235, 0.18)';
|
||||
@@ -2263,8 +2289,17 @@ pub async fn pdf_preview_page(Query(query): Query<PdfPreviewQuery>) -> Response
|
||||
context.fillRect(x, y, width, height);
|
||||
context.strokeRect(x, y, width, height);
|
||||
context.restore();
|
||||
evidenceTargetY = y + height / 2;
|
||||
}}
|
||||
window.setTimeout(() => canvas.scrollIntoView({{ block: 'center', inline: 'nearest' }}), 0);
|
||||
window.setTimeout(() => {{
|
||||
if (Number.isFinite(evidenceTargetY)) {{
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const absoluteTargetTop = rect.top + window.scrollY + evidenceTargetY;
|
||||
window.scrollTo({{ top: Math.max(0, absoluteTargetTop - window.innerHeight / 2), behavior: 'auto' }});
|
||||
return;
|
||||
}}
|
||||
canvas.scrollIntoView({{ block: 'center', inline: 'nearest' }});
|
||||
}}, 0);
|
||||
}}
|
||||
}}
|
||||
|
||||
@@ -3869,6 +3904,38 @@ mod tests {
|
||||
assert!(location.contains("missingPage=local-md%3Adocs%7E2FPlan.md"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn document_shell_local_markdown_without_source_kind_does_not_fall_back_to_convex() {
|
||||
let response = app_with_unreachable_convex_without_fixture()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/documents/local-md:docs~2FPlan.md?resourceTab=primary%3A%3Aresource%3Afile%3Afile%3A%2F%2F%2Ftmp%3Adocs%2FPlan.pdf")
|
||||
.header("x-mnote-actor-id", "user_test")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::SEE_OTHER);
|
||||
assert_ne!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-error-code")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("convex_retired")
|
||||
);
|
||||
let location = response
|
||||
.headers()
|
||||
.get(header::LOCATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default();
|
||||
assert!(location.starts_with("/?"));
|
||||
assert!(location.contains("routeGuard=local_folder_source_required"));
|
||||
assert!(location.contains("missingPage=local-md%3Adocs%7E2FPlan.md"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn document_shell_records_local_markdown_page_recent() {
|
||||
let root = temp_root("mnote-document-shell-record-page-recent");
|
||||
@@ -4490,7 +4557,7 @@ mod tests {
|
||||
.headers()
|
||||
.get("x-upstream-service")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("convex-retired")
|
||||
Some("legacy-cloud-retired")
|
||||
);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
|
||||
@@ -543,6 +543,7 @@ mod tests {
|
||||
fn sidebar_settings_runtime_routes_index_and_ocr_to_lightrag_settings() {
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("mnote-knowledge-rag-settings-popover"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("/api/knowledge-rag/status?"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("/api/knowledge-rag/pipeline-events"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("/api/knowledge-rag/ingest"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("/api/knowledge-rag/prune-registry"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("use-filetree-selection"));
|
||||
@@ -553,6 +554,22 @@ mod tests {
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("Rerank"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("scheduleKnowledgeRagStatusBridge"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("knowledgeRagStatusHasInFlight"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("new EventSource(url.toString())"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("pipeline_status"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("pipeline.pendingEnqueues"));
|
||||
assert!(
|
||||
SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("data-mnote-knowledge-rag-watcher-required")
|
||||
);
|
||||
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("runKnowledgeRagStatusBridge"));
|
||||
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("knowledgeRagBridgeDelays"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("knowledgeRagRegistryStatusDigest"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("registry-status-changed"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("knowledgeRagIndeterminateProgress"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("data-progress-mode"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("indeterminate"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("analyzing"));
|
||||
assert!(crate::ssr::styles::MNOTE_CSS
|
||||
.contains("mnote-knowledge-rag-source-progress[data-progress-mode=\"indeterminate\"]"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("delete_submitted"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("delete_completed"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("publicKnowledgeRagDashboardUrl"));
|
||||
@@ -695,11 +712,15 @@ mod tests {
|
||||
SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("function pageAiLoadBackendSessionDetail")
|
||||
);
|
||||
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("function pageAiSearchBackendSessions"));
|
||||
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS
|
||||
.contains("function pageAiDeleteSelectedBackendSessions"));
|
||||
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("function pageAiCheckActiveRun"));
|
||||
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("/api/page-ai/sessions/"));
|
||||
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("/active-run?"));
|
||||
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-rename"));
|
||||
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-delete"));
|
||||
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-select"));
|
||||
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-delete-selected"));
|
||||
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-resume"));
|
||||
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-search"));
|
||||
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("function pageAiRenderThoughtGroup"));
|
||||
|
||||
@@ -4038,9 +4038,17 @@ body {
|
||||
background: #2F7D4A;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-progress[data-progress-mode="indeterminate"] i {
|
||||
width: 38%;
|
||||
animation: mnote-task-progress-slide 1.15s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-progress span {
|
||||
max-width: 72px;
|
||||
overflow: hidden;
|
||||
color: #5F5A54;
|
||||
font: 10px/14px "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -4537,6 +4545,10 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wolai-page-ai-ghost--danger {
|
||||
color: #B33A3A;
|
||||
}
|
||||
|
||||
.wolai-page-share-dialog {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -5234,8 +5246,16 @@ html[data-mnote-page-ai-resizing="true"] {
|
||||
}
|
||||
|
||||
.wolai-page-ai-message--history {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(0, 1fr);
|
||||
width: 100%;
|
||||
min-height: 58px;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.08);
|
||||
border-radius: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -5244,6 +5264,59 @@ html[data-mnote-page-ai-resizing="true"] {
|
||||
background: #F4F8FF;
|
||||
}
|
||||
|
||||
.wolai-page-ai-message--history.is-selected {
|
||||
border-color: rgba(37, 99, 235, 0.36);
|
||||
background: #F5F8FF;
|
||||
}
|
||||
|
||||
.wolai-page-ai-history-selection {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
color: #5A5A5A;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-history-selection[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.wolai-page-ai-session-check {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 1px;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(27, 28, 28, 0.18);
|
||||
border-radius: 6px;
|
||||
background: #FFF;
|
||||
color: #2563EB;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.wolai-page-ai-session-check span {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
place-items: center;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wolai-page-ai-message--history:hover .wolai-page-ai-session-check,
|
||||
.wolai-page-ai-message--history:focus-within .wolai-page-ai-session-check,
|
||||
.wolai-page-ai-message--history.is-selected .wolai-page-ai-session-check {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.wolai-page-ai-session-check[aria-checked="true"] {
|
||||
border-color: rgba(37, 99, 235, 0.5);
|
||||
background: #EAF1FF;
|
||||
}
|
||||
|
||||
.wolai-page-ai-message--user {
|
||||
background: #F4F8FF;
|
||||
}
|
||||
@@ -5358,12 +5431,43 @@ button.wolai-page-ai-message-text {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.wolai-page-ai-history-main {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
button.wolai-page-ai-history-main strong,
|
||||
button.wolai-page-ai-history-main span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wolai-page-ai-message-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-history-actions {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-end;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.wolai-page-ai-message--history:hover .wolai-page-ai-history-actions,
|
||||
.wolai-page-ai-message--history:focus-within .wolai-page-ai-history-actions {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.wolai-page-ai-permission-dialog {
|
||||
position: fixed;
|
||||
right: 28px;
|
||||
|
||||
@@ -1 +1 @@
|
||||
pub mod convex;
|
||||
pub mod legacy_cloud_guard;
|
||||
|
||||
+2
-2
@@ -19,11 +19,11 @@ pub struct RetiredCloudCommandExecution {
|
||||
fn retired_error(context: &RequestContext, phase: &'static str) -> WebError {
|
||||
WebError::service_unavailable_code(
|
||||
"convex_retired",
|
||||
"Convex 运行时已退役;请使用 local-first Rust/SQLite control-plane 路径",
|
||||
"旧 cloud/Convex 兼容运行时已退役;请使用 local-first Rust/SQLite control-plane 路径",
|
||||
)
|
||||
.with_context(context)
|
||||
.with_header("x-error-phase", phase)
|
||||
.with_header("x-upstream-service", "convex-retired")
|
||||
.with_header("x-upstream-service", "legacy-cloud-retired")
|
||||
}
|
||||
|
||||
fn load_query_fixture(
|
||||
Reference in New Issue
Block a user