集成 OpenHub 与 WeKnora Page AI
This commit is contained in:
Generated
+17
@@ -1759,6 +1759,16 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -2367,6 +2377,7 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
@@ -3255,6 +3266,12 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
|
||||
@@ -16,7 +16,7 @@ hyper = "1"
|
||||
hyper-util = { version = "0.1", features = ["tokio"] }
|
||||
leptos = { version = "0.8.14", default-features = false, features = ["ssr"] }
|
||||
mnote-editor-core = { path = "../mnote-editor-core" }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls", "stream"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls", "stream"] }
|
||||
rusqlite = { version = "0.34", features = ["bundled"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -199,6 +199,12 @@ export function createSidebarPageAiRuntime(context) {
|
||||
pageAiOpencodeEventSource: null,
|
||||
pageAiOpencodeEventSessionId: '',
|
||||
pageAiOpencodeEventTimer: 0,
|
||||
pageAiOpenHubStatus: null,
|
||||
pageAiOpenHubStatusError: '',
|
||||
pageAiOpenHubBootstrap: null,
|
||||
pageAiOpenHubIframeUrl: '',
|
||||
pageAiOpenHubScopeSummary: '',
|
||||
pageAiOpenHubFallbackReason: '',
|
||||
pageAiSessionSearchQuery: '',
|
||||
pageAiSessionSearchResults: [],
|
||||
pageAiSessionSearchTimer: 0,
|
||||
@@ -886,6 +892,10 @@ export function createSidebarPageAiRuntime(context) {
|
||||
}
|
||||
}
|
||||
|
||||
function pageAiOpenHubHostEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function pageAiOpencodeStatusLabel(status) {
|
||||
var node = status && typeof status === 'object' ? status : {};
|
||||
var state = String(node.status || node.state || node.runtimeStatus || '').trim();
|
||||
@@ -949,6 +959,44 @@ export function createSidebarPageAiRuntime(context) {
|
||||
});
|
||||
}
|
||||
|
||||
function pageAiNormalizeRootRelativePath(path) {
|
||||
return String(path || '').trim().replace(/\\/g, '/').replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
function pageAiRootPathFromFileUri(rootUri) {
|
||||
var root = String(rootUri || '').trim();
|
||||
if (!root.startsWith('file://')) return '';
|
||||
try {
|
||||
return decodeURIComponent(new URL(root).pathname || '').replace(/\/+$/, '');
|
||||
} catch (_error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function pageAiRootRelativePathFromChangedPath(path, rootUri) {
|
||||
var value = String(path || '').trim();
|
||||
if (!value) return '';
|
||||
try {
|
||||
var parsed = new URL(value, window.location.origin);
|
||||
if (parsed.pathname === '/api/local-folder/files/open') {
|
||||
return pageAiNormalizeRootRelativePath(parsed.searchParams.get('path') || '');
|
||||
}
|
||||
if (parsed.protocol === 'file:') value = decodeURIComponent(parsed.pathname || '');
|
||||
} catch (_error) {}
|
||||
value = value.replace(/^file:\/\//, '');
|
||||
var rootPath = pageAiRootPathFromFileUri(rootUri || currentRootUri());
|
||||
if (rootPath && value.startsWith(rootPath + '/')) {
|
||||
value = value.slice(rootPath.length + 1);
|
||||
}
|
||||
return pageAiNormalizeRootRelativePath(value);
|
||||
}
|
||||
|
||||
function pageAiDocumentIdFromRootRelativePath(relativePath) {
|
||||
var normalized = pageAiNormalizeRootRelativePath(relativePath);
|
||||
if (!normalized || !/\.(md|markdown)$/i.test(normalized)) return '';
|
||||
return localMarkdownDocumentIdFromPageAiRelativePath(normalized);
|
||||
}
|
||||
|
||||
function pageAiOpencodeCurrentPageLabel() {
|
||||
var aggregate = currentPageAggregate();
|
||||
var meta = aggregate && aggregate.meta && typeof aggregate.meta === 'object' ? aggregate.meta : {};
|
||||
@@ -983,6 +1031,95 @@ export function createSidebarPageAiRuntime(context) {
|
||||
}
|
||||
}
|
||||
|
||||
function pageAiMNoteDocumentPathSegment(documentId) {
|
||||
return encodeURIComponent(String(documentId || '').trim()).replace(/%3A/g, ':').replace(/%7E/g, '~');
|
||||
}
|
||||
|
||||
function pageAiParentRelativePath(relativePath) {
|
||||
var normalized = pageAiNormalizeRootRelativePath(relativePath);
|
||||
if (!normalized || normalized.indexOf('/') === -1) return '';
|
||||
return normalized.split('/').slice(0, -1).join('/');
|
||||
}
|
||||
|
||||
function pageAiFileUriForRootRelativePath(rootUri, relativePath) {
|
||||
var root = String(rootUri || '').trim().replace(/\/+$/, '');
|
||||
var relative = pageAiNormalizeRootRelativePath(relativePath);
|
||||
if (!root) return '';
|
||||
if (!relative) return root;
|
||||
return root + '/' + relative.split('/').map(function(segment) {
|
||||
return encodeURIComponent(segment);
|
||||
}).join('/');
|
||||
}
|
||||
|
||||
function pageAiCurrentActiveTabEditorTarget() {
|
||||
var snapshot = currentPageAiOpenEditorsSnapshot();
|
||||
if (snapshot && snapshot.activeEditor) {
|
||||
var activeTarget = pageAiTargetFromOpenEditor(snapshot.activeEditor, 'open_editors_active_tab');
|
||||
if (activeTarget) return activeTarget;
|
||||
}
|
||||
if (snapshot) {
|
||||
var entries = pageAiNormalizeArray(snapshot.editors).concat(pageAiNormalizeArray(snapshot.resourceEditors));
|
||||
var activeEntry = entries.find(function(entry) { return entry && entry.active === true; });
|
||||
if (activeEntry) {
|
||||
var target = pageAiTargetFromOpenEditor(activeEntry, 'open_editors_active_tab');
|
||||
if (target) return target;
|
||||
}
|
||||
}
|
||||
return pageAiFallbackEditorTarget();
|
||||
}
|
||||
|
||||
function pageAiCurrentActiveTabAddressPayload() {
|
||||
var target = pageAiCurrentActiveTabEditorTarget() || {};
|
||||
var workspacePath = target && target.workspacePath && typeof target.workspacePath === 'object' ? target.workspacePath : {};
|
||||
var rootUri = String(workspacePath.rootUri || target.rootUri || currentRootUri() || '').trim();
|
||||
var sourceKind = String(workspacePath.sourceKind || target.sourceKind || currentSourceKind() || '').trim();
|
||||
var workspaceId = String(workspacePath.workspaceId || target.workspaceId || resolveWorkspaceId(document.body) || '').trim();
|
||||
var documentId = String(workspacePath.documentId || target.documentId || currentDocumentId() || '').trim();
|
||||
var relativePath = pageAiNormalizeRootRelativePath(workspacePath.relativePath || workspacePath.path || target.relativePath || target.path || '');
|
||||
if (!relativePath) relativePath = pageAiNormalizeRootRelativePath(localMarkdownRelativePathFromPageAiDocumentId(documentId));
|
||||
if (!documentId && relativePath && /\.(md|markdown)$/i.test(relativePath)) {
|
||||
documentId = localMarkdownDocumentIdFromPageAiRelativePath(relativePath);
|
||||
}
|
||||
if (!documentId) documentId = String(currentDocumentId() || '').trim();
|
||||
var resourceKind = String(target.resourceKind || workspacePath.resourceKind || target.editorKind || '').trim().toLowerCase();
|
||||
var editorKind = String(target.editorKind || '').trim().toLowerCase();
|
||||
var objectIdentity = String(target.objectIdentity || target.targetId || workspacePath.objectIdentity || '').trim();
|
||||
var tabUrl = new URL('/documents/' + pageAiMNoteDocumentPathSegment(documentId), window.location.origin);
|
||||
if (sourceKind) tabUrl.searchParams.set('sourceKind', sourceKind);
|
||||
if (rootUri) tabUrl.searchParams.set('rootUri', rootUri);
|
||||
if (workspaceId) tabUrl.searchParams.set('workspaceId', workspaceId);
|
||||
var isResourceTab = Boolean(relativePath)
|
||||
&& resourceKind !== 'markdown_page'
|
||||
&& resourceKind !== 'page'
|
||||
&& editorKind !== 'page';
|
||||
if (isResourceTab) {
|
||||
tabUrl.searchParams.set('resourcePath', relativePath);
|
||||
if (objectIdentity) tabUrl.searchParams.set('resourceTab', objectIdentity);
|
||||
}
|
||||
var folderRelativePath = pageAiParentRelativePath(relativePath);
|
||||
var folderUrl = pageAiFileUriForRootRelativePath(rootUri, folderRelativePath);
|
||||
if (!folderUrl) {
|
||||
var folderMNoteUrl = new URL('/documents/' + pageAiMNoteDocumentPathSegment(documentId), window.location.origin);
|
||||
if (sourceKind) folderMNoteUrl.searchParams.set('sourceKind', sourceKind);
|
||||
if (rootUri) folderMNoteUrl.searchParams.set('rootUri', rootUri);
|
||||
if (workspaceId) folderMNoteUrl.searchParams.set('workspaceId', workspaceId);
|
||||
if (folderRelativePath) folderMNoteUrl.searchParams.set('fileTreeScope', folderRelativePath);
|
||||
folderUrl = folderMNoteUrl.toString();
|
||||
}
|
||||
return {
|
||||
schema: 'mnote.page_ai.active_tab_address.v1',
|
||||
tabUrl: tabUrl.toString(),
|
||||
folderUrl: folderUrl,
|
||||
documentId: documentId,
|
||||
rootUri: rootUri,
|
||||
workspaceId: workspaceId,
|
||||
relativePath: relativePath,
|
||||
folderRelativePath: folderRelativePath,
|
||||
targetId: String(target.targetId || objectIdentity || '').trim(),
|
||||
resourceKind: resourceKind || 'markdown_page'
|
||||
};
|
||||
}
|
||||
|
||||
function pageAiBuildOpencodeContextPayload() {
|
||||
var selectedText = currentPageAiSelectedText();
|
||||
var target = currentPageAiEditorTarget() || {};
|
||||
@@ -1014,6 +1151,116 @@ export function createSidebarPageAiRuntime(context) {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function pageAiOpenHubScopeSummary(scope) {
|
||||
var node = scope && typeof scope === 'object' ? scope : {};
|
||||
var workspace = node.workspaceScope && typeof node.workspaceScope === 'object' ? node.workspaceScope : {};
|
||||
var items = [];
|
||||
if (node.openhubUserKey || node.openhub_user_key) items.push('openhub_user_key=' + String(node.openhubUserKey || node.openhub_user_key));
|
||||
if (node.openhubWorkspaceKey || node.workspace_key) items.push('workspace_key=' + String(node.openhubWorkspaceKey || node.workspace_key));
|
||||
if (workspace.workspaceId) items.push('workspace=' + String(workspace.workspaceId));
|
||||
if (workspace.rootUri) items.push('rootUri=' + String(workspace.rootUri));
|
||||
if (node.openhubSessionScope || node.session_scope) items.push('session_scope=' + String(node.openhubSessionScope || node.session_scope));
|
||||
if (node.skillScope || node.skill_scope) items.push('skill_scope=' + String(node.skillScope || node.skill_scope));
|
||||
if (node.mcpScope) items.push('mcp_scope=' + String(node.mcpScope));
|
||||
if (node.toolPermissionScope || node.tool_permission_scope) items.push('tool_permission_scope=' + String(node.toolPermissionScope || node.tool_permission_scope));
|
||||
if (node.weknoraToolScope || node.weknora_tool_scope) items.push('weknora_tool_scope=' + String(node.weknoraToolScope || node.weknora_tool_scope));
|
||||
return items.join('\n');
|
||||
}
|
||||
|
||||
function pageAiOpenHubStatusText(status) {
|
||||
var node = status && typeof status === 'object' ? status : {};
|
||||
if (node.ok === true || String(node.status || '').trim() === 'ready') return '已连接 OpenHub';
|
||||
if (String(node.status || '').trim() === 'degraded') return 'OpenHub 连接受限';
|
||||
return '正在连接 OpenHub';
|
||||
}
|
||||
|
||||
function pageAiRenderOpenHubHostChrome() {
|
||||
var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
|
||||
if (!(drawer instanceof HTMLElement) || drawer.getAttribute('data-page-ai-openhub-host') !== 'true') return;
|
||||
var bootstrap = pageUiState.pageAiOpenHubBootstrap && typeof pageUiState.pageAiOpenHubBootstrap === 'object' ? pageUiState.pageAiOpenHubBootstrap : {};
|
||||
var scope = bootstrap.scope && typeof bootstrap.scope === 'object' ? bootstrap.scope : {};
|
||||
var status = pageUiState.pageAiOpenHubStatus && typeof pageUiState.pageAiOpenHubStatus === 'object' ? pageUiState.pageAiOpenHubStatus : {};
|
||||
var statusNode = drawer.querySelector('[data-page-ai-openhub-runtime-status]');
|
||||
if (statusNode instanceof HTMLElement) {
|
||||
statusNode.textContent = pageUiState.pageAiOpenHubStatusError || pageAiOpenHubStatusText(status);
|
||||
statusNode.setAttribute('data-state', pageUiState.pageAiOpenHubStatusError ? 'error' : (status.ok ? 'ok' : 'degraded'));
|
||||
}
|
||||
var userNode = drawer.querySelector('[data-page-ai-openhub-user-key]');
|
||||
if (userNode instanceof HTMLElement) userNode.textContent = String(scope.openhubUserKey || scope.openhub_user_key || '等待 bootstrap');
|
||||
var authNode = drawer.querySelector('[data-page-ai-openhub-auth-truth]');
|
||||
if (authNode instanceof HTMLElement) authNode.textContent = '登录真相:MNote mnote_session;拒绝 OpenHub JWT/localStorage';
|
||||
var workspaceNode = drawer.querySelector('[data-page-ai-openhub-workspace-scope]');
|
||||
if (workspaceNode instanceof HTMLElement) workspaceNode.textContent = pageAiOpenHubScopeSummary(scope) || '等待 workspace/rootUri/session/tool scope';
|
||||
var guardNode = drawer.querySelector('[data-page-ai-openhub-route-guard]');
|
||||
if (guardNode instanceof HTMLElement) guardNode.textContent = '非 AI 路由 guard:login/admin/file/knowledge 由 MNote 接管';
|
||||
var fallbackNode = drawer.querySelector('[data-page-ai-openhub-fallback]');
|
||||
if (fallbackNode instanceof HTMLElement) {
|
||||
fallbackNode.textContent = pageUiState.pageAiOpenHubFallbackReason || 'OpenHub 不可用时提示用户刷新或检查服务,不再提供 opencode 页面回退';
|
||||
}
|
||||
var iframe = drawer.querySelector('[data-page-ai-openhub-iframe]');
|
||||
if (iframe instanceof HTMLIFrameElement) {
|
||||
var nextUrl = String(pageUiState.pageAiOpenHubIframeUrl || bootstrap.openhubIframeUrl || '/page-ai/openhub/ai').trim();
|
||||
if (nextUrl && iframe.getAttribute('src') !== nextUrl) iframe.setAttribute('src', nextUrl);
|
||||
iframe.hidden = !nextUrl;
|
||||
}
|
||||
}
|
||||
|
||||
function pageAiEnsureOpenHubHostDrawer() {
|
||||
var drawer = ensurePageAiDrawer();
|
||||
pageAiInstallMNoteOpenFileBridge();
|
||||
if (drawer.getAttribute('data-page-ai-openhub-host') !== 'true') {
|
||||
drawer.setAttribute('data-page-ai-openhub-host', 'true');
|
||||
drawer.removeAttribute('data-page-ai-opencode-host');
|
||||
drawer.setAttribute('data-mnote-acp-runtime', 'openhub-opencode');
|
||||
drawer.innerHTML = '' +
|
||||
'<div class="wolai-page-ai-resize-handle" data-page-ai-resize-handle title="拖动调整页面 AI 宽度" aria-hidden="true"></div>' +
|
||||
'<div class="wolai-page-ai-panel wolai-page-ai-opencode-panel" role="dialog" aria-modal="false" aria-label="OpenHub AI">' +
|
||||
'<details class="wolai-page-ai-openhub-diagnostics" data-page-ai-openhub-bootstrap-copy hidden aria-hidden="true">' +
|
||||
'<summary>诊断</summary>' +
|
||||
'<div class="wolai-page-ai-opencode-row"><span>用户隔离</span><code data-page-ai-openhub-user-key>等待 bootstrap</code></div>' +
|
||||
'<div class="wolai-page-ai-opencode-row"><span>认证边界</span><strong data-page-ai-openhub-auth-truth>登录真相:MNote mnote_session</strong></div>' +
|
||||
'<pre class="wolai-page-ai-opencode-empty" data-page-ai-openhub-workspace-scope>等待 workspace/rootUri/session/tool scope</pre>' +
|
||||
'<div class="wolai-page-ai-opencode-badges"><span data-page-ai-openhub-route-guard>非 AI 路由 guard:login/admin/file/knowledge</span><span data-page-ai-openhub-fallback>OpenHub 不可用时提示刷新或检查服务</span></div>' +
|
||||
'</details>' +
|
||||
'<div class="wolai-page-ai-opencode-frame-wrap" data-page-ai-openhub-frame-wrap>' +
|
||||
'<iframe class="wolai-page-ai-opencode-iframe" data-page-ai-openhub-iframe title="OpenHub AI 面板" allow="clipboard-read; clipboard-write" referrerpolicy="same-origin" src="about:blank"></iframe>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
pageAiRenderOpenHubHostChrome();
|
||||
void pageAiBootstrapOpenHubHost();
|
||||
return drawer;
|
||||
}
|
||||
|
||||
async function pageAiBootstrapOpenHubHost() {
|
||||
var context = pageAiBuildOpencodeContextPayload();
|
||||
try {
|
||||
var statusResponse = await fetch('/api/page-ai/openhub/status', { headers: { accept: 'application/json' }, cache: 'no-store' });
|
||||
pageUiState.pageAiOpenHubStatus = await statusResponse.json().catch(function() { return null; }) || {};
|
||||
var response = await fetch('/api/page-ai/openhub/bootstrap', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
||||
cache: 'no-store',
|
||||
body: JSON.stringify(context)
|
||||
});
|
||||
var payload = await response.json().catch(function() { return null; });
|
||||
if (!response.ok) throw new Error(payload && (payload.error || payload.message) ? String(payload.error || payload.message) : 'HTTP ' + response.status);
|
||||
pageUiState.pageAiOpenHubBootstrap = payload || {};
|
||||
pageUiState.pageAiOpenHubStatusError = '';
|
||||
pageUiState.pageAiOpenHubIframeUrl = String(payload && payload.openhubIframeUrl || '/page-ai/openhub/ai').trim();
|
||||
var statusFallback = pageUiState.pageAiOpenHubStatus && pageUiState.pageAiOpenHubStatus.fallback ? pageUiState.pageAiOpenHubStatus.fallback : null;
|
||||
pageUiState.pageAiOpenHubFallbackReason = payload && payload.fallback
|
||||
? String(payload.fallback.reason || '')
|
||||
: (statusFallback ? String(statusFallback.reason || '') : '');
|
||||
pageUiState.pageAiOpenHubScopeSummary = pageAiOpenHubScopeSummary(payload && payload.scope);
|
||||
pageAiRenderOpenHubHostChrome();
|
||||
} catch (error) {
|
||||
pageUiState.pageAiOpenHubStatusError = error instanceof Error ? error.message : String(error);
|
||||
pageUiState.pageAiOpenHubFallbackReason = 'OpenHub bootstrap 不可用,请刷新或检查 OpenHub 服务';
|
||||
pageAiRenderOpenHubHostChrome();
|
||||
}
|
||||
}
|
||||
|
||||
function pageAiPostOpencodeBridgeMessage(type, payload) {
|
||||
var iframe = document.querySelector('[data-page-ai-opencode-iframe]');
|
||||
if (!(iframe instanceof HTMLIFrameElement) || !iframe.contentWindow) return false;
|
||||
@@ -1021,6 +1268,41 @@ export function createSidebarPageAiRuntime(context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function pageAiInstallMNoteOpenFileBridge() {
|
||||
if (pageUiState.pageAiMNoteOpenFileBridgeInstalled) return;
|
||||
pageUiState.pageAiMNoteOpenFileBridgeInstalled = true;
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
var message = event.data && typeof event.data === 'object' ? event.data : null;
|
||||
if (message && message.source === 'openhub-ai' && message.type === 'mnote:get-active-tab-address') {
|
||||
var addressPayload = pageAiCurrentActiveTabAddressPayload();
|
||||
var requestPayload = message.payload && typeof message.payload === 'object' ? message.payload : {};
|
||||
var kind = String(requestPayload.kind || message.kind || 'tab').trim();
|
||||
var value = kind === 'folder' ? addressPayload.folderUrl : addressPayload.tabUrl;
|
||||
if (event.source && typeof event.source.postMessage === 'function') {
|
||||
event.source.postMessage({
|
||||
source: 'mnote-page-ai',
|
||||
type: 'mnote:active-tab-address',
|
||||
requestId: message.requestId || '',
|
||||
payload: Object.assign({}, addressPayload, {
|
||||
kind: kind === 'folder' ? 'folder' : 'tab',
|
||||
value: value
|
||||
})
|
||||
}, event.origin);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!message || (message.type !== 'mnote:open-file' && message.type !== 'mnote:open-reference')) return;
|
||||
if (message.source !== 'openhub-diff' && message.source !== 'openhub-changed-files' && message.source !== 'openhub-citation') return;
|
||||
var payload = message.payload && typeof message.payload === 'object' ? message.payload : {};
|
||||
pageAiOpenOpencodeChangedFile(message.path || payload.path || payload.href || '', {
|
||||
rootUri: String(message.rootUri || payload.rootUri || payload.root_uri || currentRootUri() || '').trim(),
|
||||
workspaceId: String(message.workspaceId || payload.workspaceId || payload.workspace_id || resolveWorkspaceId(document.body) || '').trim(),
|
||||
documentId: String(message.documentId || payload.documentId || payload.document_id || '').trim()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function pageAiInstallOpencodeBridge() {
|
||||
if (pageUiState.pageAiOpencodeBridgeInstalled) return;
|
||||
pageUiState.pageAiOpencodeBridgeInstalled = true;
|
||||
@@ -1254,8 +1536,10 @@ export function createSidebarPageAiRuntime(context) {
|
||||
|
||||
function pageAiEnsureOpencodeHostDrawer() {
|
||||
var drawer = ensurePageAiDrawer();
|
||||
pageAiInstallMNoteOpenFileBridge();
|
||||
if (drawer.getAttribute('data-page-ai-opencode-host') !== 'true') {
|
||||
drawer.setAttribute('data-page-ai-opencode-host', 'true');
|
||||
drawer.removeAttribute('data-page-ai-openhub-host');
|
||||
drawer.setAttribute('data-mnote-acp-runtime', 'opencode');
|
||||
drawer.innerHTML = '' +
|
||||
'<div class="wolai-page-ai-resize-handle" data-page-ai-resize-handle title="拖动调整页面 AI 宽度" aria-hidden="true"></div>' +
|
||||
@@ -1530,16 +1814,59 @@ export function createSidebarPageAiRuntime(context) {
|
||||
pageAiRenderOpencodeHostChrome();
|
||||
}
|
||||
|
||||
function pageAiOpenOpencodeChangedFile(path) {
|
||||
var targetPath = String(path || '').trim();
|
||||
if (!targetPath || !window.__mnoteDocumentPaneRuntime || typeof window.__mnoteDocumentPaneRuntime.openResourceInActiveTab !== 'function') return false;
|
||||
void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({ path: targetPath });
|
||||
function pageAiOpenOpencodeChangedFile(path, options) {
|
||||
var rootUri = String(options && options.rootUri || currentRootUri() || '').trim();
|
||||
var targetPath = pageAiRootRelativePathFromChangedPath(path, rootUri);
|
||||
if (!targetPath || !window.__mnoteDocumentPaneRuntime) return false;
|
||||
var documentId = String(options && options.documentId || pageAiDocumentIdFromRootRelativePath(targetPath) || '').trim();
|
||||
var workspaceId = String(options && options.workspaceId || resolveWorkspaceId(document.body) || '').trim();
|
||||
var isMarkdown = Boolean(documentId);
|
||||
if (isMarkdown) {
|
||||
if (window.__mnoteLocalFolderEventBus && typeof window.__mnoteLocalFolderEventBus.emitSyntheticWatchBatch === 'function') {
|
||||
window.__mnoteLocalFolderEventBus.emitSyntheticWatchBatch({
|
||||
source: 'openhub_changed_file_bridge',
|
||||
reason: 'openhub_changed_file',
|
||||
rootUri: rootUri,
|
||||
workspaceId: workspaceId,
|
||||
payload: {
|
||||
schema: 'mnote.local_folder.watch_batch.v1',
|
||||
source: 'openhub_changed_file_bridge',
|
||||
rootUri: rootUri,
|
||||
changedPaths: [{ relativePath: targetPath, documentId: documentId, changeType: 'modified' }],
|
||||
affectedParents: [{ relativePath: pageAiParentRelativePath(targetPath), reason: 'openhub_changed_file' }]
|
||||
}
|
||||
});
|
||||
}
|
||||
if (typeof window.__mnoteDocumentPaneRuntime.refreshPrimaryDocument === 'function') {
|
||||
void window.__mnoteDocumentPaneRuntime.refreshPrimaryDocument({
|
||||
documentId: documentId,
|
||||
workspaceId: workspaceId,
|
||||
source: 'openhub-changed-file-bridge'
|
||||
});
|
||||
}
|
||||
if (typeof window.__mnoteDocumentPaneRuntime.openPrimaryDocument === 'function') {
|
||||
void window.__mnoteDocumentPaneRuntime.openPrimaryDocument({
|
||||
documentId: documentId,
|
||||
workspaceId: workspaceId,
|
||||
sourceKind: currentSourceKind() || 'local_folder',
|
||||
rootUri: rootUri
|
||||
});
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-openhub-document-pane-refresh', targetPath);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (typeof window.__mnoteDocumentPaneRuntime.openResourceInActiveTab !== 'function') return false;
|
||||
void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({ path: targetPath, rootUri: rootUri });
|
||||
return true;
|
||||
}
|
||||
|
||||
function pageAiRefreshOpencodeCurrentPage(reason) {
|
||||
if (window.__mnoteDocumentPaneRuntime && typeof window.__mnoteDocumentPaneRuntime.refreshPrimaryDocument === 'function') {
|
||||
void window.__mnoteDocumentPaneRuntime.refreshPrimaryDocument({ reason: reason || 'page-ai-opencode-host' });
|
||||
if (reason) {
|
||||
void window.__mnoteDocumentPaneRuntime.refreshPrimaryDocument({ reason: reason });
|
||||
} else {
|
||||
void window.__mnoteDocumentPaneRuntime.refreshPrimaryDocument({ reason: 'page-ai-opencode-host' });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -1739,7 +2066,7 @@ export function createSidebarPageAiRuntime(context) {
|
||||
'mnote.selection.read',
|
||||
'mnote.open_resources.snapshot',
|
||||
'mnote.allowed_roots.describe',
|
||||
'mnote.lightrag.query',
|
||||
'mnote.weknora.search',
|
||||
'mnote.reference.open',
|
||||
'mnote.page_aggregate.snapshot',
|
||||
'mnote.local_file.receipt'
|
||||
@@ -3540,6 +3867,20 @@ export function createSidebarPageAiRuntime(context) {
|
||||
|
||||
|
||||
function openPageAiDrawer() {
|
||||
if (pageAiOpenHubHostEnabled()) {
|
||||
pageAiEnsureContextRefState();
|
||||
void pageAiLoadAllowedRoots().then(function() {
|
||||
pageAiRenderOpenHubHostChrome();
|
||||
}).catch(function() {
|
||||
pageAiRenderOpenHubHostChrome();
|
||||
});
|
||||
var openhubDrawer = pageAiEnsureOpenHubHostDrawer();
|
||||
pageAiApplyDrawerWidth(openhubDrawer);
|
||||
openhubDrawer.hidden = false;
|
||||
pageUiState.pageAiOpen = true;
|
||||
updatePageAiTriggerState();
|
||||
return;
|
||||
}
|
||||
if (pageAiOpencodeHostEnabled()) {
|
||||
pageAiEnsureContextRefState();
|
||||
void pageAiLoadAllowedRoots().then(function() {
|
||||
@@ -3748,7 +4089,7 @@ export function createSidebarPageAiRuntime(context) {
|
||||
return text.indexOf(word) >= 0;
|
||||
});
|
||||
if (!asksCitation) return false;
|
||||
return ['lightrag', '资料库', '知识库', 'rag', '文献', '论文', '保护基'].some(function(word) {
|
||||
return ['weknora', '资料库', '知识库', 'rag', '文献', '论文', '保护基'].some(function(word) {
|
||||
return text.indexOf(word) >= 0;
|
||||
});
|
||||
}
|
||||
@@ -3759,7 +4100,7 @@ export function createSidebarPageAiRuntime(context) {
|
||||
var afterColon = text.split(/[::]/).slice(1).join(':').trim();
|
||||
var candidate = afterColon || text;
|
||||
candidate = candidate
|
||||
.replace(/^(请|请用|帮我|帮忙|用)?(资料库|知识库|lightrag|rag)?(搜索|检索|查找|查询|回答|说明|解释|总结)?/i, '')
|
||||
.replace(/^(请|请用|帮我|帮忙|用)?(资料库|知识库|weknora|rag)?(搜索|检索|查找|查询|回答|说明|解释|总结)?/i, '')
|
||||
.replace(/(请)?(给出|给我|附上|提供)?(链接|引用|来源|出处|证据|定位).*$/i, '')
|
||||
.trim();
|
||||
var stopIndex = candidate.search(/(在|的|是|有哪些|有什么|如何|怎么|用于|用途|作用|资料|文献|论文)/);
|
||||
@@ -3885,7 +4226,7 @@ export function createSidebarPageAiRuntime(context) {
|
||||
pageAiPersistSessions();
|
||||
renderPageAiConversation();
|
||||
} catch (error) {
|
||||
console.warn('MNote Page AI 自动追加 LightRAG 引用失败', error);
|
||||
console.warn('MNote Page AI 自动追加知识库引用失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4749,6 +5090,9 @@ export function createSidebarPageAiRuntime(context) {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (action === 'openhub-refresh-bootstrap') {
|
||||
void pageAiBootstrapOpenHubHost();
|
||||
}
|
||||
if (action === 'cancel-queued-run') {
|
||||
void pageAiCancelQueuedRun(pageAiAction.getAttribute('data-page-ai-queue-id'));
|
||||
}
|
||||
@@ -5159,7 +5503,7 @@ export function createSidebarPageAiRuntime(context) {
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
var api = {
|
||||
ensurePageAiStateFacade: (...args) => ensurePageAiStateFacade(...args),
|
||||
installPageAiDelegates: (...args) => installPageAiDelegates(...args),
|
||||
handlePageAiClick: (...args) => handlePageAiClick(...args),
|
||||
@@ -5217,4 +5561,6 @@ export function createSidebarPageAiRuntime(context) {
|
||||
pageAiRefreshOpencodeCurrentPage: (...args) => pageAiRefreshOpencodeCurrentPage(...args),
|
||||
updatePageAiTriggerState: (...args) => updatePageAiTriggerState(...args)
|
||||
};
|
||||
window.__mnoteSidebarPageAiRuntime = api;
|
||||
return api;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -210,6 +210,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
const openPageHistoryDrawer = (...args) => sidebarPageSettings.openPageHistoryDrawer(...args);
|
||||
const openPageIndexSettingsPopover = (...args) => sidebarPageSettings.openPageIndexSettingsPopover(...args);
|
||||
const openKnowledgeRagSettingsPopover = (...args) => sidebarPageSettings.openKnowledgeRagSettingsPopover(...args);
|
||||
const openKnowledgeRagSourceReference = (...args) => sidebarPageSettings.openKnowledgeRagSourceReference(...args);
|
||||
const openPageShareDialog = (...args) => sidebarPageSettings.openPageShareDialog(...args);
|
||||
const pageOptionIsSupported = (...args) => sidebarPageSettings.pageOptionIsSupported(...args);
|
||||
const addLocalIndexRange = (...args) => sidebarPageSettings.addLocalIndexRange(...args);
|
||||
@@ -219,6 +220,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
const ingestSingleKnowledgeRagSource = (...args) => sidebarPageSettings.ingestSingleKnowledgeRagSource(...args);
|
||||
const loadKnowledgeRagStatus = (...args) => sidebarPageSettings.loadKnowledgeRagStatus(...args);
|
||||
const openKnowledgeRagDashboard = (...args) => sidebarPageSettings.openKnowledgeRagDashboard(...args);
|
||||
const createKnowledgeRagKnowledgeBase = (...args) => sidebarPageSettings.createKnowledgeRagKnowledgeBase(...args);
|
||||
const setActiveKnowledgeRagDetailTab = (...args) => sidebarPageSettings.setActiveKnowledgeRagDetailTab(...args);
|
||||
const performKnowledgeRagSearch = (...args) => sidebarPageSettings.performKnowledgeRagSearch(...args);
|
||||
const saveKnowledgeRagSearchParams = (...args) => sidebarPageSettings.saveKnowledgeRagSearchParams(...args);
|
||||
@@ -227,6 +229,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
const refreshDashboardPanel = (...args) => sidebarPageSettings.refreshDashboardPanel(...args);
|
||||
|
||||
const pruneKnowledgeRagRegistry = (...args) => sidebarPageSettings.pruneKnowledgeRagRegistry(...args);
|
||||
const setKnowledgeRagSelectedProviderKnowledgeBaseId = (...args) => sidebarPageSettings.setKnowledgeRagSelectedProviderKnowledgeBaseId(...args);
|
||||
const setKnowledgeRagSourceFilter = (...args) => sidebarPageSettings.setKnowledgeRagSourceFilter(...args);
|
||||
const useKnowledgeRagFileTreeSelection = (...args) => sidebarPageSettings.useKnowledgeRagFileTreeSelection(...args);
|
||||
const persistLocalIndexSettings = (...args) => sidebarPageSettings.persistLocalIndexSettings(...args);
|
||||
@@ -3055,6 +3058,19 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
void loadKnowledgeRagStatus(true);
|
||||
return;
|
||||
}
|
||||
if (knowledgeRagActionName === 'create-kb') {
|
||||
void createKnowledgeRagKnowledgeBase();
|
||||
return;
|
||||
}
|
||||
if (knowledgeRagActionName === 'focus-create-kb') {
|
||||
var createKbInput = document.querySelector('[data-knowledge-rag-create-kb-name]');
|
||||
if (createKbInput instanceof HTMLInputElement) createKbInput.focus();
|
||||
return;
|
||||
}
|
||||
if (knowledgeRagActionName === 'select-kb') {
|
||||
setKnowledgeRagSelectedProviderKnowledgeBaseId(knowledgeRagAction.getAttribute('data-knowledge-rag-kb-id') || '');
|
||||
return;
|
||||
}
|
||||
if (knowledgeRagActionName === 'add-source') {
|
||||
addKnowledgeRagSourceInput();
|
||||
return;
|
||||
@@ -3079,6 +3095,10 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
void deleteKnowledgeRagSource(knowledgeRagAction.getAttribute('data-knowledge-rag-source-path') || '');
|
||||
return;
|
||||
}
|
||||
if (knowledgeRagActionName === 'open-source-reference') {
|
||||
openKnowledgeRagSourceReference(knowledgeRagAction.getAttribute('data-knowledge-rag-source-path') || '');
|
||||
return;
|
||||
}
|
||||
if (knowledgeRagActionName === 'prune') {
|
||||
void pruneKnowledgeRagRegistry();
|
||||
return;
|
||||
@@ -3590,6 +3610,11 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
if (pageWidthSelect instanceof HTMLSelectElement) {
|
||||
var pageWidthType = pageWidthSelect.getAttribute('data-page-width-select') || '';
|
||||
void persistPageWidthPreference(pageWidthType, pageWidthSelect.value);
|
||||
return;
|
||||
}
|
||||
var knowledgeRagKbSelect = closestAction(event.target, '[data-knowledge-rag-kb-select]');
|
||||
if (knowledgeRagKbSelect instanceof HTMLSelectElement) {
|
||||
setKnowledgeRagSelectedProviderKnowledgeBaseId(knowledgeRagKbSelect.value);
|
||||
}
|
||||
});
|
||||
sidebarPageAi.installPageAiDelegates();
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::hermes_tools::ToolCallInput;
|
||||
use crate::routes::knowledge_rag::{
|
||||
KnowledgeRagOpenReferenceRequest, KnowledgeRagQueryRequest, KnowledgeRagSectionContextRequest,
|
||||
KnowledgeRagStatusQuery,
|
||||
KnowledgeRagOpenReferenceRequest, KnowledgeRagQueryRequest, KnowledgeRagSearchRequest,
|
||||
KnowledgeRagSectionContextRequest, KnowledgeRagStatusQuery,
|
||||
};
|
||||
use axum::extract::{Extension, Json, Query, State};
|
||||
use serde_json::{json, Value};
|
||||
@@ -36,22 +36,37 @@ pub async fn status(
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
pub async fn search(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let mut args = input.args.clone().unwrap_or_else(|| json!({}));
|
||||
ensure_weknora_scope(&args, input, context)?;
|
||||
inject_identity_args(&mut args, input);
|
||||
let body = serde_json::from_value::<KnowledgeRagSearchRequest>(args).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"mnote_weknora_search_payload_invalid",
|
||||
format!("WeKnora 检索参数无效: {error}"),
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
let Json(payload) = crate::routes::knowledge_rag::search(
|
||||
State(state.clone()),
|
||||
Extension(context.clone()),
|
||||
Json(body),
|
||||
)
|
||||
.await?;
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
pub async fn query(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let mut args = input.args.clone().unwrap_or_else(|| json!({}));
|
||||
if args.get("workspaceId").is_none() {
|
||||
if let Some(workspace_id) = input.effective_workspace_id() {
|
||||
args["workspaceId"] = json!(workspace_id);
|
||||
}
|
||||
}
|
||||
if args.get("rootUri").is_none() {
|
||||
if let Some(root_uri) = input.effective_root_uri() {
|
||||
args["rootUri"] = json!(root_uri);
|
||||
}
|
||||
}
|
||||
inject_identity_args(&mut args, input);
|
||||
let body = serde_json::from_value::<KnowledgeRagQueryRequest>(args).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"mnote_knowledge_rag_query_payload_invalid",
|
||||
@@ -74,16 +89,8 @@ pub async fn open_reference(
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let mut args = input.args.clone().unwrap_or_else(|| json!({}));
|
||||
if args.get("workspaceId").is_none() {
|
||||
if let Some(workspace_id) = input.effective_workspace_id() {
|
||||
args["workspaceId"] = json!(workspace_id);
|
||||
}
|
||||
}
|
||||
if args.get("rootUri").is_none() {
|
||||
if let Some(root_uri) = input.effective_root_uri() {
|
||||
args["rootUri"] = json!(root_uri);
|
||||
}
|
||||
}
|
||||
ensure_weknora_scope(&args, input, context)?;
|
||||
inject_identity_args(&mut args, input);
|
||||
let body =
|
||||
serde_json::from_value::<KnowledgeRagOpenReferenceRequest>(args).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
@@ -107,16 +114,7 @@ pub async fn section_context(
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let mut args = input.args.clone().unwrap_or_else(|| json!({}));
|
||||
if args.get("workspaceId").is_none() {
|
||||
if let Some(workspace_id) = input.effective_workspace_id() {
|
||||
args["workspaceId"] = json!(workspace_id);
|
||||
}
|
||||
}
|
||||
if args.get("rootUri").is_none() {
|
||||
if let Some(root_uri) = input.effective_root_uri() {
|
||||
args["rootUri"] = json!(root_uri);
|
||||
}
|
||||
}
|
||||
inject_identity_args(&mut args, input);
|
||||
let body =
|
||||
serde_json::from_value::<KnowledgeRagSectionContextRequest>(args).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
@@ -134,6 +132,109 @@ pub async fn section_context(
|
||||
Ok(compact_section_context_for_agent(payload))
|
||||
}
|
||||
|
||||
pub async fn list_sources(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let args = input.args.clone().unwrap_or_else(|| json!({}));
|
||||
ensure_weknora_scope(&args, input, context)?;
|
||||
let payload = status(state, context, input).await?;
|
||||
Ok(json!({
|
||||
"ok": payload.get("ok").cloned().unwrap_or_else(|| json!(true)),
|
||||
"schema": "mnote.weknora.sources_result.v1",
|
||||
"provider": payload.get("provider").cloned().unwrap_or_else(|| json!("weknora")),
|
||||
"providerConfig": payload.get("providerConfig").cloned().unwrap_or(Value::Null),
|
||||
"registry": payload.get("registry").cloned().unwrap_or(Value::Null),
|
||||
"documents": payload.get("documents").cloned().unwrap_or(Value::Null),
|
||||
"locatorPolicy": "WeKnora provider ids are returned separately; filenames and chunk ids are not local paths.",
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_source_status(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let args = input.args.clone().unwrap_or_else(|| json!({}));
|
||||
ensure_weknora_scope(&args, input, context)?;
|
||||
let payload = list_sources(state, context, input).await?;
|
||||
let requested_source = args
|
||||
.get("sourcePath")
|
||||
.or_else(|| args.get("providerKnowledgeId"))
|
||||
.or_else(|| args.get("providerSourceId"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let entries = payload
|
||||
.pointer("/registry/entries")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let matched = entries
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
requested_source.is_empty()
|
||||
|| entry
|
||||
.get("sourceRootRelativePath")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|value| value == requested_source)
|
||||
|| entry
|
||||
.get("sourceId")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|value| value == requested_source)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"schema": "mnote.weknora.source_status_result.v1",
|
||||
"provider": payload.get("provider").cloned().unwrap_or_else(|| json!("weknora")),
|
||||
"source": if matched.len() == 1 { matched[0].clone() } else { Value::Null },
|
||||
"sources": matched,
|
||||
"requestedSource": requested_source,
|
||||
}))
|
||||
}
|
||||
|
||||
fn inject_identity_args(args: &mut Value, input: &ToolCallInput) {
|
||||
if args.get("workspaceId").is_none() {
|
||||
if let Some(workspace_id) = input.effective_workspace_id() {
|
||||
args["workspaceId"] = json!(workspace_id);
|
||||
}
|
||||
}
|
||||
if args.get("rootUri").is_none() {
|
||||
if let Some(root_uri) = input.effective_root_uri() {
|
||||
args["rootUri"] = json!(root_uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_weknora_scope(
|
||||
args: &Value,
|
||||
input: &ToolCallInput,
|
||||
context: &RequestContext,
|
||||
) -> Result<(), WebError> {
|
||||
let has_root_uri = args
|
||||
.get("rootUri")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|value| !value.trim().is_empty())
|
||||
|| input.effective_root_uri().is_some();
|
||||
let has_scope = args.get("scope").is_some()
|
||||
|| args.get("allowlist").is_some()
|
||||
|| args.get("allowedRoots").is_some()
|
||||
|| args.get("aiAccessScope").is_some()
|
||||
|| args
|
||||
.get("sourcePaths")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|items| !items.is_empty());
|
||||
if has_root_uri && has_scope {
|
||||
return Ok(());
|
||||
}
|
||||
Err(WebError::bad_request_code(
|
||||
"mnote_weknora_scope_required",
|
||||
"WeKnora tool 调用必须包含 rootUri 以及 scope/allowlist/allowedRoots/aiAccessScope/sourcePaths 之一",
|
||||
)
|
||||
.with_context(context))
|
||||
}
|
||||
|
||||
fn compact_query_result_for_agent(payload: Value) -> Value {
|
||||
let references = payload
|
||||
.get("references")
|
||||
@@ -181,7 +282,7 @@ fn compact_query_result_for_agent(payload: Value) -> Value {
|
||||
json!({
|
||||
"ok": payload.get("ok").cloned().unwrap_or_else(|| json!(true)),
|
||||
"schema": "mnote.knowledge_rag.agent_query_result.v1",
|
||||
"provider": payload.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
|
||||
"provider": payload.get("provider").cloned().unwrap_or_else(|| json!("weknora")),
|
||||
"answerGuidance": "Final answers should answer the user in plain text. Do not copy citationMarkdown, citationUrl, /documents, mnote://, or search-engine wrapped local links into the answer. MNote UI will render citations[] / references[].citationMarkdown after the answer as clickable source locators. Treat rawMetadata.keywords and entity/relation counts only as query analysis, not proof that a source contains those words. Use references[].quote/contentDiagnostics as evidence. For book-like or skip-KG documents, call the tool with sourcePaths and includeDocumentStructureIndex=true; MNote fixes their effective retrieval mode to naive because they intentionally do not build KG. If documentStructureIndex is present, use it as a section map and call mnote.knowledge_rag.section_context with the section range when you need bounded chapter text for second-pass reading; do not cite the map itself unless the same claim appears in references[].quote or section_context text. If locatorDegraded is true, say the source location is degraded instead of inventing page or bbox. If contentDiagnostics.ocrTextExposed is false, say OCR text was not exposed in the returned reference instead of claiming OCR succeeded.",
|
||||
"references": references,
|
||||
"citations": citations,
|
||||
@@ -351,7 +452,7 @@ fn compact_section_context_for_agent(payload: Value) -> Value {
|
||||
json!({
|
||||
"ok": payload.get("ok").cloned().unwrap_or_else(|| json!(true)),
|
||||
"schema": payload.get("schema").cloned().unwrap_or_else(|| json!("mnote.knowledge_rag.section_context.v1")),
|
||||
"provider": payload.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
|
||||
"provider": payload.get("provider").cloned().unwrap_or_else(|| json!("weknora")),
|
||||
"sourceId": payload.get("sourceId").cloned().unwrap_or(Value::Null),
|
||||
"sourceRootRelativePath": payload.get("sourceRootRelativePath").cloned().unwrap_or(Value::Null),
|
||||
"lightRagDocId": payload.get("lightRagDocId").cloned().unwrap_or(Value::Null),
|
||||
@@ -382,7 +483,7 @@ fn compact_reference_for_agent(reference: &Value) -> Value {
|
||||
.unwrap_or_else(|| quote_diagnostics("e));
|
||||
json!({
|
||||
"schema": reference.get("schema").cloned().unwrap_or_else(|| json!("mnote.knowledge_rag.reference.v1")),
|
||||
"provider": reference.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
|
||||
"provider": reference.get("provider").cloned().unwrap_or_else(|| json!("weknora")),
|
||||
"citationId": reference.get("citationId").cloned().unwrap_or(Value::Null),
|
||||
"citationLabel": reference.get("citationLabel").cloned().unwrap_or(Value::Null),
|
||||
"filePath": reference.get("filePath").cloned().unwrap_or(Value::Null),
|
||||
@@ -416,7 +517,7 @@ fn compact_citation_for_agent(citation: &Value) -> Value {
|
||||
.unwrap_or_default();
|
||||
json!({
|
||||
"schema": citation.get("schema").cloned().unwrap_or_else(|| json!("mnote.knowledge_rag.citation.v1")),
|
||||
"provider": citation.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
|
||||
"provider": citation.get("provider").cloned().unwrap_or_else(|| json!("weknora")),
|
||||
"citationId": citation.get("citationId").cloned().unwrap_or(Value::Null),
|
||||
"citationLabel": citation.get("citationLabel").cloned().unwrap_or(Value::Null),
|
||||
"sourceId": citation.get("sourceId").cloned().unwrap_or(Value::Null),
|
||||
@@ -490,7 +591,7 @@ mod tests {
|
||||
fn compact_query_result_marks_post_filter_scope_without_raw_chunks() {
|
||||
let payload = json!({
|
||||
"ok": true,
|
||||
"provider": "lightrag",
|
||||
"provider": "lightrag_legacy",
|
||||
"sourceScope": ["docs/a.md"],
|
||||
"sourceScopeMode": "post_filter_mapped_references",
|
||||
"rawScopeFiltered": false,
|
||||
@@ -522,7 +623,9 @@ mod tests {
|
||||
assert!(compact.get("chunks").is_none());
|
||||
assert_eq!(
|
||||
compact["rawMetadataMeaning"].as_str(),
|
||||
Some("provider metadata describes query processing/retrieval bookkeeping; it is not source text evidence unless the same text appears in references[].quote")
|
||||
Some(
|
||||
"provider metadata describes query processing/retrieval bookkeeping; it is not source text evidence unless the same text appears in references[].quote"
|
||||
)
|
||||
);
|
||||
assert!(compact["answerGuidance"]
|
||||
.as_str()
|
||||
|
||||
@@ -57,6 +57,10 @@ fn doc_tools() -> Vec<Value> {
|
||||
fn knowledge_rag_tools() -> Vec<Value> {
|
||||
vec![
|
||||
knowledge_rag_status_tool(),
|
||||
weknora_search_tool(),
|
||||
weknora_list_sources_tool(),
|
||||
weknora_get_source_status_tool(),
|
||||
weknora_open_reference_tool(),
|
||||
knowledge_rag_query_tool(),
|
||||
knowledge_rag_section_context_tool(),
|
||||
knowledge_rag_open_reference_tool(),
|
||||
@@ -355,7 +359,7 @@ fn knowledge_rag_status_tool() -> Value {
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.knowledge_rag.status",
|
||||
"description": "查看 LightRAG 资料库 provider 状态、dashboard 地址、source registry 和同步状态。",
|
||||
"description": "查看当前资料库 provider 状态、dashboard 地址、source registry 和同步状态。默认 provider 是 WeKnora;LightRAG 仅作为 legacy fallback。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
@@ -367,6 +371,114 @@ fn knowledge_rag_status_tool() -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn weknora_scope_properties() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("rootUri".into(), json!({ "type": "string" }));
|
||||
map.insert("scope".into(), json!({ "type": "object" }));
|
||||
map.insert(
|
||||
"allowlist".into(),
|
||||
json!({ "type": "array", "items": { "type": "string" } }),
|
||||
);
|
||||
map.insert("allowedRoots".into(), json!({ "type": "array" }));
|
||||
map.insert("aiAccessScope".into(), json!({ "type": "object" }));
|
||||
map.insert(
|
||||
"sourcePaths".into(),
|
||||
json!({ "type": "array", "items": { "type": "string" } }),
|
||||
);
|
||||
}
|
||||
properties
|
||||
}
|
||||
|
||||
fn weknora_search_tool() -> Value {
|
||||
let mut properties = weknora_scope_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("query".into(), json!({ "type": "string" }));
|
||||
map.insert("topK".into(), json!({ "type": "integer", "default": 10 }));
|
||||
map.insert(
|
||||
"includeChunkContent".into(),
|
||||
json!({ "type": "boolean", "default": true }),
|
||||
);
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.weknora.search",
|
||||
"description": "只读调用 WeKnora 检索。必须带 rootUri 和 scope/allowlist,返回 provider ids、MNote source registry 映射以及 locatorDegraded;不要把 WeKnora filename/chunk id 当成本地 path。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "rootUri", "query"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn weknora_list_sources_tool() -> Value {
|
||||
json!({
|
||||
"name": "mnote.weknora.list_sources",
|
||||
"description": "只读列出 MNote source registry 与 WeKnora provider 状态。必须带 rootUri 和 scope/allowlist。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "rootUri"],
|
||||
"properties": weknora_scope_properties()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn weknora_get_source_status_tool() -> Value {
|
||||
let mut properties = weknora_scope_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("sourcePath".into(), json!({ "type": "string" }));
|
||||
map.insert("providerKnowledgeId".into(), json!({ "type": "string" }));
|
||||
map.insert("providerSourceId".into(), json!({ "type": "string" }));
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.weknora.get_source_status",
|
||||
"description": "只读查询某个 MNote source 或 WeKnora provider knowledge id 的映射状态。必须带 rootUri 和 scope/allowlist。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "rootUri"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn weknora_open_reference_tool() -> Value {
|
||||
let mut properties = weknora_scope_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("reference".into(), json!({ "type": "object" }));
|
||||
map.insert(
|
||||
"providerKnowledgeBaseId".into(),
|
||||
json!({ "type": "string" }),
|
||||
);
|
||||
map.insert("providerKnowledgeId".into(), json!({ "type": "string" }));
|
||||
map.insert("providerChunkId".into(), json!({ "type": "string" }));
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.weknora.open_reference",
|
||||
"description": "只读把 WeKnora reference 映射为 MNote open action。provider ids 独立返回;未命中 source registry 时必须视为 locatorDegraded。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "rootUri", "reference"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn knowledge_rag_query_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
@@ -391,7 +503,7 @@ fn knowledge_rag_query_tool() -> Value {
|
||||
json!({
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "返回由 LightRAG sidecar headings 派生的 document_structure_index;适合大书/长文档问题做章节导航和上下文扩展,不替代 references 引用证据。"
|
||||
"description": "返回由 provider sidecar/headings 派生的 document_structure_index;适合大书/长文档问题做章节导航和上下文扩展,不替代 references 引用证据。"
|
||||
}),
|
||||
);
|
||||
map.insert(
|
||||
@@ -399,13 +511,13 @@ fn knowledge_rag_query_tool() -> Value {
|
||||
json!({
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "可选 MNote workspace 相对路径范围;可传文件或目录。当前语义是 LightRAG provider 检索后,MNote 只过滤返回的 references;provider raw 仍可能是全局结果。"
|
||||
"description": "可选 MNote workspace 相对路径范围;可传文件或目录。当前语义是 provider 检索后,MNote 只过滤返回的 references;provider raw 仍可能是全局结果。"
|
||||
}),
|
||||
);
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.knowledge_rag.query",
|
||||
"description": "向 LightRAG 资料库提问,返回 provider 原始结果和经 MNote registry 映射、sourcePaths 后过滤的 references/citations。用户要求链接、来源、引用或证据时优先调用;返回的 citationMarkdown 由 MNote 前端自动追加成可点击来源定位,agent 不应在最终回答中手写 /documents、mnote:// 或搜索引擎包装链接。",
|
||||
"description": "向当前资料库 provider 提问,返回 provider 原始结果和经 MNote registry 映射、sourcePaths 后过滤的 references/citations。默认 provider 是 WeKnora,LightRAG 仅 legacy fallback。用户要求链接、来源、引用或证据时优先调用;返回的 citationMarkdown 由 MNote 前端自动追加成可点击来源定位,agent 不应在最终回答中手写 /documents、mnote:// 或搜索引擎包装链接。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
@@ -429,7 +541,7 @@ fn knowledge_rag_open_reference_tool() -> Value {
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.knowledge_rag.open_reference",
|
||||
"description": "把 LightRAG reference 映射为 MNote 可打开的本地 resource action;没有 registry 命中时明确返回定位降级。",
|
||||
"description": "把 provider reference 映射为 MNote 可打开的本地 resource action;没有 registry 命中时明确返回定位降级。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
@@ -474,7 +586,7 @@ fn knowledge_rag_section_context_tool() -> Value {
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.knowledge_rag.section_context",
|
||||
"description": "按 documentStructureIndex section 的 block/paragraph range,从 LightRAG native sidecar 拉取有限正文 blocks/chunks,供大书/长文档二次解读;不做检索、重排或 fallback。",
|
||||
"description": "按 documentStructureIndex section 的 block/paragraph range,从 provider sidecar 拉取有限正文 blocks/chunks,供大书/长文档二次解读;不做检索、重排或 fallback。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
|
||||
@@ -38,8 +38,7 @@ const CAPABILITY_PACKS: &[MnoteCapabilityPack] = &[
|
||||
MnoteCapabilityPack {
|
||||
id: "mnote-knowledge-rag",
|
||||
title: "资料库问答",
|
||||
description:
|
||||
"通过 LightRAG provider 检索多本书、论文、PDF 和附件资料库,并返回可回跳来源。",
|
||||
description: "通过当前知识库 provider 检索多本书、论文、PDF 和附件资料库,并返回可回跳来源;默认 provider 是 WeKnora。",
|
||||
category: "knowledge",
|
||||
agent_ids: &["hermes", "reasonix"],
|
||||
read_only: true,
|
||||
@@ -47,6 +46,10 @@ const CAPABILITY_PACKS: &[MnoteCapabilityPack] = &[
|
||||
tool_names: &[
|
||||
"mnote.context.snapshot",
|
||||
"mnote.context.resolve_target",
|
||||
"mnote.weknora.search",
|
||||
"mnote.weknora.list_sources",
|
||||
"mnote.weknora.get_source_status",
|
||||
"mnote.weknora.open_reference",
|
||||
"mnote.knowledge_rag.status",
|
||||
"mnote.knowledge_rag.query",
|
||||
"mnote.knowledge_rag.section_context",
|
||||
|
||||
@@ -16,6 +16,7 @@ pub mod hermes_tools;
|
||||
pub mod local_folder_watcher_registry;
|
||||
pub mod middleware;
|
||||
pub mod page_aggregate;
|
||||
pub mod provider_identity_sync;
|
||||
pub mod routes;
|
||||
pub mod ssr;
|
||||
pub mod transport;
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
use crate::error::WebError;
|
||||
use control_plane::UserRecord;
|
||||
use reqwest::header::{HeaderMap, HeaderValue};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::env;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::time::Duration;
|
||||
|
||||
const DEFAULT_OPENHUB_BASE_URL: &str = "http://127.0.0.1:18080";
|
||||
const DEFAULT_WEKNORA_ENDPOINT: &str = "http://127.0.0.1:8080/api/v1";
|
||||
const PROVIDER_IDENTITY_SYNC_TIMEOUT_MS: u64 = 1_500;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProviderIdentitySyncResult {
|
||||
pub provider: &'static str,
|
||||
pub ok: bool,
|
||||
pub message: String,
|
||||
pub provider_user_id: Option<String>,
|
||||
}
|
||||
|
||||
fn env_flag(key: &str, default: bool) -> bool {
|
||||
env::var(key)
|
||||
.ok()
|
||||
.map(|value| matches!(value.trim(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn clean_url(value: String) -> Option<String> {
|
||||
let trimmed = value.trim().trim_end_matches('/').to_string();
|
||||
(!trimmed.is_empty()).then_some(trimmed)
|
||||
}
|
||||
|
||||
fn openhub_base_url() -> String {
|
||||
env::var("MNOTE_OPENHUB_BASE_URL")
|
||||
.ok()
|
||||
.and_then(clean_url)
|
||||
.unwrap_or_else(|| DEFAULT_OPENHUB_BASE_URL.to_string())
|
||||
}
|
||||
|
||||
fn weknora_endpoint() -> String {
|
||||
env::var("MNOTE_WEKNORA_ENDPOINT")
|
||||
.or_else(|_| env::var("WEKNORA_ENDPOINT"))
|
||||
.ok()
|
||||
.and_then(clean_url)
|
||||
.unwrap_or_else(|| DEFAULT_WEKNORA_ENDPOINT.to_string())
|
||||
}
|
||||
|
||||
fn join_url(base_url: &str, path: &str) -> String {
|
||||
format!(
|
||||
"{}/{}",
|
||||
base_url.trim_end_matches('/'),
|
||||
path.trim_start_matches('/')
|
||||
)
|
||||
}
|
||||
|
||||
fn internal_headers() -> HeaderMap {
|
||||
let mut headers = HeaderMap::new();
|
||||
if let Ok(secret) = env::var("MNOTE_PROVIDER_IDENTITY_SYNC_SECRET")
|
||||
.or_else(|_| env::var("MNOTE_INTERNAL_API_SECRET"))
|
||||
.or_else(|_| env::var("INTERNAL_API_SECRET"))
|
||||
{
|
||||
if let Ok(value) = HeaderValue::from_str(secret.trim()) {
|
||||
headers.insert("x-internal-token", value);
|
||||
}
|
||||
}
|
||||
headers
|
||||
}
|
||||
|
||||
fn stable_openhub_user_id(user_id: &str) -> i64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
format!("mnote-openhub-user:{user_id}").hash(&mut hasher);
|
||||
(hasher.finish() % 1_900_000_000) as i64 + 100_000_000
|
||||
}
|
||||
|
||||
fn fallback_email(user: &UserRecord) -> String {
|
||||
user.email
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| format!("{}@mnote.local", user.username))
|
||||
}
|
||||
|
||||
fn default_workspace_path(user: &UserRecord) -> String {
|
||||
format!(
|
||||
"/mnt/Data1T/Mnote_data/users/{}/workspaces/my-space",
|
||||
user.id
|
||||
)
|
||||
}
|
||||
|
||||
fn sync_disabled() -> bool {
|
||||
!env_flag("MNOTE_PROVIDER_IDENTITY_SYNC", true)
|
||||
}
|
||||
|
||||
pub async fn sync_provider_identities(
|
||||
user: &UserRecord,
|
||||
password: &str,
|
||||
) -> Vec<ProviderIdentitySyncResult> {
|
||||
if sync_disabled() {
|
||||
return vec![ProviderIdentitySyncResult {
|
||||
provider: "all",
|
||||
ok: true,
|
||||
message: "provider identity sync disabled".to_string(),
|
||||
provider_user_id: None,
|
||||
}];
|
||||
}
|
||||
|
||||
let sync_openhub = env_flag("MNOTE_PROVIDER_IDENTITY_SYNC_OPENHUB", true);
|
||||
let sync_weknora = env_flag("MNOTE_PROVIDER_IDENTITY_SYNC_WEKNORA", true);
|
||||
|
||||
match (sync_openhub, sync_weknora) {
|
||||
(true, true) => {
|
||||
let (openhub, weknora) = tokio::join!(
|
||||
sync_openhub_identity(user, password),
|
||||
sync_weknora_identity(user, password)
|
||||
);
|
||||
vec![openhub, weknora]
|
||||
}
|
||||
(true, false) => vec![sync_openhub_identity(user, password).await],
|
||||
(false, true) => vec![sync_weknora_identity(user, password).await],
|
||||
(false, false) => vec![ProviderIdentitySyncResult {
|
||||
provider: "all",
|
||||
ok: true,
|
||||
message: "provider identity sync disabled".to_string(),
|
||||
provider_user_id: None,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
async fn post_json(url: String, body: Value) -> Result<Value, WebError> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(PROVIDER_IDENTITY_SYNC_TIMEOUT_MS))
|
||||
.build()
|
||||
.map_err(|error| WebError::internal(format!("provider identity sync client: {error}")))?;
|
||||
let response = client
|
||||
.post(url.clone())
|
||||
.headers(internal_headers())
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
WebError::bad_gateway_code(
|
||||
"provider_identity_sync_unreachable",
|
||||
format!("{url}: {error}"),
|
||||
)
|
||||
})?;
|
||||
let status = response.status();
|
||||
let payload = response.json::<Value>().await.unwrap_or_else(|_| json!({}));
|
||||
if !status.is_success() {
|
||||
return Err(WebError::bad_gateway_code(
|
||||
"provider_identity_sync_failed",
|
||||
format!("{url} returned {status}: {payload}"),
|
||||
));
|
||||
}
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
async fn sync_openhub_identity(user: &UserRecord, password: &str) -> ProviderIdentitySyncResult {
|
||||
let provider_user_id = stable_openhub_user_id(&user.id);
|
||||
let payload = json!({
|
||||
"provider_user_id": provider_user_id,
|
||||
"mnote_user_id": user.id,
|
||||
"username": user.username,
|
||||
"email": fallback_email(user),
|
||||
"password": password,
|
||||
"workspace_path": default_workspace_path(user),
|
||||
"disabled": user.status != "active",
|
||||
"is_admin": false,
|
||||
});
|
||||
match post_json(
|
||||
join_url(&openhub_base_url(), "/api/internal/mnote/users/provision"),
|
||||
payload,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => ProviderIdentitySyncResult {
|
||||
provider: "openhub",
|
||||
ok: true,
|
||||
message: "synced".to_string(),
|
||||
provider_user_id: Some(provider_user_id.to_string()),
|
||||
},
|
||||
Err(error) => ProviderIdentitySyncResult {
|
||||
provider: "openhub",
|
||||
ok: false,
|
||||
message: error.message().to_string(),
|
||||
provider_user_id: Some(provider_user_id.to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn sync_weknora_identity(user: &UserRecord, password: &str) -> ProviderIdentitySyncResult {
|
||||
let payload = json!({
|
||||
"mnote_user_id": user.id,
|
||||
"username": user.username,
|
||||
"email": fallback_email(user),
|
||||
"password": password,
|
||||
"role": "contributor",
|
||||
"is_active": user.status == "active",
|
||||
});
|
||||
match post_json(
|
||||
join_url(&weknora_endpoint(), "/internal/mnote/users/provision"),
|
||||
payload,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(payload) => ProviderIdentitySyncResult {
|
||||
provider: "weknora",
|
||||
ok: true,
|
||||
message: "synced".to_string(),
|
||||
provider_user_id: payload
|
||||
.pointer("/user/id")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned),
|
||||
},
|
||||
Err(error) => ProviderIdentitySyncResult {
|
||||
provider: "weknora",
|
||||
ok: false,
|
||||
message: error.message().to_string(),
|
||||
provider_user_id: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::provider_identity_sync::sync_provider_identities;
|
||||
use crate::routes::local_folder_source::{
|
||||
control_plane_db_path_display, create_default_local_workspace_for_actor,
|
||||
ensure_local_workspace_read_access_with_state, is_local_access_policy_admin_context,
|
||||
@@ -136,7 +137,7 @@ pub async fn auth_api(
|
||||
return Ok(build_sqlite_sign_out_response(&state, &context));
|
||||
}
|
||||
|
||||
handle_sqlite_auth_action(&state, &context, &payload)
|
||||
handle_sqlite_auth_action(&state, &context, &payload).await
|
||||
}
|
||||
|
||||
pub async fn auth_entry(
|
||||
@@ -2069,7 +2070,7 @@ fn has_real_auth_context(state: &AppState, context: &RequestContext) -> bool {
|
||||
|| extract_cookie_value(context, COOKIE_MNOTE_WEB_CONVEX_TOKEN).is_some()
|
||||
}
|
||||
|
||||
fn handle_sqlite_auth_action(
|
||||
async fn handle_sqlite_auth_action(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
payload: &serde_json::Value,
|
||||
@@ -2098,6 +2099,7 @@ fn handle_sqlite_auth_action(
|
||||
})?;
|
||||
let session_token = new_session_token();
|
||||
let token_hash = session_token_hash(&session_token);
|
||||
let password_for_provider_sync = password.clone();
|
||||
let resolved = if flow == "signUp" {
|
||||
let email = params
|
||||
.get("email")
|
||||
@@ -2200,14 +2202,46 @@ fn handle_sqlite_auth_action(
|
||||
.unwrap_or_else(|_| "{}".to_string()),
|
||||
});
|
||||
|
||||
Ok(build_sqlite_auth_response(
|
||||
let provider_sync_results = if flow == "signUp" {
|
||||
let results = sync_provider_identities(&resolved.user, &password_for_provider_sync).await;
|
||||
let _ = state.control_plane().append_audit(AppendAuditInput {
|
||||
actor_user_id: Some(resolved.user.id.clone()),
|
||||
action: "control.auth.provider_identities_synced".to_string(),
|
||||
target_kind: "user".to_string(),
|
||||
target_id: Some(resolved.user.id.clone()),
|
||||
metadata_json: serde_json::to_string(&json!({
|
||||
"providers": results.iter().map(|item| json!({
|
||||
"provider": item.provider,
|
||||
"ok": item.ok,
|
||||
"message": item.message,
|
||||
"providerUserId": item.provider_user_id,
|
||||
})).collect::<Vec<_>>(),
|
||||
}))
|
||||
.unwrap_or_else(|_| "{}".to_string()),
|
||||
});
|
||||
Some(results)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut response = build_sqlite_auth_response(
|
||||
context,
|
||||
&session_token,
|
||||
&resolved.user.id,
|
||||
resolved.user.email.as_deref().unwrap_or_default(),
|
||||
&resolved.user.display_name,
|
||||
&effective_sqlite_auth_actor_type(&resolved.user.id, &resolved.user.role),
|
||||
))
|
||||
);
|
||||
if let Some(results) = provider_sync_results {
|
||||
let all_ok = results.iter().all(|item| item.ok);
|
||||
let header_value = if all_ok { "ok" } else { "partial" };
|
||||
if let Ok(value) = HeaderValue::from_str(header_value) {
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("x-mnote-provider-identity-sync", value);
|
||||
}
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn sqlite_auth_error(
|
||||
|
||||
@@ -368,11 +368,21 @@ pub(crate) async fn execute_mnote_tool_call(
|
||||
Err(WebError::new(
|
||||
StatusCode::GONE,
|
||||
"mnote_evidence_tools_retired",
|
||||
"旧 docs/evidence/LiteParse tools 已退役;请使用 mnote.knowledge_rag.query/open_reference",
|
||||
"旧 docs/evidence/LiteParse tools 已退役;请使用 mnote.weknora.search/open_reference 或兼容 mnote.knowledge_rag.query/open_reference",
|
||||
)
|
||||
.with_context(&context))
|
||||
}
|
||||
"mnote.knowledge_rag.status" => knowledge_rag::status(&state, &context, &input).await,
|
||||
"mnote.weknora.search" => knowledge_rag::search(&state, &context, &input).await,
|
||||
"mnote.weknora.list_sources" => {
|
||||
knowledge_rag::list_sources(&state, &context, &input).await
|
||||
}
|
||||
"mnote.weknora.get_source_status" => {
|
||||
knowledge_rag::get_source_status(&state, &context, &input).await
|
||||
}
|
||||
"mnote.weknora.open_reference" => {
|
||||
knowledge_rag::open_reference(&state, &context, &input).await
|
||||
}
|
||||
"mnote.knowledge_rag.query" => knowledge_rag::query(&state, &context, &input).await,
|
||||
"mnote.knowledge_rag.section_context" => {
|
||||
knowledge_rag::section_context(&state, &context, &input).await
|
||||
@@ -384,7 +394,7 @@ pub(crate) async fn execute_mnote_tool_call(
|
||||
WebError::new(
|
||||
StatusCode::GONE,
|
||||
"mnote_index_tools_retired",
|
||||
"旧本地索引 tools 已退役;资料索引统一由 LightRAG provider 处理",
|
||||
"旧本地索引 tools 已退役;资料索引统一由当前知识库 provider 处理",
|
||||
)
|
||||
.with_context(&context),
|
||||
),
|
||||
@@ -728,6 +738,10 @@ fn is_read_tool(tool_name: &str) -> bool {
|
||||
| "mnote.evidence.read"
|
||||
| "mnote.evidence.open"
|
||||
| "mnote.knowledge_rag.status"
|
||||
| "mnote.weknora.search"
|
||||
| "mnote.weknora.list_sources"
|
||||
| "mnote.weknora.get_source_status"
|
||||
| "mnote.weknora.open_reference"
|
||||
| "mnote.knowledge_rag.query"
|
||||
| "mnote.knowledge_rag.section_context"
|
||||
| "mnote.knowledge_rag.open_reference"
|
||||
@@ -761,6 +775,10 @@ fn is_evidence_receipt_tool(tool_name: &str) -> bool {
|
||||
| "mnote.evidence.read"
|
||||
| "mnote.evidence.open"
|
||||
| "mnote.knowledge_rag.status"
|
||||
| "mnote.weknora.search"
|
||||
| "mnote.weknora.list_sources"
|
||||
| "mnote.weknora.get_source_status"
|
||||
| "mnote.weknora.open_reference"
|
||||
| "mnote.knowledge_rag.query"
|
||||
| "mnote.knowledge_rag.section_context"
|
||||
| "mnote.knowledge_rag.open_reference"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@ mod onlyoffice;
|
||||
pub(crate) mod onlyoffice_bridge;
|
||||
mod page_ai_board;
|
||||
mod page_ai_opencode;
|
||||
mod page_ai_openhub;
|
||||
mod page_ai_workflow;
|
||||
mod query_support;
|
||||
mod resource_trash;
|
||||
@@ -91,6 +92,10 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/api/knowledge-rag/pipeline-events",
|
||||
get(knowledge_rag::pipeline_events),
|
||||
)
|
||||
.route(
|
||||
"/api/knowledge-rag/knowledge-bases",
|
||||
post(knowledge_rag::create_knowledge_base),
|
||||
)
|
||||
.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))
|
||||
@@ -389,6 +394,68 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/api/page-ai/opencode/status",
|
||||
get(page_ai_opencode::status),
|
||||
)
|
||||
.route("/api/page-ai/openhub/status", get(page_ai_openhub::status))
|
||||
.route(
|
||||
"/api/page-ai/openhub/bootstrap",
|
||||
post(page_ai_openhub::bootstrap),
|
||||
)
|
||||
.route(
|
||||
"/api/page-ai/openhub/artifact-index",
|
||||
get(page_ai_openhub::artifact_index_get).post(page_ai_openhub::artifact_index_upsert),
|
||||
)
|
||||
.route("/page-ai/openhub/ai", get(page_ai_openhub::ai_shell))
|
||||
.route(
|
||||
"/page-ai/openhub/ai/{*path}",
|
||||
any(page_ai_openhub::ai_proxy),
|
||||
)
|
||||
.route(
|
||||
"/page-ai/openhub/login",
|
||||
any(page_ai_openhub::non_ai_route_guard),
|
||||
)
|
||||
.route(
|
||||
"/page-ai/openhub/login/{*path}",
|
||||
any(page_ai_openhub::non_ai_route_guard),
|
||||
)
|
||||
.route(
|
||||
"/page-ai/openhub/admin",
|
||||
any(page_ai_openhub::non_ai_route_guard),
|
||||
)
|
||||
.route(
|
||||
"/page-ai/openhub/admin/{*path}",
|
||||
any(page_ai_openhub::non_ai_route_guard),
|
||||
)
|
||||
.route(
|
||||
"/page-ai/openhub/file",
|
||||
any(page_ai_openhub::non_ai_route_guard),
|
||||
)
|
||||
.route(
|
||||
"/page-ai/openhub/file/{*path}",
|
||||
any(page_ai_openhub::non_ai_route_guard),
|
||||
)
|
||||
.route(
|
||||
"/page-ai/openhub/files",
|
||||
any(page_ai_openhub::non_ai_route_guard),
|
||||
)
|
||||
.route(
|
||||
"/page-ai/openhub/files/{*path}",
|
||||
any(page_ai_openhub::non_ai_route_guard),
|
||||
)
|
||||
.route(
|
||||
"/page-ai/openhub/knowledge",
|
||||
any(page_ai_openhub::non_ai_route_guard),
|
||||
)
|
||||
.route(
|
||||
"/page-ai/openhub/knowledge/{*path}",
|
||||
any(page_ai_openhub::non_ai_route_guard),
|
||||
)
|
||||
.route(
|
||||
"/page-ai/openhub/git",
|
||||
any(page_ai_openhub::non_ai_route_guard),
|
||||
)
|
||||
.route(
|
||||
"/page-ai/openhub/git/{*path}",
|
||||
any(page_ai_openhub::non_ai_route_guard),
|
||||
)
|
||||
.route(
|
||||
"/api/page-ai/opencode/session",
|
||||
post(page_ai_opencode::bind_session),
|
||||
@@ -452,16 +519,31 @@ pub fn build_router(state: AppState) -> Router {
|
||||
.route("/page-ai/opencode/{*path}", any(page_ai_opencode::proxy))
|
||||
.route("/assets/{*path}", any(page_ai_opencode::proxy_assets))
|
||||
.route("/global/{*path}", any(page_ai_opencode::proxy_assets))
|
||||
.route("/favicon-96x96-v3.png", any(page_ai_opencode::proxy_current_path))
|
||||
.route(
|
||||
"/favicon-96x96-v3.png",
|
||||
any(page_ai_opencode::proxy_current_path),
|
||||
)
|
||||
.route("/favicon-v3.svg", any(page_ai_opencode::proxy_current_path))
|
||||
.route("/favicon-v3.ico", any(page_ai_opencode::proxy_current_path))
|
||||
.route("/apple-touch-icon-v3.png", any(page_ai_opencode::proxy_current_path))
|
||||
.route("/site.webmanifest", any(page_ai_opencode::proxy_current_path))
|
||||
.route("/social-share.png", any(page_ai_opencode::proxy_current_path))
|
||||
.route(
|
||||
"/apple-touch-icon-v3.png",
|
||||
any(page_ai_opencode::proxy_current_path),
|
||||
)
|
||||
.route(
|
||||
"/site.webmanifest",
|
||||
any(page_ai_opencode::proxy_current_path),
|
||||
)
|
||||
.route(
|
||||
"/social-share.png",
|
||||
any(page_ai_opencode::proxy_current_path),
|
||||
)
|
||||
.route("/provider", any(page_ai_opencode::proxy_current_path))
|
||||
.route("/path", any(page_ai_opencode::proxy_current_path))
|
||||
.route("/project", any(page_ai_opencode::proxy_current_path))
|
||||
.route("/project/{*path}", any(page_ai_opencode::proxy_current_path))
|
||||
.route(
|
||||
"/project/{*path}",
|
||||
any(page_ai_opencode::proxy_current_path),
|
||||
)
|
||||
.route("/lsp", any(page_ai_opencode::proxy_current_path))
|
||||
.route("/command", any(page_ai_opencode::proxy_current_path))
|
||||
.route("/mcp", any(page_ai_opencode::proxy_current_path))
|
||||
@@ -472,10 +554,19 @@ pub fn build_router(state: AppState) -> Router {
|
||||
.route("/question", any(page_ai_opencode::proxy_current_path))
|
||||
.route("/event", any(page_ai_opencode::proxy_current_path))
|
||||
.route("/session", any(page_ai_opencode::proxy_current_path))
|
||||
.route("/session/{*path}", any(page_ai_opencode::proxy_current_path))
|
||||
.route(
|
||||
"/session/{*path}",
|
||||
any(page_ai_opencode::proxy_current_path),
|
||||
)
|
||||
.route("/new-session", any(page_ai_opencode::proxy_current_path))
|
||||
.route("/{opencode_dir}/session", any(page_ai_opencode::proxy_current_path))
|
||||
.route("/{opencode_dir}/session/{*path}", any(page_ai_opencode::proxy_current_path))
|
||||
.route(
|
||||
"/{opencode_dir}/session",
|
||||
any(page_ai_opencode::proxy_current_path),
|
||||
)
|
||||
.route(
|
||||
"/{opencode_dir}/session/{*path}",
|
||||
any(page_ai_opencode::proxy_current_path),
|
||||
)
|
||||
.route("/api/page-ai/board/status", get(page_ai_board::status))
|
||||
.route("/api/page-ai/board/workers", get(page_ai_board::workers))
|
||||
.route(
|
||||
|
||||
@@ -142,7 +142,9 @@ fn file_uri_to_path(root_uri: &str) -> Option<String> {
|
||||
Some(percent_decode_path_lossy(&normalized))
|
||||
}
|
||||
|
||||
fn opencode_project_directory_for_request(request: Option<&OpencodeSessionRequest>) -> Option<String> {
|
||||
fn opencode_project_directory_for_request(
|
||||
request: Option<&OpencodeSessionRequest>,
|
||||
) -> Option<String> {
|
||||
if let Some(override_directory) = std::env::var("MNOTE_OPENCODE_PROJECT_DIR")
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
@@ -1025,6 +1027,23 @@ async fn proxy_to_opencode(
|
||||
body: Body,
|
||||
) -> Result<Response, WebError> {
|
||||
ensure_authenticated(&context)?;
|
||||
if std::env::var("MNOTE_PAGE_AI_OPENCODE_LEGACY_PROXY")
|
||||
.ok()
|
||||
.map(|value| value.trim() == "1")
|
||||
.unwrap_or(false)
|
||||
!= true
|
||||
{
|
||||
return Response::builder()
|
||||
.status(StatusCode::GONE)
|
||||
.header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
|
||||
.body(Body::from("page_ai_opencode_legacy_proxy_disabled"))
|
||||
.map_err(|error| {
|
||||
WebError::internal(format!(
|
||||
"opencode legacy proxy disabled response 构造失败: {error}"
|
||||
))
|
||||
.with_context(&context)
|
||||
});
|
||||
}
|
||||
let client =
|
||||
opencode_client(Duration::from_secs(120)).map_err(|error| error.with_context(&context))?;
|
||||
let url = upstream_url(&path, uri.query()).map_err(|error| error.with_context(&context))?;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2530,9 +2530,536 @@
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-settings-panel {
|
||||
width: min(440px, calc(100vw - 24px));
|
||||
width: min(1120px, calc(100vw - 24px));
|
||||
max-height: calc(100vh - 72px);
|
||||
overflow-y: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page {
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
border-radius: 10px;
|
||||
background: #F6F7F9;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-title {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-eyebrow {
|
||||
color: #2F7D4A;
|
||||
font: 11px/16px "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-title strong {
|
||||
color: #111827;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-title span:last-child {
|
||||
color: #6B7280;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-actions button {
|
||||
display: inline-flex;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
color: #4B5563;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-actions button:hover:not(:disabled) {
|
||||
background: #F3F4F6;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-status {
|
||||
padding: 10px 18px;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
background: #F9FAFB;
|
||||
color: #4B5563;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-page-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 320px) minmax(0, 1fr);
|
||||
min-height: 620px;
|
||||
max-height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-pane,
|
||||
.mnote-weknora-kb-detail-pane {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border-right: 1px solid #E5E7EB;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-head div {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-head strong {
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-head span {
|
||||
min-width: 22px;
|
||||
border-radius: 999px;
|
||||
background: #EEF2FF;
|
||||
color: #3730A3;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-head button,
|
||||
.mnote-weknora-doc-toolbar button,
|
||||
.mnote-weknora-doc-actions button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-height: 30px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-head button {
|
||||
width: 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-head button:hover,
|
||||
.mnote-weknora-doc-toolbar button:hover,
|
||||
.mnote-weknora-doc-actions button:hover {
|
||||
background: #F3F4F6;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-card-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-card {
|
||||
display: grid;
|
||||
grid-template-columns: 28px minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-card.is-active {
|
||||
border-color: #2F7D4A;
|
||||
background: #F0F9F4;
|
||||
box-shadow: inset 3px 0 0 #2F7D4A;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-card-star {
|
||||
display: inline-flex;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: #EEF2FF;
|
||||
color: #3730A3;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-card-main {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-card-main strong,
|
||||
.mnote-weknora-kb-card-main em {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-card-main strong {
|
||||
color: #111827;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 19px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-card-main em {
|
||||
color: #6B7280;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-card-main span {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-card-main i {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
color: #6B7280;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-card-main i.is-processing {
|
||||
color: #B45309;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-card-main .material-symbols-outlined {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-card-status,
|
||||
.mnote-weknora-kb-type-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 999px;
|
||||
background: #ECFDF5;
|
||||
color: #047857;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
padding: 0 7px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-create-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border: 1px dashed #D1D5DB;
|
||||
border-radius: 8px;
|
||||
background: #F9FAFB;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-create-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #111827;
|
||||
font-size: 13px;
|
||||
line-height: 19px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-detail-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-detail-hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(210px, 280px);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
padding: 16px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-detail-hero h2 {
|
||||
margin: 8px 0 4px;
|
||||
color: #111827;
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
color: #6B7280;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-breadcrumb strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-detail-hero p {
|
||||
margin: 0;
|
||||
color: #6B7280;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-toolbar,
|
||||
.mnote-weknora-doc-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 184px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-height: 260px;
|
||||
padding: 12px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-sidebar-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-sidebar-head strong {
|
||||
color: #111827;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-sidebar-head span {
|
||||
color: #9CA3AF;
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-source-tags,
|
||||
.mnote-weknora-doc-tags {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-source-tags button,
|
||||
.mnote-weknora-doc-tags span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-height: 28px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 7px;
|
||||
background: #FFFFFF;
|
||||
color: #4B5563;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
padding: 0 9px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-source-tags button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-source-tags button[data-active="true"] {
|
||||
border-color: #A7D8B8;
|
||||
background: #F0F9F4;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-source-tags .material-symbols-outlined {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-main {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mnote-weknora-view-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.mnote-weknora-view-switch button {
|
||||
width: 28px;
|
||||
min-height: 26px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #6B7280;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mnote-weknora-view-switch button.is-active {
|
||||
background: #EAF7EA;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-search {
|
||||
display: flex;
|
||||
flex: 1 1 260px;
|
||||
min-width: 220px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 34px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-search input {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: #111827;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-table[data-weknora-document-view="grid"] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-table::before {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 104px 104px 124px;
|
||||
gap: 8px;
|
||||
padding: 9px 12px;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
background: #F9FAFB;
|
||||
color: #6B7280;
|
||||
content: "文档 状态 Chunk 操作";
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-table[data-weknora-document-view="grid"]::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnote-weknora-placeholder-panel {
|
||||
display: flex;
|
||||
min-height: 220px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 18px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.mnote-weknora-placeholder-panel strong {
|
||||
color: #111827;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.mnote-weknora-placeholder-panel span {
|
||||
max-width: 560px;
|
||||
font-size: 12px;
|
||||
line-height: 19px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta {
|
||||
@@ -2663,6 +3190,27 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-main .mnote-weknora-source-aux {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.mnote-weknora-source-aux i {
|
||||
display: inline-flex;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: #F3F4F6;
|
||||
color: #4B5563;
|
||||
font-style: normal;
|
||||
line-height: 18px;
|
||||
padding: 0 7px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-progress {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
@@ -2747,5 +3295,3 @@
|
||||
color: #1B1C1C;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2221,10 +2221,71 @@ button.wolai-page-ai-history-main span {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wolai-page-ai-drawer[data-page-ai-openhub-host="true"] .wolai-page-ai-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wolai-page-ai-drawer[data-page-ai-openhub-host="true"] .wolai-page-ai-resize-handle {
|
||||
left: -12px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-opencode-header {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.wolai-page-ai-openhub-diagnostics {
|
||||
flex: 0 0 auto;
|
||||
max-height: 32px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid rgba(27, 28, 28, 0.08);
|
||||
background: rgba(247, 247, 245, 0.78);
|
||||
color: rgba(27, 28, 28, 0.58);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-openhub-diagnostics[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.wolai-page-ai-openhub-diagnostics summary {
|
||||
min-height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.wolai-page-ai-openhub-diagnostics summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wolai-page-ai-openhub-diagnostics:not([open]) > :not(summary) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.wolai-page-ai-openhub-diagnostics[open] {
|
||||
max-height: min(36vh, 220px);
|
||||
overflow: auto;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-openhub-diagnostics[open] summary {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-openhub-diagnostics .wolai-page-ai-opencode-row,
|
||||
.wolai-page-ai-openhub-diagnostics .wolai-page-ai-opencode-empty,
|
||||
.wolai-page-ai-openhub-diagnostics .wolai-page-ai-opencode-badges {
|
||||
margin-right: 12px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-opencode-chrome {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
|
||||
Reference in New Issue
Block a user