chore: checkpoint turso and ai runtime work
This commit is contained in:
@@ -1469,7 +1469,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const isKnowledgeRagSourceEntry = (entry) => {
|
||||
if (!entry || String(entry.sourceKind || '').trim() !== 'local_folder') return false;
|
||||
if (!String(entry.rootUri || '').trim() || !String(entry.path || '').trim()) return false;
|
||||
return entry.kind === 'image' || entry.kind === 'pdf';
|
||||
return ['image', 'pdf', 'markdown', 'text', 'code'].includes(entry.kind);
|
||||
};
|
||||
|
||||
const knowledgeRagProvider = () => {
|
||||
@@ -1487,11 +1487,11 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const statusNode = entry.panel.querySelector('[data-mnote-knowledge-rag-status-text]');
|
||||
if (statusNode instanceof HTMLElement) {
|
||||
statusNode.textContent = message || (
|
||||
normalizedStatus === 'done' ? '资料库已索引'
|
||||
: normalizedStatus === 'running' ? '资料库索引中'
|
||||
: normalizedStatus === 'failed' ? '资料库索引失败'
|
||||
: normalizedStatus === 'stale' ? '资料库需更新'
|
||||
: '资料库未索引'
|
||||
normalizedStatus === 'done' ? '知识库已索引'
|
||||
: normalizedStatus === 'running' ? '知识库索引中'
|
||||
: normalizedStatus === 'failed' ? '知识库索引失败'
|
||||
: normalizedStatus === 'stale' ? '知识库需更新'
|
||||
: '知识库未索引'
|
||||
);
|
||||
}
|
||||
const openButton = entry.panel.querySelector('[data-mnote-knowledge-rag-action="open"]');
|
||||
@@ -1523,7 +1523,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
artifactRootRelativePath: '',
|
||||
provider: knowledgeRagProvider(),
|
||||
status,
|
||||
stageLabel: status === 'done' ? '资料库已索引' : status === 'failed' ? '资料库需重建' : '资料库索引中',
|
||||
stageLabel: status === 'done' ? '知识库已索引' : status === 'failed' ? '知识库需重建' : '知识库索引中',
|
||||
stale: registryEntry.stale === true,
|
||||
updatedAtMs: Number(registryEntry.updatedAtMs || registryEntry.updated_at_ms || Date.now()),
|
||||
finishedAtMs: status === 'done' ? Number(registryEntry.indexedAtMs || registryEntry.indexed_at_ms || Date.now()) : null,
|
||||
@@ -1750,7 +1750,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
.filter((node) => node instanceof HTMLButtonElement);
|
||||
taskToggles.forEach((toggle) => {
|
||||
const opensSettings = toggle.getAttribute('data-mnote-action') === 'open-knowledge-rag-settings';
|
||||
const label = runningCount > 0 ? `${runningCount} 个资料库索引处理中,打开资料库设置` : (jobs.length > 0 ? `${jobs.length} 个资料库任务,打开资料库设置` : '资料库设置');
|
||||
const label = runningCount > 0 ? `${runningCount} 个知识库索引处理中,打开知识库设置` : (jobs.length > 0 ? `${jobs.length} 个知识库任务,打开知识库设置` : '知识库设置');
|
||||
const taskLabel = runningCount > 0 ? `${runningCount} 个后台任务正在运行` : (jobs.length > 0 ? `${jobs.length} 个后台任务` : '后台任务');
|
||||
toggle.setAttribute('title', opensSettings ? label : taskLabel);
|
||||
toggle.setAttribute('aria-label', opensSettings ? label : taskLabel);
|
||||
@@ -1811,7 +1811,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
row.setAttribute('data-mnote-knowledge-rag-task-row', String(job.sourceRootRelativePath || ''));
|
||||
row.setAttribute('data-mnote-knowledge-rag-task-status', String(job.status || ''));
|
||||
row.setAttribute('data-mnote-knowledge-rag-task-category', knowledgeRagTaskCategory(job));
|
||||
const title = String(job.sourceRootRelativePath || '').split('/').filter(Boolean).pop() || '资料库索引';
|
||||
const title = String(job.sourceRootRelativePath || '').split('/').filter(Boolean).pop() || '知识库索引';
|
||||
const category = knowledgeRagTaskCategory(job);
|
||||
const progress = knowledgeRagTaskProgress(job);
|
||||
row.innerHTML = '<div class="mnote-knowledge-rag-task-main"><div class="mnote-knowledge-rag-task-title-line"><strong></strong><em></em></div><span></span><div class="mnote-knowledge-rag-task-progress" role="progressbar"><i></i></div></div><div class="mnote-knowledge-rag-task-actions"><button type="button" data-mnote-knowledge-rag-task-open>打开</button><button type="button" data-mnote-knowledge-rag-task-retry>重试</button><button type="button" data-mnote-knowledge-rag-task-clear>清除</button><button type="button" data-mnote-knowledge-rag-task-delete>删除</button></div>';
|
||||
@@ -1909,9 +1909,9 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const entryRootUri = String(entry.rootUri || '').trim();
|
||||
if (entryRootUri) knowledgeRagTaskState.rootUri = entryRootUri;
|
||||
const startedAt = Date.now();
|
||||
const pendingJob = knowledgeRagTaskSnapshotFromEntry(entry, 'running', knowledgeRagProvider(), '资料库索引中', startedAt);
|
||||
const pendingJob = knowledgeRagTaskSnapshotFromEntry(entry, 'running', knowledgeRagProvider(), '知识库索引中', startedAt);
|
||||
pendingJob.taskKind = 'knowledge_rag';
|
||||
setKnowledgeRagStatus(entry, 'running', '资料库索引中', pendingJob);
|
||||
setKnowledgeRagStatus(entry, 'running', '知识库索引中', pendingJob);
|
||||
updateKnowledgeRagTaskState(pendingJob);
|
||||
const workspaceId = String(entry.workspaceId || currentWebShellWorkspaceId() || '').trim();
|
||||
const body = {
|
||||
@@ -1945,12 +1945,12 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const job = knowledgeRagJobFromRegistryEntry(registryEntry, entryRootUri) || {
|
||||
...pendingJob,
|
||||
status: payload.retryRequired ? 'running' : 'done',
|
||||
stageLabel: payload.retryRequired ? '知识库忙碌,等待重试' : '资料库已提交索引',
|
||||
stageLabel: payload.retryRequired ? '知识库忙碌,等待重试' : '知识库已提交索引',
|
||||
updatedAtMs: Date.now(),
|
||||
finishedAtMs: payload.retryRequired ? null : Date.now(),
|
||||
};
|
||||
const displayStatus = knowledgeRagDisplayStatus(job, 'done');
|
||||
setKnowledgeRagStatus(entry, displayStatus, displayStatus === 'stale' ? '资料库需更新' : statusTextForKnowledgeRagTask(job), job);
|
||||
setKnowledgeRagStatus(entry, displayStatus, displayStatus === 'stale' ? '知识库需更新' : statusTextForKnowledgeRagTask(job), job);
|
||||
if (job) updateKnowledgeRagTaskState(job);
|
||||
window.dispatchEvent(new CustomEvent('mnote:knowledge-rag-source-updated', {
|
||||
detail: { rootUri: entryRootUri, workspaceId, sourceRootRelativePath: String(entry.path || '').trim(), result: payload }
|
||||
@@ -2003,6 +2003,25 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const workspaceId = currentWebShellWorkspaceId();
|
||||
const seen = new Set();
|
||||
const candidates = [];
|
||||
const rawDocumentId = String(documentId || '').trim();
|
||||
const markdownPath = rawDocumentId.startsWith('local-md:') ? localMarkdownRelativePathFromDocumentId(rawDocumentId) : '';
|
||||
if (markdownPath) {
|
||||
seen.add(markdownPath);
|
||||
const title = markdownPath.split('/').filter(Boolean).pop() || markdownPath;
|
||||
candidates.push({
|
||||
sourceKind: 'local_folder',
|
||||
rootUri,
|
||||
path: markdownPath,
|
||||
kind: 'markdown',
|
||||
title,
|
||||
fileName: title,
|
||||
documentId,
|
||||
ownerDocumentId: documentId,
|
||||
workspaceId,
|
||||
objectIdentity: `local-md:${markdownPath}`,
|
||||
assetId: `local-md:${markdownPath}`,
|
||||
});
|
||||
}
|
||||
document.querySelectorAll(`.document-pane[data-pane-role="${role}"] .ProseMirror img`).forEach((image) => {
|
||||
if (!(image instanceof HTMLImageElement)) return;
|
||||
const sourcePath = localFileOpenPathFromTiptapHref(image.getAttribute('src') || image.src || '');
|
||||
@@ -2146,13 +2165,13 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
});
|
||||
});
|
||||
}
|
||||
setKnowledgeRagStatus(entry, 'idle', '资料库未索引', null);
|
||||
setKnowledgeRagStatus(entry, 'idle', '知识库未索引', null);
|
||||
ensureKnowledgeRagTaskDock();
|
||||
void loadKnowledgeRagTasks(entry.rootUri).catch(() => undefined);
|
||||
void readKnowledgeRagStatus(entry).then((job) => {
|
||||
if (!job) return;
|
||||
const displayStatus = knowledgeRagDisplayStatus(job, 'done');
|
||||
setKnowledgeRagStatus(entry, displayStatus, displayStatus === 'stale' ? '资料库需更新' : statusTextForKnowledgeRagTask(job), job);
|
||||
setKnowledgeRagStatus(entry, displayStatus, displayStatus === 'stale' ? '知识库需更新' : statusTextForKnowledgeRagTask(job), job);
|
||||
updateKnowledgeRagTaskState(job);
|
||||
}).catch(() => undefined);
|
||||
};
|
||||
|
||||
@@ -151,6 +151,10 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
relativePath: String(item.relativePath || item.relative_path || item.path || '').trim(),
|
||||
documentId: String(item.documentId || item.document_id || '').trim(),
|
||||
eventKind: String(item.eventKind || item.event_kind || item.changeType || item.change_type || '').trim(),
|
||||
selfWriteEcho: Boolean(item.selfWriteEcho || item.self_write_echo),
|
||||
fileVersion: String(item.fileVersion || item.file_version || item.observedFileVersion || item.observed_file_version || '').trim(),
|
||||
observedFileVersion: String(item.observedFileVersion || item.observed_file_version || '').trim(),
|
||||
bufferFileVersion: String(item.bufferFileVersion || item.buffer_file_version || '').trim(),
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}
|
||||
@@ -1377,11 +1381,17 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
const relativePath = typeof item.relativePath === 'string' ? item.relativePath.trim() : '';
|
||||
if (targetSession.sessionKind === 'resource') {
|
||||
if (!relativePath || relativePath !== resourceWatchPath(targetSession)) return;
|
||||
if (targetSession.saving) {
|
||||
if (item.selfWriteEcho === true || targetSession.saving && shouldSuppressLocalFolderSelfChange(targetSession.documentId, String(item.eventKind || ''))) {
|
||||
targetSession.externalChangePending = false;
|
||||
targetSession.lastSelfSaveSignalAt = Date.now();
|
||||
return;
|
||||
}
|
||||
if (targetSession.hasExternalConflict || sessionHasRecentLocalInput(targetSession) || targetSession.dirty || targetSession.saveTimer || targetSession.saving) {
|
||||
targetSession.lastExternalChangeSignalAt = Date.now();
|
||||
targetSession.externalChangePending = true;
|
||||
markSessionExternalConflict(targetSession, '当前本地资源文件已在外部更新,请刷新或保存前先处理冲突');
|
||||
return;
|
||||
}
|
||||
targetSession.lastExternalChangeSignalAt = Date.now();
|
||||
targetSession.externalChangePending = true;
|
||||
scheduleSessionExternalRefresh(targetSession, 'mnote-web-local-resource-watch');
|
||||
@@ -1394,14 +1404,30 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
|
||||
|| (!documentId && relativePath && relativeTarget && relativePath === relativeTarget)
|
||||
);
|
||||
if (!targetsCurrentDocument) return;
|
||||
if (item.selfWriteEcho === true) {
|
||||
targetSession.externalChangePending = false;
|
||||
targetSession.lastSelfSaveSignalAt = Date.now();
|
||||
if (item.fileVersion) {
|
||||
targetSession.fileVersion = item.fileVersion;
|
||||
targetSession.conflictDetectionKey = item.fileVersion;
|
||||
targetSession.lastExternalConflictDetectionKey = item.fileVersion;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (shouldSuppressLocalFolderSelfChange(targetSession.documentId, eventKind)) {
|
||||
targetSession.externalChangePending = false;
|
||||
targetSession.lastSelfSaveSignalAt = Date.now();
|
||||
return;
|
||||
}
|
||||
if (targetSession.saving) {
|
||||
targetSession.externalChangePending = false;
|
||||
targetSession.lastSelfSaveSignalAt = Date.now();
|
||||
if (targetSession.hasExternalConflict || sessionHasRecentLocalInput(targetSession) || targetSession.dirty || targetSession.saveTimer || targetSession.saving) {
|
||||
targetSession.lastExternalChangeSignalAt = Date.now();
|
||||
targetSession.externalChangePending = true;
|
||||
markSessionExternalConflict(targetSession, conflictMessageFromEnvelope(targetSession, {
|
||||
message: externalConflictMessage,
|
||||
dirtyState: targetSession.dirty ? 'Dirty' : (targetSession.saving ? 'Saving' : 'Clean'),
|
||||
fileVersion: item.fileVersion || null,
|
||||
externalActor: 'external-editor',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
targetSession.lastExternalChangeSignalAt = Date.now();
|
||||
|
||||
@@ -7,8 +7,16 @@
|
||||
|
||||
var connections = new Map();
|
||||
var sidebarRefreshQueues = new Map();
|
||||
var fileChangeQueues = new Map();
|
||||
var lastSource = '';
|
||||
var lastReason = '';
|
||||
var lastFileChangeDiagnostics = {
|
||||
source: '',
|
||||
changedCount: 0,
|
||||
droppedCount: 0,
|
||||
lastReaction: '',
|
||||
lastBatchSchema: ''
|
||||
};
|
||||
|
||||
function root() {
|
||||
return document.documentElement;
|
||||
@@ -21,6 +29,11 @@
|
||||
root().setAttribute('data-mnote-local-folder-event-bus-connections', String(connections.size));
|
||||
root().setAttribute('data-mnote-local-folder-event-bus-last-source', lastSource);
|
||||
root().setAttribute('data-mnote-local-folder-event-bus-last-reason', lastReason);
|
||||
root().setAttribute('data-mnote-file-change-service', 'ready');
|
||||
root().setAttribute('data-mnote-file-change-service-last-source', lastFileChangeDiagnostics.source || lastSource);
|
||||
root().setAttribute('data-mnote-file-change-service-last-count', String(lastFileChangeDiagnostics.changedCount || 0));
|
||||
root().setAttribute('data-mnote-file-change-service-dropped-count', String(lastFileChangeDiagnostics.droppedCount || 0));
|
||||
root().setAttribute('data-mnote-file-change-service-last-reaction', lastFileChangeDiagnostics.lastReaction || '');
|
||||
}
|
||||
|
||||
function normalizeRootUri(rootUri) {
|
||||
@@ -53,6 +66,375 @@
|
||||
return [String(value)].filter(Boolean);
|
||||
}
|
||||
|
||||
function safeNow() {
|
||||
return Date.now ? Date.now() : new Date().getTime();
|
||||
}
|
||||
|
||||
function decodeFileUriPath(rootUri) {
|
||||
var value = String(rootUri || '').trim();
|
||||
if (!value) return '';
|
||||
try {
|
||||
var url = new URL(value);
|
||||
if (url.protocol !== 'file:') return '';
|
||||
return decodeURIComponent(url.pathname || '').replace(/\/+$/, '');
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSlashes(value) {
|
||||
return String(value || '').trim().replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
function rootRelativePath(rawPath, rootUri) {
|
||||
var value = normalizeSlashes(rawPath).replace(/^file:\/\//, '');
|
||||
if (!value) return '';
|
||||
if (/^[A-Za-z]:\//.test(value) || value.charAt(0) === '/') {
|
||||
var rootPath = normalizeSlashes(decodeFileUriPath(rootUri));
|
||||
if (rootPath && (value === rootPath || value.indexOf(rootPath + '/') === 0)) {
|
||||
return value.slice(rootPath.length).replace(/^\/+/, '');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
return value.replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
function localMarkdownDocumentId(relativePath) {
|
||||
var normalized = normalizeSlashes(relativePath).replace(/^\/+/, '');
|
||||
if (!/\.md(?:own)?$/i.test(normalized)) return '';
|
||||
return 'local-md:' + normalized.replace(/\//g, '~2F');
|
||||
}
|
||||
|
||||
function normalizeChangeType(item, fallback) {
|
||||
var raw = String(
|
||||
(item && (item.changeType || item.change_type || item.eventKind || item.event_kind || item.kind))
|
||||
|| fallback
|
||||
|| 'modified'
|
||||
).trim();
|
||||
if (/remove|delete/i.test(raw)) return 'deleted';
|
||||
if (/create/i.test(raw)) return 'created';
|
||||
if (/rename|name/i.test(raw)) return 'renamed';
|
||||
return 'modified';
|
||||
}
|
||||
|
||||
function resourceKindForPath(relativePath) {
|
||||
return /\.md(?:own)?$/i.test(String(relativePath || '')) ? 'markdown' : 'resource';
|
||||
}
|
||||
|
||||
function normalizeFileChangeItem(item, context) {
|
||||
var source = String((context && context.source) || (item && item.source) || 'watcher').trim() || 'watcher';
|
||||
var rootUri = normalizeRootUri((context && context.rootUri) || (item && item.rootUri));
|
||||
var workspaceId = normalizeWorkspaceId((context && context.workspaceId) || (item && item.workspaceId));
|
||||
var rawPath = typeof item === 'string'
|
||||
? item
|
||||
: String(item && (
|
||||
item.relativePath
|
||||
|| item.relative_path
|
||||
|| item.path
|
||||
|| item.sourcePath
|
||||
|| item.source_path
|
||||
|| item.filePath
|
||||
|| item.file_path
|
||||
) || '');
|
||||
var relativePath = rootRelativePath(rawPath, rootUri);
|
||||
if (!relativePath) {
|
||||
return { dropped: true, reason: 'outside_root_or_empty_path', rawPath: rawPath };
|
||||
}
|
||||
var eventKind = String(item && (item.eventKind || item.event_kind || item.kind || item.changeType || item.change_type) || '').trim();
|
||||
var documentId = String(item && (item.documentId || item.document_id) || '').trim() || localMarkdownDocumentId(relativePath);
|
||||
var observedFileVersion = String(item && (item.observedFileVersion || item.observed_file_version || '') || '').trim();
|
||||
var bufferFileVersion = String(item && (item.bufferFileVersion || item.buffer_file_version || '') || '').trim();
|
||||
var fileVersion = String(item && (item.fileVersion || item.file_version || '') || '').trim()
|
||||
|| observedFileVersion
|
||||
|| bufferFileVersion
|
||||
|| String(item && item.revision || (context && context.revision) || '').trim();
|
||||
return {
|
||||
schema: 'mnote.file_change.v1',
|
||||
rootUri: rootUri,
|
||||
workspaceId: workspaceId,
|
||||
relativePath: relativePath,
|
||||
documentId: documentId,
|
||||
resourceKind: String(item && (item.resourceKind || item.resource_kind) || resourceKindForPath(relativePath)),
|
||||
changeType: normalizeChangeType(item, eventKind),
|
||||
eventKind: eventKind,
|
||||
fileVersion: fileVersion,
|
||||
source: source,
|
||||
selfWriteEcho: Boolean(item && (item.selfWriteEcho || item.self_write_echo)),
|
||||
observedFileVersion: observedFileVersion,
|
||||
bufferFileVersion: bufferFileVersion,
|
||||
lastWriteIntentId: String(item && (item.lastWriteIntentId || item.last_write_intent_id || '') || '').trim(),
|
||||
lastSaveOperationId: String(item && (item.lastSaveOperationId || item.last_save_operation_id || '') || '').trim(),
|
||||
createdAt: Number(item && (item.createdAt || item.created_at) || safeNow())
|
||||
};
|
||||
}
|
||||
|
||||
function standardBatchToCompatPayload(batch) {
|
||||
return {
|
||||
schema: 'mnote.local_folder.watch_batch.v1',
|
||||
kind: 'watch_batch',
|
||||
sourceKind: 'local_folder',
|
||||
source: batch.source,
|
||||
rootUri: batch.rootUri,
|
||||
workspaceId: batch.workspaceId,
|
||||
revision: batch.revision,
|
||||
changedPaths: batch.changes.map(function(change) {
|
||||
return {
|
||||
relativePath: change.relativePath,
|
||||
documentId: change.documentId,
|
||||
kind: change.eventKind || change.changeType,
|
||||
eventKind: change.eventKind || change.changeType,
|
||||
changeType: change.changeType,
|
||||
revision: change.fileVersion || batch.revision,
|
||||
fileVersion: change.fileVersion,
|
||||
observedFileVersion: change.observedFileVersion,
|
||||
bufferFileVersion: change.bufferFileVersion,
|
||||
lastWriteIntentId: change.lastWriteIntentId,
|
||||
lastSaveOperationId: change.lastSaveOperationId,
|
||||
selfWriteEcho: change.selfWriteEcho,
|
||||
source: change.source
|
||||
};
|
||||
}),
|
||||
affectedParents: batch.affectedParents,
|
||||
eventKinds: Array.from(new Set(batch.changes.map(function(change) {
|
||||
return change.eventKind || change.changeType || '';
|
||||
}).filter(Boolean))),
|
||||
fallbackResync: batch.resyncRequired === true
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeFileChangeBatch(entry, event, payload, meta) {
|
||||
var revision = revisionOf(payload, event);
|
||||
var source = String((meta && meta.source) || payload.source || payload.sourceKind || 'watcher_sse').trim() || 'watcher_sse';
|
||||
var reason = String((meta && meta.reason) || payload.reason || 'watch_batch').trim() || 'watch_batch';
|
||||
var rootUri = normalizeRootUri((entry && entry.rootUri) || payload.rootUri);
|
||||
var workspaceId = normalizeWorkspaceId((entry && entry.workspaceId) || payload.workspaceId);
|
||||
var raw = []
|
||||
.concat(Array.isArray(payload.changedPaths) ? payload.changedPaths : [])
|
||||
.concat(Array.isArray(payload.changed_paths) ? payload.changed_paths : [])
|
||||
.concat(Array.isArray(payload.paths) ? payload.paths : [])
|
||||
.concat(Array.isArray(payload.events) ? payload.events : []);
|
||||
if (!raw.length && payload.relativePath) raw.push(payload);
|
||||
var dropped = [];
|
||||
var changes = [];
|
||||
raw.forEach(function(item) {
|
||||
var normalized = normalizeFileChangeItem(item, {
|
||||
source: source,
|
||||
rootUri: rootUri,
|
||||
workspaceId: workspaceId,
|
||||
revision: revision
|
||||
});
|
||||
if (normalized && normalized.dropped) {
|
||||
dropped.push(normalized);
|
||||
} else if (normalized) {
|
||||
changes.push(normalized);
|
||||
}
|
||||
});
|
||||
var affectedParents = collectAffectedParents(payload).map(function(parent) {
|
||||
return { relativePath: parent, reason: reason };
|
||||
});
|
||||
changes.forEach(function(change) {
|
||||
var parent = change.relativePath.indexOf('/') >= 0 ? change.relativePath.split('/').slice(0, -1).join('/') : '';
|
||||
if (!affectedParents.some(function(item) { return item.relativePath === parent; })) {
|
||||
affectedParents.push({ relativePath: parent, reason: reason });
|
||||
}
|
||||
});
|
||||
return {
|
||||
schema: 'mnote.file_change_batch.v1',
|
||||
rootUri: rootUri,
|
||||
workspaceId: workspaceId,
|
||||
source: source,
|
||||
reason: reason,
|
||||
changes: changes,
|
||||
affectedParents: affectedParents,
|
||||
revision: revision,
|
||||
dropped: dropped,
|
||||
resyncRequired: payload.fallbackResync === true
|
||||
|| payload.requiresResync === true
|
||||
|| payload.resyncRequired === true
|
||||
|| payload.resync_required === true,
|
||||
rawPayload: payload,
|
||||
bootstrap: entry && entry.bootstrap || null
|
||||
};
|
||||
}
|
||||
|
||||
function mergeFileChange(existing, change) {
|
||||
if (!existing) return change;
|
||||
return Object.assign({}, existing, change, {
|
||||
source: change.source || existing.source,
|
||||
selfWriteEcho: change.selfWriteEcho === true ? true : (change.selfWriteEcho === false ? false : existing.selfWriteEcho),
|
||||
observedFileVersion: change.observedFileVersion || existing.observedFileVersion,
|
||||
bufferFileVersion: change.bufferFileVersion || existing.bufferFileVersion,
|
||||
fileVersion: change.fileVersion || existing.fileVersion,
|
||||
createdAt: Math.max(Number(existing.createdAt || 0), Number(change.createdAt || 0))
|
||||
});
|
||||
}
|
||||
|
||||
function fileChangeQueueKey(batch) {
|
||||
return [batch.rootUri || '', batch.workspaceId || 'default'].join('#');
|
||||
}
|
||||
|
||||
function enqueueFileChangeBatch(batch) {
|
||||
if (!batch || !batch.rootUri) return;
|
||||
var key = fileChangeQueueKey(batch);
|
||||
var queue = fileChangeQueues.get(key);
|
||||
if (!queue) {
|
||||
queue = {
|
||||
rootUri: batch.rootUri,
|
||||
workspaceId: batch.workspaceId,
|
||||
sources: new Set(),
|
||||
reasons: new Set(),
|
||||
revisions: new Set(),
|
||||
changes: new Map(),
|
||||
affectedParents: new Map(),
|
||||
dropped: [],
|
||||
resyncRequired: false,
|
||||
bootstrap: batch.bootstrap || null,
|
||||
timer: 0
|
||||
};
|
||||
fileChangeQueues.set(key, queue);
|
||||
}
|
||||
if (batch.source) queue.sources.add(batch.source);
|
||||
if (batch.reason) queue.reasons.add(batch.reason);
|
||||
if (batch.revision) queue.revisions.add(String(batch.revision));
|
||||
queue.dropped = queue.dropped.concat(batch.dropped || []);
|
||||
queue.resyncRequired = queue.resyncRequired || batch.resyncRequired === true;
|
||||
(batch.changes || []).forEach(function(change) {
|
||||
var changeKey = [change.rootUri || batch.rootUri, change.relativePath, change.fileVersion || ''].join('#');
|
||||
queue.changes.set(changeKey, mergeFileChange(queue.changes.get(changeKey), change));
|
||||
});
|
||||
(batch.affectedParents || []).forEach(function(parent) {
|
||||
var relativePath = String(parent && parent.relativePath || parent || '');
|
||||
queue.affectedParents.set(relativePath, {
|
||||
relativePath: relativePath,
|
||||
reason: String(parent && parent.reason || batch.reason || 'file-change')
|
||||
});
|
||||
});
|
||||
if (!queue.timer) {
|
||||
queue.timer = window.setTimeout(function() {
|
||||
flushFileChangeQueue(key);
|
||||
}, 80);
|
||||
}
|
||||
}
|
||||
|
||||
function currentDocumentSessionState(change) {
|
||||
var snapshot = null;
|
||||
try {
|
||||
snapshot = window.__mnoteDebugDocumentSessions
|
||||
&& window.__mnoteDebugDocumentSessions.snapshot
|
||||
&& window.__mnoteDebugDocumentSessions.snapshot();
|
||||
} catch (_) {
|
||||
snapshot = null;
|
||||
}
|
||||
root().setAttribute('data-mnote-file-change-service-session-api', snapshot ? 'available' : 'unavailable');
|
||||
var sessions = Array.isArray(snapshot && snapshot.sessions) ? snapshot.sessions : [];
|
||||
return sessions.find(function(session) {
|
||||
if (!session || session.sourceKind !== 'local_folder') return false;
|
||||
if (change.rootUri && session.rootUri && change.rootUri !== session.rootUri) return false;
|
||||
if (change.documentId && session.documentId === change.documentId) return true;
|
||||
return false;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function reactionTypeForChange(change) {
|
||||
if (change.selfWriteEcho) return 'ignore_self_write_echo';
|
||||
if (change.resourceKind !== 'markdown') return 'refresh_resource_tab';
|
||||
var session = currentDocumentSessionState(change);
|
||||
if (session && /dirty|external/i.test(String(session.dirtyState || session.status || ''))) {
|
||||
return 'external_conflict_current_document';
|
||||
}
|
||||
if (session) return 'refresh_current_document';
|
||||
if (change.changeType === 'created' || change.changeType === 'deleted' || change.changeType === 'renamed') {
|
||||
return 'refresh_page_tree_projection';
|
||||
}
|
||||
return 'refresh_filetree_parents';
|
||||
}
|
||||
|
||||
function emitFileChangeReactions(detail) {
|
||||
var reactionTypes = new Set();
|
||||
(detail.fileChangeBatch.changes || []).forEach(function(change) {
|
||||
var type = reactionTypeForChange(change);
|
||||
reactionTypes.add(type);
|
||||
emit('mnote:file-change-reaction', {
|
||||
schema: 'mnote.file_change_reaction.v1',
|
||||
type: type,
|
||||
rootUri: detail.rootUri,
|
||||
workspaceId: detail.workspaceId,
|
||||
source: detail.source,
|
||||
change: change,
|
||||
batch: detail.fileChangeBatch
|
||||
});
|
||||
});
|
||||
if (detail.affectedParents && detail.affectedParents.length) {
|
||||
reactionTypes.add('refresh_filetree_parents');
|
||||
}
|
||||
lastFileChangeDiagnostics.lastReaction = Array.from(reactionTypes).join(',') || '';
|
||||
root().setAttribute('data-mnote-file-change-service-last-reaction', lastFileChangeDiagnostics.lastReaction);
|
||||
return reactionTypes;
|
||||
}
|
||||
|
||||
function flushFileChangeQueue(key) {
|
||||
var queue = fileChangeQueues.get(key);
|
||||
if (!queue) return;
|
||||
fileChangeQueues.delete(key);
|
||||
var sources = Array.from(queue.sources);
|
||||
var reasons = Array.from(queue.reasons);
|
||||
var revision = Array.from(queue.revisions).pop() || null;
|
||||
var batch = {
|
||||
schema: 'mnote.file_change_batch.v1',
|
||||
rootUri: queue.rootUri,
|
||||
workspaceId: queue.workspaceId,
|
||||
source: sources.join(',') || 'watcher',
|
||||
reason: reasons.join(',') || 'watch_batch',
|
||||
changes: Array.from(queue.changes.values()),
|
||||
affectedParents: Array.from(queue.affectedParents.values()),
|
||||
revision: revision,
|
||||
dropped: queue.dropped,
|
||||
resyncRequired: queue.resyncRequired
|
||||
};
|
||||
var compatPayload = standardBatchToCompatPayload(batch);
|
||||
var detail = {
|
||||
schema: 'mnote.local_folder.event_bus.watch_batch.v1',
|
||||
source: batch.source,
|
||||
reason: batch.reason,
|
||||
rootUri: batch.rootUri,
|
||||
workspaceId: batch.workspaceId,
|
||||
revision: batch.revision,
|
||||
payload: compatPayload,
|
||||
bootstrap: queue.bootstrap || null,
|
||||
changedPaths: compatPayload.changedPaths,
|
||||
affectedParents: batch.affectedParents,
|
||||
fileChangeBatch: batch,
|
||||
resyncRequired: batch.resyncRequired,
|
||||
viaEventBus: true
|
||||
};
|
||||
lastFileChangeDiagnostics = {
|
||||
source: batch.source,
|
||||
changedCount: batch.changes.length,
|
||||
droppedCount: batch.dropped.length,
|
||||
lastReaction: '',
|
||||
lastBatchSchema: batch.schema
|
||||
};
|
||||
setDiagnostics(batch.source, batch.reason);
|
||||
root().setAttribute('data-mnote-tree-live-revision', String(batch.revision || ''));
|
||||
root().setAttribute('data-mnote-file-change-service-last-schema', batch.schema);
|
||||
emit('mnote:file-change-batch', batch);
|
||||
emit('mnote:local-folder:watch-batch', detail);
|
||||
emit('tree:local-folder-watch-batch', detail);
|
||||
var reactionTypes = emitFileChangeReactions(detail);
|
||||
if (batch.affectedParents.length > 0 || reactionTypes.has('refresh_filetree_parents')) {
|
||||
emit('mnote:local-folder:filetree-parent-changed', detail);
|
||||
}
|
||||
if (batch.changes.length > 0) {
|
||||
emit('mnote:local-folder:document-changed', detail);
|
||||
emit('mnote:local-folder:resource-changed', detail);
|
||||
emit('mnote:local-folder:knowledge-rag-source-updated', detail);
|
||||
}
|
||||
queueSidebarRefresh(detail);
|
||||
if (batch.resyncRequired) {
|
||||
emit('mnote:local-folder:resync-required', detail);
|
||||
}
|
||||
}
|
||||
|
||||
function pathArrayOf(value) {
|
||||
if (!value) return [];
|
||||
var list = Array.isArray(value) ? value : [value];
|
||||
@@ -191,47 +573,7 @@
|
||||
}
|
||||
|
||||
function dispatchWatchBatch(entry, event, payload, meta) {
|
||||
var revision = revisionOf(payload, event);
|
||||
var changedPaths = collectChangedPaths(payload);
|
||||
var affectedParents = collectAffectedParents(payload);
|
||||
var source = String((meta && meta.source) || payload.source || 'watcher_sse').trim() || 'watcher_sse';
|
||||
var reason = String((meta && meta.reason) || payload.reason || 'watch_batch').trim() || 'watch_batch';
|
||||
var resyncRequired = payload.fallbackResync === true
|
||||
|| payload.requiresResync === true
|
||||
|| payload.resyncRequired === true
|
||||
|| payload.resync_required === true;
|
||||
var detail = {
|
||||
schema: 'mnote.local_folder.event_bus.watch_batch.v1',
|
||||
source: source,
|
||||
reason: reason,
|
||||
rootUri: entry.rootUri,
|
||||
workspaceId: entry.workspaceId,
|
||||
revision: revision,
|
||||
payload: payload,
|
||||
bootstrap: entry.bootstrap || null,
|
||||
changedPaths: collectChangedPathItems(payload, reason),
|
||||
affectedParents: pathItems(affectedParents, reason),
|
||||
resyncRequired: resyncRequired,
|
||||
viaEventBus: true
|
||||
};
|
||||
|
||||
setDiagnostics(source, reason);
|
||||
root().setAttribute('data-mnote-tree-live-revision', String(revision || ''));
|
||||
emit('mnote:local-folder:watch-batch', detail);
|
||||
emit('tree:local-folder-watch-batch', detail);
|
||||
|
||||
if (affectedParents.length > 0) {
|
||||
emit('mnote:local-folder:filetree-parent-changed', detail);
|
||||
}
|
||||
if (changedPaths.length > 0) {
|
||||
emit('mnote:local-folder:document-changed', detail);
|
||||
emit('mnote:local-folder:resource-changed', detail);
|
||||
emit('mnote:local-folder:knowledge-rag-source-updated', detail);
|
||||
}
|
||||
queueSidebarRefresh(detail);
|
||||
if (resyncRequired) {
|
||||
emit('mnote:local-folder:resync-required', detail);
|
||||
}
|
||||
enqueueFileChangeBatch(normalizeFileChangeBatch(entry, event, payload, meta));
|
||||
}
|
||||
|
||||
function closeEntry(entry) {
|
||||
@@ -326,6 +668,32 @@
|
||||
});
|
||||
}
|
||||
|
||||
function emitChangedFiles(detail) {
|
||||
var payload = detail && detail.payload && typeof detail.payload === 'object' ? detail.payload : {};
|
||||
var changedFiles = []
|
||||
.concat(Array.isArray(detail && detail.changedFiles) ? detail.changedFiles : [])
|
||||
.concat(Array.isArray(payload.changedFiles) ? payload.changedFiles : [])
|
||||
.concat(Array.isArray(payload.changed_files) ? payload.changed_files : []);
|
||||
if (!changedFiles.length) return;
|
||||
var rootUri = normalizeRootUri((detail && detail.rootUri) || payload.rootUri);
|
||||
var workspaceId = normalizeWorkspaceId((detail && detail.workspaceId) || payload.workspaceId);
|
||||
emitSyntheticWatchBatch({
|
||||
source: (detail && detail.source) || payload.source || 'changed_files_adapter',
|
||||
reason: (detail && detail.reason) || payload.reason || 'changed_files_adapter',
|
||||
rootUri: rootUri,
|
||||
workspaceId: workspaceId,
|
||||
payload: {
|
||||
schema: 'mnote.local_folder.watch_batch.v1',
|
||||
source: (detail && detail.source) || payload.source || 'changed_files_adapter',
|
||||
rootUri: rootUri,
|
||||
workspaceId: workspaceId,
|
||||
revision: (detail && detail.revision) || payload.revision || null,
|
||||
changedPaths: changedFiles,
|
||||
affectedParents: Array.isArray(detail && detail.affectedParents) ? detail.affectedParents : []
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeAll() {
|
||||
Array.from(connections.values()).forEach(function(entry) {
|
||||
closeEntry(entry);
|
||||
@@ -337,6 +705,7 @@
|
||||
window.__mnoteLocalFolderEventBus = {
|
||||
startLocalFolderWatcher: startLocalFolderWatcher,
|
||||
emitSyntheticWatchBatch: emitSyntheticWatchBatch,
|
||||
emitChangedFiles: emitChangedFiles,
|
||||
closeAll: closeAll,
|
||||
flushSidebarRefresh: function(rootUri) {
|
||||
flushSidebarRefresh(normalizeRootUri(rootUri));
|
||||
@@ -346,7 +715,8 @@
|
||||
return {
|
||||
connections: connections.size,
|
||||
lastSource: lastSource,
|
||||
lastReason: lastReason
|
||||
lastReason: lastReason,
|
||||
fileChange: Object.assign({}, lastFileChangeDiagnostics)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -415,7 +415,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
||||
|| String(detail && (detail.localRelativePath || detail.path || detail.title) || '').trim();
|
||||
var rootUri = knowledgeRagRootUri(detail, trigger);
|
||||
if (!sourceRootRelativePath || !rootUri) {
|
||||
throw new Error('缺少资料库来源或 rootUri');
|
||||
throw new Error('缺少知识库来源或 rootUri');
|
||||
}
|
||||
var workspaceId = String(detail && detail.workspaceId || resolveWorkspaceId(document.body) || '').trim();
|
||||
document.documentElement.setAttribute('data-mnote-knowledge-rag-menu-status', 'running');
|
||||
@@ -426,12 +426,14 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
||||
body: JSON.stringify({
|
||||
workspaceId: workspaceId,
|
||||
rootUri: rootUri,
|
||||
sources: [{ sourcePath: sourceRootRelativePath }]
|
||||
sources: [{ sourcePath: sourceRootRelativePath }],
|
||||
knowledgeBaseId: String(detail && detail.kbId || '').trim() || undefined,
|
||||
providerKnowledgeBaseId: String(detail && (detail.providerKbId || detail.kbId) || '').trim() || undefined
|
||||
})
|
||||
});
|
||||
var payload = await response.json().catch(function() { return null; });
|
||||
if (!response.ok || !payload || payload.ok === false) {
|
||||
var message = payload && payload.message ? payload.message : '资料库索引失败';
|
||||
var message = payload && payload.message ? payload.message : '知识库索引失败';
|
||||
document.documentElement.setAttribute('data-mnote-knowledge-rag-menu-status', 'failed');
|
||||
throw new Error(message);
|
||||
}
|
||||
@@ -450,12 +452,30 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
||||
recordFileTreeActionStatus('pending', detail);
|
||||
void ingestKnowledgeRagForDetail(detail, trigger).then(function() {
|
||||
recordFileTreeActionStatus('done', detail);
|
||||
if (window.mnote && window.mnote.toast) {
|
||||
window.mnote.toast('已加入知识库', { kind: 'success' });
|
||||
}
|
||||
}).catch(function(error) {
|
||||
var msg = error && error.message ? error.message : '知识库索引失败';
|
||||
recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' }));
|
||||
window.alert(error && error.message ? error.message : '资料库索引失败');
|
||||
if (window.mnote && window.mnote.toast) {
|
||||
window.mnote.toast(msg, { kind: 'error' });
|
||||
} else {
|
||||
window.alert(msg);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (action === 'open-knowledge-page') {
|
||||
var knowledgeUrl = '/knowledge';
|
||||
if (typeof window.open === 'function') {
|
||||
window.open(knowledgeUrl, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
window.location.href = knowledgeUrl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (detail.contextKind === 'attachment') {
|
||||
if (action === 'copy-link') {
|
||||
void copyTreeContextValue(detail.href || '', 'attachment-copy-link');
|
||||
@@ -683,6 +703,148 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
||||
menu.appendChild(sep);
|
||||
return;
|
||||
}
|
||||
// Submenu root: render parent button with child flyout
|
||||
if (item.children && Array.isArray(item.children) && item.children.length > 0) {
|
||||
if (typeof injectKnowledgeSubmenuStyles === 'function') injectKnowledgeSubmenuStyles();
|
||||
var rootBtn = document.createElement('button');
|
||||
rootBtn.type = 'button';
|
||||
rootBtn.className = 'mnote-tree-context-menu__item mnote-tree-context-menu__item--submenu-root';
|
||||
rootBtn.setAttribute('role', 'menuitem');
|
||||
rootBtn.setAttribute('data-action', item.action || 'knowledge-rag-root');
|
||||
rootBtn.setAttribute('data-command-id', item.commandId || item.action || 'knowledge-rag-root');
|
||||
if (item.when) rootBtn.setAttribute('data-command-when', item.when);
|
||||
if (item.danger || item.destructive) rootBtn.setAttribute('data-destructive', 'true');
|
||||
if (item.requiresApproval) rootBtn.setAttribute('data-requires-approval', 'true');
|
||||
rootBtn.disabled = item.disabled === true;
|
||||
if (item.title) rootBtn.title = item.title;
|
||||
if (item.disabled === true && item.title) rootBtn.setAttribute('data-disabled-reason', item.title);
|
||||
var rootIcon = document.createElement('span');
|
||||
rootIcon.className = 'material-symbols-outlined mnote-tree-context-menu__icon';
|
||||
rootIcon.setAttribute('aria-hidden', 'true');
|
||||
rootIcon.setAttribute('data-icon', item.icon || 'radio_button_unchecked');
|
||||
var rootLabel = document.createElement('span');
|
||||
rootLabel.className = 'mnote-tree-context-menu__label';
|
||||
rootLabel.textContent = item.label;
|
||||
var arrow = document.createElement('span');
|
||||
arrow.className = 'mnote-tree-context-menu__submenu-arrow';
|
||||
arrow.textContent = '▶';
|
||||
arrow.setAttribute('aria-hidden', 'true');
|
||||
rootBtn.appendChild(rootIcon);
|
||||
rootBtn.appendChild(rootLabel);
|
||||
rootBtn.appendChild(arrow);
|
||||
var submenu = document.createElement('div');
|
||||
submenu.className = 'mnote-tree-context-menu__submenu';
|
||||
submenu.setAttribute('data-testid', 'mnote-knowledge-submenu');
|
||||
submenu.setAttribute('role', 'menu');
|
||||
function renderSubmenuChildren(children) {
|
||||
submenu.textContent = '';
|
||||
children.forEach(function(child) {
|
||||
if (child.separator) {
|
||||
var childSep = document.createElement('div');
|
||||
childSep.className = 'mnote-tree-context-menu__separator';
|
||||
childSep.setAttribute('role', 'separator');
|
||||
submenu.appendChild(childSep);
|
||||
return;
|
||||
}
|
||||
var childBtn = document.createElement('button');
|
||||
childBtn.type = 'button';
|
||||
childBtn.className = child.danger ? 'mnote-tree-context-menu__item mnote-tree-context-menu__item--danger' : 'mnote-tree-context-menu__item';
|
||||
if (child.heading) childBtn.className += ' mnote-tree-context-menu__item--heading';
|
||||
childBtn.setAttribute('role', 'menuitem');
|
||||
childBtn.setAttribute('data-action', child.action || '');
|
||||
childBtn.setAttribute('data-command-id', child.commandId || child.action || '');
|
||||
if (child.when) childBtn.setAttribute('data-command-when', child.when);
|
||||
childBtn.disabled = child.disabled === true || child.heading === true;
|
||||
if (child.title) childBtn.title = child.title;
|
||||
if (child.disabled === true && child.title) childBtn.setAttribute('data-disabled-reason', child.title);
|
||||
var childIcon = document.createElement('span');
|
||||
childIcon.className = 'material-symbols-outlined mnote-tree-context-menu__icon';
|
||||
childIcon.setAttribute('aria-hidden', 'true');
|
||||
childIcon.setAttribute('data-icon', child.icon || 'radio_button_unchecked');
|
||||
var childLabel = document.createElement('span');
|
||||
childLabel.className = 'mnote-tree-context-menu__label';
|
||||
childLabel.textContent = child.label;
|
||||
childBtn.appendChild(childIcon);
|
||||
childBtn.appendChild(childLabel);
|
||||
if (child.shortcut) {
|
||||
var childShortcut = document.createElement('span');
|
||||
childShortcut.className = 'mnote-tree-context-menu__shortcut';
|
||||
childShortcut.textContent = child.shortcut;
|
||||
childBtn.appendChild(childShortcut);
|
||||
}
|
||||
var childDetail = child.kbId ? Object.assign({}, detail, { kbId: child.kbId, providerKbId: child.providerKbId || child.kbId }) : detail;
|
||||
(function(act, actDetail, actTrigger) {
|
||||
childBtn.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!act || childBtn.disabled) return;
|
||||
handleTreeContextMenuAction(act, actDetail, actTrigger);
|
||||
});
|
||||
})(child.action, childDetail, trigger);
|
||||
submenu.appendChild(childBtn);
|
||||
});
|
||||
}
|
||||
renderSubmenuChildren(item.children);
|
||||
rootBtn.appendChild(submenu);
|
||||
var submenuShown = false;
|
||||
var submenuHideTimer = null;
|
||||
var lastSubmenuPointerY = 0;
|
||||
function knowledgeSubmenuPosition(pointerY) {
|
||||
var btnRect = rootBtn.getBoundingClientRect();
|
||||
submenu.style.display = 'block';
|
||||
submenu.style.visibility = 'hidden';
|
||||
var smRect = submenu.getBoundingClientRect();
|
||||
var leftPos = btnRect.right + 4;
|
||||
var anchorY = Number(pointerY || lastSubmenuPointerY || 0);
|
||||
var topPos = anchorY > 0 ? anchorY - 18 : btnRect.top;
|
||||
if (leftPos + smRect.width > window.innerWidth - 8) {
|
||||
leftPos = Math.max(8, btnRect.left - smRect.width - 4);
|
||||
}
|
||||
if (topPos + smRect.height > window.innerHeight - 8) {
|
||||
topPos = Math.max(8, window.innerHeight - smRect.height - 8);
|
||||
}
|
||||
submenu.style.left = leftPos + 'px';
|
||||
submenu.style.top = topPos + 'px';
|
||||
submenu.style.visibility = 'visible';
|
||||
}
|
||||
function knowledgeSubmenuShow(pointerY) {
|
||||
if (Number(pointerY || 0) > 0) lastSubmenuPointerY = pointerY;
|
||||
if (submenuHideTimer) { window.clearTimeout(submenuHideTimer); submenuHideTimer = null; }
|
||||
if (!submenuShown) { knowledgeSubmenuPosition(pointerY); submenuShown = true; rootBtn.classList.add('mnote-tree-context-menu__item--submenu-open'); }
|
||||
}
|
||||
function knowledgeSubmenuScheduleHide() {
|
||||
if (submenuHideTimer) window.clearTimeout(submenuHideTimer);
|
||||
submenuHideTimer = window.setTimeout(function() {
|
||||
if (submenuShown) { submenu.style.display = 'none'; submenuShown = false; rootBtn.classList.remove('mnote-tree-context-menu__item--submenu-open'); }
|
||||
submenuHideTimer = null;
|
||||
}, 150);
|
||||
}
|
||||
rootBtn.addEventListener('mouseenter', function(event) { if (!rootBtn.disabled) knowledgeSubmenuShow(event.clientY); });
|
||||
rootBtn.addEventListener('mousemove', function(event) {
|
||||
if (rootBtn.disabled || !submenuShown) return;
|
||||
lastSubmenuPointerY = event.clientY;
|
||||
});
|
||||
rootBtn.addEventListener('mouseleave', function(event) {
|
||||
if (event.relatedTarget && (submenu.contains(event.relatedTarget) || rootBtn.contains(event.relatedTarget))) return;
|
||||
knowledgeSubmenuScheduleHide();
|
||||
});
|
||||
submenu.addEventListener('mouseenter', function() {
|
||||
if (submenuHideTimer) { window.clearTimeout(submenuHideTimer); submenuHideTimer = null; }
|
||||
});
|
||||
submenu.addEventListener('mouseleave', function(event) {
|
||||
if (event.relatedTarget && (rootBtn.contains(event.relatedTarget) || submenu.contains(event.relatedTarget))) return;
|
||||
knowledgeSubmenuScheduleHide();
|
||||
});
|
||||
if (typeof item.loadChildren === 'function') {
|
||||
item.loadChildren().then(function(children) {
|
||||
if (!Array.isArray(children) || children.length === 0) return;
|
||||
renderSubmenuChildren(children);
|
||||
if (submenuShown) knowledgeSubmenuPosition(lastSubmenuPointerY);
|
||||
}).catch(function() {});
|
||||
}
|
||||
menu.appendChild(rootBtn);
|
||||
return;
|
||||
}
|
||||
var button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = item.danger ? 'mnote-tree-context-menu__item mnote-tree-context-menu__item--danger' : 'mnote-tree-context-menu__item';
|
||||
@@ -718,6 +880,106 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
||||
menu.appendChild(button);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── 知识库二级菜单 CSS 注入(仅一次) ──
|
||||
var __mnoteKnowledgeSubmenuStylesInjected = false;
|
||||
function injectKnowledgeSubmenuStyles() {
|
||||
if (__mnoteKnowledgeSubmenuStylesInjected) return;
|
||||
__mnoteKnowledgeSubmenuStylesInjected = true;
|
||||
var style = document.createElement('style');
|
||||
style.id = 'mnote-knowledge-submenu-styles';
|
||||
style.textContent = '.mnote-tree-context-menu__item--submenu-root{position:relative}.mnote-tree-context-menu__submenu{position:fixed;z-index:calc(var(--wolai-z-context-menu,9999) + 1);width:240px;min-width:220px;max-width:min(340px,calc(100vw - 16px));padding:6px;border:1px solid rgba(27,28,28,.08);border-radius:6px;background:#FFF;box-shadow:0 8px 20px rgba(27,28,28,.12);backdrop-filter:blur(24px);color:var(--atelier-text);display:none}.mnote-tree-context-menu__submenu-arrow{margin-left:auto;font-size:11px;color:#6D6A65;padding-left:12px}.mnote-tree-context-menu__item--submenu-open{background:#F4F3F3}.mnote-tree-context-menu__item--heading{font-size:12px;color:#6D6A65;background:transparent!important;cursor:default}';
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// ── 知识库列表获取:只读 MNote 后端 registry/status,避免前端直连 WeKnora service key。 ──
|
||||
function fetchKnowledgeBaseList(detail) {
|
||||
var rootUri = knowledgeRagRootUri(detail, null);
|
||||
if (!rootUri) return Promise.resolve([]);
|
||||
var workspaceId = String(detail && detail.workspaceId || resolveWorkspaceId(document.body) || '').trim();
|
||||
var sourcePath = knowledgeRagSourceRelativePath(detail);
|
||||
var statusUrl = '/api/knowledge-rag/status?rootUri=' + encodeURIComponent(rootUri);
|
||||
if (workspaceId) statusUrl += '&workspaceId=' + encodeURIComponent(workspaceId);
|
||||
return fetch(statusUrl, { credentials: 'include', headers: { accept: 'application/json' } })
|
||||
.then(function(response) { return response.ok ? response.json() : null; })
|
||||
.then(function(payload) {
|
||||
// 构建 knowledgeBase → 已索引 sourcePath 快速查找
|
||||
var indexedByKb = {};
|
||||
var registry = payload && payload.registry;
|
||||
if (registry && Array.isArray(registry.entries) && sourcePath) {
|
||||
registry.entries.forEach(function(entry) {
|
||||
var entryKbId = String(entry.providerKnowledgeBaseId || entry.provider_kb_id || '').trim();
|
||||
var entryPath = String(entry.sourceRootRelativePath || entry.source_root_relative_path || '').trim();
|
||||
if (entryKbId && entryPath && entry.indexedAtMs && entryPath === sourcePath) {
|
||||
if (!indexedByKb[entryKbId]) indexedByKb[entryKbId] = {};
|
||||
indexedByKb[entryKbId][entryPath] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
var bases = payload && payload.knowledgeBases && Array.isArray(payload.knowledgeBases.bases)
|
||||
? payload.knowledgeBases.bases
|
||||
: [];
|
||||
return bases.filter(function(base) {
|
||||
return base && base.canWrite !== false && (base.providerKbId || base.provider_kb_id || base.baseId || base.base_id);
|
||||
}).map(function(base) {
|
||||
var providerKbId = String(base.providerKbId || base.provider_kb_id || '').trim();
|
||||
var baseId = String(base.baseId || base.base_id || providerKbId).trim();
|
||||
var kbId = providerKbId || baseId;
|
||||
return {
|
||||
id: baseId,
|
||||
providerKbId: kbId,
|
||||
name: String(base.name || baseId || providerKbId || '未命名知识库').trim(),
|
||||
sourceCount: Number(base.sourceCount || base.source_count || 0) || 0,
|
||||
sourceAlreadyIndexed: Boolean(sourcePath && indexedByKb[kbId] && indexedByKb[kbId][sourcePath])
|
||||
};
|
||||
});
|
||||
}).catch(function() { return []; });
|
||||
}
|
||||
|
||||
function buildKnowledgeRagSubmenuChildren(detail, writableBases) {
|
||||
var children = [
|
||||
{ action: 'knowledge-rag-index', icon: 'travel_explore', label: '索引到默认知识库', when: '!workspace.readonly' },
|
||||
{ separator: true },
|
||||
{ heading: true, icon: 'history', label: '最近使用知识库' }
|
||||
];
|
||||
if (writableBases && writableBases.length > 0) {
|
||||
writableBases.slice(0, 3).forEach(function(base) {
|
||||
var indexed = base.sourceAlreadyIndexed;
|
||||
children.push({
|
||||
action: 'knowledge-rag-index',
|
||||
icon: indexed ? 'check_circle' : 'database',
|
||||
label: (indexed ? '✓ ' : '') + base.name,
|
||||
kbId: base.id,
|
||||
providerKbId: base.providerKbId,
|
||||
title: indexed ? '重新索引' : (base.sourceCount ? ('已有 ' + base.sourceCount + ' 个来源') : '加入此知识库'),
|
||||
when: '!workspace.readonly'
|
||||
});
|
||||
});
|
||||
} else {
|
||||
children.push({ action: '', icon: 'database', label: '暂无可写知识库', disabled: true });
|
||||
}
|
||||
children.push({ heading: true, icon: 'folder_managed', label: '当前 workspace 知识库' });
|
||||
(writableBases || []).slice(3).forEach(function(base) {
|
||||
var indexed = base.sourceAlreadyIndexed;
|
||||
children.push({
|
||||
action: 'knowledge-rag-index',
|
||||
icon: indexed ? 'check_circle' : 'database',
|
||||
label: (indexed ? '✓ ' : '') + base.name,
|
||||
kbId: base.id,
|
||||
providerKbId: base.providerKbId,
|
||||
title: indexed ? '重新索引' : (base.sourceCount ? ('已有 ' + base.sourceCount + ' 个来源') : '加入此知识库'),
|
||||
when: '!workspace.readonly'
|
||||
});
|
||||
});
|
||||
children.push(
|
||||
{ separator: true },
|
||||
{ action: 'open-knowledge-page', icon: 'add', label: '新建知识库...' },
|
||||
{ action: 'open-knowledge-page', icon: 'settings', label: '管理知识库...' }
|
||||
);
|
||||
return children;
|
||||
}
|
||||
|
||||
// ── CommandContext 启用态辅助:与 Rust core-protocol CommandContext 保持同一口径 ──
|
||||
|
||||
function currentOpenEditorsSnapshotForCommandContext() {
|
||||
@@ -979,7 +1241,13 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
|
||||
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, destructive: true, requiresApproval: true, when: '!workspace.readonly && !editor.dirty' }
|
||||
];
|
||||
if (currentSourceKind() === 'local_folder' && detail.rowKind !== 'index' && knowledgeRagSupported) {
|
||||
var ragItem = { action: 'knowledge-rag-index', icon: 'travel_explore', label: '加入资料库索引', when: '!workspace.readonly' };
|
||||
var ragChildren = buildKnowledgeRagSubmenuChildren(detail, []);
|
||||
var ragItem = { action: 'knowledge-rag-root', icon: 'travel_explore', label: '加入知识库', children: ragChildren, when: '!workspace.readonly' };
|
||||
ragItem.loadChildren = function() {
|
||||
return fetchKnowledgeBaseList(detail).then(function(bases) {
|
||||
return buildKnowledgeRagSubmenuChildren(detail, bases);
|
||||
});
|
||||
};
|
||||
var insertAt = isAttachment ? 11 : isAsset ? 3 : 3;
|
||||
if (insertAt >= 0) items.splice(insertAt, 0, ragItem);
|
||||
}
|
||||
|
||||
@@ -1274,6 +1274,10 @@ export function createSidebarPageAiRuntime(context) {
|
||||
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:minimize-openhub') {
|
||||
closePageAiDrawer();
|
||||
return;
|
||||
}
|
||||
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 : {};
|
||||
@@ -1822,29 +1826,21 @@ export function createSidebarPageAiRuntime(context) {
|
||||
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({
|
||||
if (window.__mnoteLocalFolderEventBus && typeof window.__mnoteLocalFolderEventBus.emitChangedFiles === 'function') {
|
||||
window.__mnoteLocalFolderEventBus.emitChangedFiles({
|
||||
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'
|
||||
changedFiles: [{ relativePath: targetPath, documentId: documentId, changeType: 'modified' }],
|
||||
affectedParents: [{ relativePath: pageAiParentRelativePath(targetPath), reason: 'openhub_changed_file' }]
|
||||
});
|
||||
}
|
||||
if (typeof window.__mnoteDocumentPaneRuntime.openPrimaryDocument === 'function') {
|
||||
if (documentId === String(currentDocumentId() || '').trim()) {
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-openhub-document-pane-refresh', targetPath);
|
||||
return true;
|
||||
}
|
||||
void window.__mnoteDocumentPaneRuntime.openPrimaryDocument({
|
||||
documentId: documentId,
|
||||
workspaceId: workspaceId,
|
||||
@@ -1861,12 +1857,18 @@ export function createSidebarPageAiRuntime(context) {
|
||||
}
|
||||
|
||||
function pageAiRefreshOpencodeCurrentPage(reason) {
|
||||
if (window.__mnoteDocumentPaneRuntime && typeof window.__mnoteDocumentPaneRuntime.refreshPrimaryDocument === 'function') {
|
||||
if (reason) {
|
||||
void window.__mnoteDocumentPaneRuntime.refreshPrimaryDocument({ reason: reason });
|
||||
} else {
|
||||
void window.__mnoteDocumentPaneRuntime.refreshPrimaryDocument({ reason: 'page-ai-opencode-host' });
|
||||
}
|
||||
var rootUri = String(currentRootUri() || '').trim();
|
||||
var targetPath = String(pageAiOpencodePrimaryPath() || '').trim();
|
||||
var documentId = String(currentDocumentId() || pageAiDocumentIdFromRootRelativePath(targetPath) || '').trim();
|
||||
if (rootUri && targetPath && documentId && window.__mnoteLocalFolderEventBus && typeof window.__mnoteLocalFolderEventBus.emitChangedFiles === 'function') {
|
||||
window.__mnoteLocalFolderEventBus.emitChangedFiles({
|
||||
source: 'opencode_changed_files_adapter',
|
||||
reason: reason || 'page-ai-opencode-host',
|
||||
rootUri: rootUri,
|
||||
workspaceId: resolveWorkspaceId(document.body),
|
||||
changedFiles: [{ relativePath: targetPath, documentId: documentId, changeType: 'modified' }],
|
||||
affectedParents: [{ relativePath: pageAiParentRelativePath(targetPath), reason: reason || 'page-ai-opencode-host' }]
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -3168,22 +3170,25 @@ export function createSidebarPageAiRuntime(context) {
|
||||
affectedParents: affectedParents
|
||||
};
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-receipt-filetree-refresh', 'true');
|
||||
if (window.__mnoteLocalFolderEventBus && typeof window.__mnoteLocalFolderEventBus.emitSyntheticWatchBatch === 'function') {
|
||||
window.__mnoteLocalFolderEventBus.emitSyntheticWatchBatch({
|
||||
if (window.__mnoteLocalFolderEventBus && typeof window.__mnoteLocalFolderEventBus.emitChangedFiles === 'function') {
|
||||
window.__mnoteLocalFolderEventBus.emitChangedFiles({
|
||||
source: 'synthetic_page_ai_receipt',
|
||||
reason: 'agent_run_receipt',
|
||||
rootUri: rootUri,
|
||||
workspaceId: resolveWorkspaceId(document.body),
|
||||
changedFiles: changedPaths,
|
||||
affectedParents: affectedParents,
|
||||
payload: syntheticPayload
|
||||
});
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-receipt-fallback-direct', 'true');
|
||||
window.dispatchEvent(new CustomEvent('tree:local-folder-watch-batch', {
|
||||
detail: { payload: syntheticPayload }
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (refresh.touchesCurrentFile === true) {
|
||||
if (refresh.touchesCurrentFile === true && !files.length) {
|
||||
var documentId = String(refresh.currentDocumentId || receipt && receipt.documentId || currentDocumentId() || '').trim();
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-receipt-current-refresh', 'true');
|
||||
window.dispatchEvent(new CustomEvent('mnote:page-ai-tool-write-completed', {
|
||||
|
||||
@@ -700,29 +700,29 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
|
||||
function createKnowledgeRagSettingsPanelHtmlV2() {
|
||||
return '' +
|
||||
'<div class="wolai-page-settings-panel mnote-settings-panel mnote-knowledge-rag-settings-panel mnote-weknora-kb-page" role="dialog" aria-modal="false" aria-label="WeKnora 知识库" data-testid="mnote-weknora-knowledge-settings-panel" data-knowledge-rag-default-provider="weknora" data-knowledge-rag-legacy-fallback-provider="lightrag" data-knowledge-rag-source-registry="mnote" data-knowledge-rag-provider-index="weknora-kb-chunk-index" data-mnote-weknora-page-replacement="true">' +
|
||||
'<div class="wolai-page-settings-panel mnote-settings-panel mnote-knowledge-rag-settings-panel mnote-weknora-kb-page" role="dialog" aria-modal="false" aria-label="LightRAG 知识库" data-testid="mnote-weknora-knowledge-settings-panel" data-knowledge-rag-default-provider="lightrag" data-knowledge-rag-legacy-fallback-provider="lightrag" data-knowledge-rag-source-registry="mnote" data-knowledge-rag-provider-index="lightrag-source-registry" data-mnote-weknora-page-replacement="false">' +
|
||||
'<div class="mnote-weknora-kb-page-header">' +
|
||||
'<div class="mnote-weknora-kb-page-title">' +
|
||||
'<span class="mnote-weknora-kb-page-eyebrow">WeKnora</span>' +
|
||||
'<span class="mnote-weknora-kb-page-eyebrow">LightRAG</span>' +
|
||||
'<strong>知识库</strong>' +
|
||||
'<span>由 MNote 工作区授权文件驱动</span>' +
|
||||
'<span>由 MNote source registry 与 LightRAG 索引驱动</span>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-weknora-kb-page-actions">' +
|
||||
'<button type="button" data-knowledge-rag-action="refresh" title="刷新"><span class="material-symbols-outlined" data-icon="refresh" aria-hidden="true"></span></button>' +
|
||||
'<button type="button" data-knowledge-rag-action="open-dashboard" title="打开知识库" aria-label="打开知识库"><span class="material-symbols-outlined" data-icon="open_in_new" aria-hidden="true"></span></button>' +
|
||||
'<button type="button" class="mnote-settings-panel-close" data-settings-action="close" aria-label="关闭 WeKnora 知识库"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
|
||||
'<button type="button" class="mnote-settings-panel-close" data-settings-action="close" aria-label="关闭知识库"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-weknora-kb-page-status" data-testid="mnote-knowledge-rag-status"></div>' +
|
||||
'<div class="mnote-weknora-kb-page-body">' +
|
||||
'<aside class="mnote-weknora-kb-list-pane" data-testid="mnote-weknora-kb-list-pane">' +
|
||||
'<div class="mnote-weknora-kb-list-head">' +
|
||||
'<div><strong>知识库列表</strong><span data-testid="mnote-weknora-kb-list-count">0</span></div>' +
|
||||
'<button type="button" data-knowledge-rag-action="focus-create-kb" title="新建知识库"><span class="material-symbols-outlined" data-icon="create_new_folder" aria-hidden="true"></span></button>' +
|
||||
'<div><strong>资料库</strong><span data-testid="mnote-weknora-kb-list-count">0</span></div>' +
|
||||
'<button type="button" data-knowledge-rag-action="focus-create-kb" title="LightRAG 当前使用单资料库" hidden><span class="material-symbols-outlined" data-icon="create_new_folder" aria-hidden="true"></span></button>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-weknora-kb-card-wrap" data-testid="mnote-weknora-kb-card-list"></div>' +
|
||||
'<section class="mnote-weknora-kb-create-card" data-testid="mnote-weknora-kb-controls">' +
|
||||
'<div class="mnote-weknora-kb-create-title"><span class="material-symbols-outlined" data-icon="folder_add" aria-hidden="true"></span><strong>新建知识库</strong></div>' +
|
||||
'<section class="mnote-weknora-kb-create-card" data-testid="mnote-weknora-kb-controls" hidden>' +
|
||||
'<div class="mnote-weknora-kb-create-title"><span class="material-symbols-outlined" data-icon="folder_add" aria-hidden="true"></span><strong>新建逻辑资料库</strong></div>' +
|
||||
'<input type="text" class="wolai-page-settings-index-path" data-testid="mnote-weknora-create-kb-name" data-knowledge-rag-create-kb-name="true" placeholder="知识库名称" spellcheck="false" />' +
|
||||
'<input type="text" class="wolai-page-settings-index-path" data-testid="mnote-weknora-create-kb-description" data-knowledge-rag-create-kb-description="true" placeholder="描述,可留空" spellcheck="false" />' +
|
||||
'<button type="button" class="wolai-page-settings-index-add" data-knowledge-rag-action="create-kb">创建知识库</button>' +
|
||||
@@ -732,19 +732,19 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'<section class="mnote-weknora-kb-detail-pane" data-testid="mnote-weknora-kb-detail-pane">' +
|
||||
'<div class="mnote-weknora-kb-detail-hero" data-testid="mnote-weknora-kb-detail-hero">' +
|
||||
'<div>' +
|
||||
'<nav class="mnote-weknora-kb-breadcrumb" data-testid="mnote-weknora-kb-breadcrumb" aria-label="知识库面包屑"><span>知识库</span><span>/</span><strong data-testid="mnote-weknora-kb-breadcrumb-current">WeKnora 知识库</strong></nav>' +
|
||||
'<span class="mnote-weknora-kb-type-pill"><span class="material-symbols-outlined" data-icon="folder" aria-hidden="true"></span>Document KB</span>' +
|
||||
'<h2 data-testid="mnote-weknora-kb-detail-title">WeKnora 知识库</h2>' +
|
||||
'<p data-testid="mnote-weknora-kb-detail-description">MNote 负责工作区授权和 source registry,WeKnora 负责解析、chunk、检索和引用。</p>' +
|
||||
'<nav class="mnote-weknora-kb-breadcrumb" data-testid="mnote-weknora-kb-breadcrumb" aria-label="知识库面包屑"><span>知识库</span><span>/</span><strong data-testid="mnote-weknora-kb-breadcrumb-current">LightRAG 资料库</strong></nav>' +
|
||||
'<span class="mnote-weknora-kb-type-pill"><span class="material-symbols-outlined" data-icon="folder" aria-hidden="true"></span>Source Registry</span>' +
|
||||
'<h2 data-testid="mnote-weknora-kb-detail-title">LightRAG 资料库</h2>' +
|
||||
'<p data-testid="mnote-weknora-kb-detail-description">MNote 管理本地来源、引用定位和权限边界,LightRAG 负责索引与召回。</p>' +
|
||||
'</div>' +
|
||||
'<select class="mnote-kb-params-select" data-testid="mnote-weknora-kb-select" data-knowledge-rag-kb-select="true"><option value="">加载中...</option></select>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-meta" data-testid="mnote-knowledge-rag-meta"></div>' +
|
||||
'<div class="mnote-knowledge-rag-detail-tabs" role="tablist" aria-label="WeKnora 知识库详情">' +
|
||||
'<button type="button" class="mnote-knowledge-rag-detail-tab is-active" role="tab" aria-selected="true" data-kb-rag-tab="sources" data-testid="mnote-weknora-tab-documents">Documents</button>' +
|
||||
'<div class="mnote-knowledge-rag-detail-tabs" role="tablist" aria-label="LightRAG 知识库详情">' +
|
||||
'<button type="button" class="mnote-knowledge-rag-detail-tab is-active" role="tab" aria-selected="true" data-kb-rag-tab="sources" data-testid="mnote-weknora-tab-documents">来源</button>' +
|
||||
'<button type="button" class="mnote-knowledge-rag-detail-tab" role="tab" aria-selected="false" data-kb-rag-tab="wiki" data-testid="mnote-weknora-tab-wiki">Wiki</button>' +
|
||||
'<button type="button" class="mnote-knowledge-rag-detail-tab" role="tab" aria-selected="false" data-kb-rag-tab="graph" data-testid="mnote-weknora-tab-graph">Graph</button>' +
|
||||
'<button type="button" class="mnote-knowledge-rag-detail-tab" role="tab" aria-selected="false" data-kb-rag-tab="search">Search</button>' +
|
||||
'<button type="button" class="mnote-knowledge-rag-detail-tab" role="tab" aria-selected="false" data-kb-rag-tab="search">检索</button>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-detail-panel" data-kb-rag-panel="sources">' +
|
||||
'<div class="mnote-weknora-doc-layout" data-testid="mnote-weknora-document-management">' +
|
||||
@@ -755,7 +755,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'<button type="button" data-knowledge-rag-action="filter-sources" data-knowledge-rag-filter="indexed"><span class="material-symbols-outlined" data-icon="check_circle" aria-hidden="true"></span>已索引</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="filter-sources" data-knowledge-rag-filter="indexing"><span class="material-symbols-outlined" data-icon="sync" aria-hidden="true"></span>处理中</button>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-weknora-doc-sidebar-head"><strong>标签</strong><span>WeKnora views</span></div>' +
|
||||
'<div class="mnote-weknora-doc-sidebar-head"><strong>标签</strong><span>MNote views</span></div>' +
|
||||
'<div class="mnote-weknora-doc-tags" data-testid="mnote-weknora-doc-tags"><span>PDF</span><span>Markdown</span><span>Folder</span><span>Citation</span></div>' +
|
||||
'</aside>' +
|
||||
'<section class="mnote-weknora-doc-main">' +
|
||||
@@ -767,12 +767,13 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'</div>' +
|
||||
'<button type="button" data-knowledge-rag-action="use-filetree-selection"><span class="material-symbols-outlined" data-icon="account_tree" aria-hidden="true"></span>添加文件夹</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="add-source"><span class="material-symbols-outlined" data-icon="upload_file" aria-hidden="true"></span>添加文档</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="ingest-active"><span class="material-symbols-outlined" data-icon="note_add" aria-hidden="true"></span>加入当前页</button>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-source-inputs" data-testid="mnote-knowledge-rag-source-inputs">' +
|
||||
knowledgeRagSourceInputRow('') +
|
||||
'</div>' +
|
||||
'<div class="mnote-weknora-doc-actions">' +
|
||||
'<button type="button" data-knowledge-rag-action="ingest">上传/索引到 WeKnora</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="ingest">索引到 LightRAG</button>' +
|
||||
'<button type="button" data-knowledge-rag-action="prune">清除失效记录</button>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-source-filters" data-testid="mnote-knowledge-rag-source-filters"></div>' +
|
||||
@@ -783,13 +784,13 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'<div class="mnote-knowledge-rag-detail-panel" data-kb-rag-panel="wiki" hidden>' +
|
||||
'<div class="mnote-weknora-placeholder-panel" data-testid="mnote-weknora-wiki-panel">' +
|
||||
'<strong>Wiki</strong>' +
|
||||
'<span>从 WeKnora 文档解析与 chunk 结果生成的知识页入口;当前保留 MNote open-reference 边界。</span>' +
|
||||
'<span>后续可基于 LightRAG 召回结果生成知识页;当前以 MNote open-reference 边界为准。</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-detail-panel" data-kb-rag-panel="graph" hidden>' +
|
||||
'<div class="mnote-weknora-placeholder-panel" data-testid="mnote-weknora-graph-panel">' +
|
||||
'<strong>Graph</strong>' +
|
||||
'<span>展示 provider 侧文档关系与引用入口,不迁移 WeKnora tenant 或文件真相。</span>' +
|
||||
'<span>展示 LightRAG 图谱和文档关系入口,本地文件真相仍由 MNote source registry 管理。</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-detail-panel" data-kb-rag-panel="search" hidden>' +
|
||||
@@ -798,7 +799,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'<button type="button" class="wolai-page-settings-index-add" data-kb-rag-action="search">搜索</button>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-kb-search-results" data-kb-rag-search-results="true">' +
|
||||
'<span class="mnote-extensions-loading">输入关键词后用 WeKnora hybrid 检索</span>' +
|
||||
'<span class="mnote-extensions-loading">输入关键词后用 LightRAG hybrid 检索</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-knowledge-rag-detail-panel" data-kb-rag-panel="params" hidden>' +
|
||||
@@ -806,7 +807,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'<div class="mnote-kb-params-row">' +
|
||||
'<label class="mnote-kb-params-label">Provider 检索策略</label>' +
|
||||
'<select class="mnote-kb-params-select" data-kb-rag-param="mode">' +
|
||||
'<option value="hybrid" selected>WeKnora hybrid</option>' +
|
||||
'<option value="hybrid" selected>LightRAG hybrid</option>' +
|
||||
'<option value="vector">向量召回</option>' +
|
||||
'<option value="keyword">关键词召回</option>' +
|
||||
'<option value="rerank">Hybrid + rerank</option>' +
|
||||
@@ -1214,11 +1215,11 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
}
|
||||
|
||||
function knowledgeRagCurrentFilter() {
|
||||
return pageUiState.knowledgeRagSourceFilter || 'attention';
|
||||
return pageUiState.knowledgeRagSourceFilter || 'all';
|
||||
}
|
||||
|
||||
function setKnowledgeRagSourceFilter(filter) {
|
||||
pageUiState.knowledgeRagSourceFilter = String(filter || 'attention').trim() || 'attention';
|
||||
pageUiState.knowledgeRagSourceFilter = String(filter || 'all').trim() || 'all';
|
||||
renderKnowledgeRagSettings();
|
||||
}
|
||||
|
||||
@@ -1226,7 +1227,8 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var deleted = entry && entry.deletedAtMs;
|
||||
var stale = entry && entry.stale;
|
||||
var providerStatus = knowledgeRagEntryProviderStatus(entry);
|
||||
var failed = providerStatus === 'failed';
|
||||
var providerMissing = providerStatus === 'provider_missing';
|
||||
var failed = providerStatus === 'failed' || providerMissing;
|
||||
var deleting = providerStatus === 'delete_submitted';
|
||||
var deletedDone = providerStatus === 'delete_completed';
|
||||
var docId = knowledgeRagEntryProviderDocId(entry);
|
||||
@@ -1239,14 +1241,14 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
|
||||
function knowledgeRagProviderName(status) {
|
||||
var config = status && status.providerConfig && typeof status.providerConfig === 'object' ? status.providerConfig : {};
|
||||
return String(status && status.provider || config.active || 'weknora').trim() || 'weknora';
|
||||
return String(status && status.provider || config.active || 'lightrag_legacy').trim() || 'lightrag_legacy';
|
||||
}
|
||||
|
||||
function knowledgeRagProviderLabel(status) {
|
||||
var providerName = knowledgeRagProviderName(status);
|
||||
if (providerName === 'weknora') return 'WeKnora';
|
||||
if (providerName === 'lightrag') return 'LightRAG legacy';
|
||||
if (providerName === 'lightrag_legacy') return 'LightRAG legacy';
|
||||
if (providerName === 'lightrag') return 'LightRAG';
|
||||
if (providerName === 'lightrag_legacy') return 'LightRAG';
|
||||
return providerName;
|
||||
}
|
||||
|
||||
@@ -1405,14 +1407,25 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
seen[providerKbId] = true;
|
||||
output.push({
|
||||
providerKbId: providerKbId,
|
||||
name: 'WeKnora KB ' + providerKbId.slice(0, 8),
|
||||
description: '由 WeKnora provider 配置注入的知识库',
|
||||
name: 'Provider KB ' + providerKbId.slice(0, 8),
|
||||
description: '由知识库 provider 配置注入的资料库',
|
||||
sourceCount: 0,
|
||||
chunkCount: 0,
|
||||
updatedAt: '',
|
||||
status: 'configured'
|
||||
});
|
||||
});
|
||||
if (!output.length && ['lightrag', 'lightrag_legacy'].includes(knowledgeRagProviderName(status || {}))) {
|
||||
output.push({
|
||||
providerKbId: 'lightrag-default',
|
||||
name: '默认资料库',
|
||||
description: 'LightRAG 单工作区索引,由 MNote source registry 划分范围',
|
||||
sourceCount: ((status && status.registry && Array.isArray(status.registry.entries)) ? status.registry.entries.length : 0),
|
||||
chunkCount: 0,
|
||||
updatedAt: '',
|
||||
status: 'active'
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -1432,6 +1445,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
|
||||
function knowledgeRagEntriesForKb(entries, providerKnowledgeBaseId) {
|
||||
var selected = String(providerKnowledgeBaseId || '').trim();
|
||||
if (selected === 'lightrag-default') return Array.isArray(entries) ? entries : [];
|
||||
return (Array.isArray(entries) ? entries : []).filter(function(entry) {
|
||||
return selected && knowledgeRagEntryProviderKnowledgeBaseId(entry) === selected;
|
||||
});
|
||||
@@ -1439,6 +1453,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
|
||||
function knowledgeRagProviderDocsForKb(providerDocs, providerKnowledgeBaseId) {
|
||||
var selected = String(providerKnowledgeBaseId || '').trim();
|
||||
if (selected === 'lightrag-default') return Array.isArray(providerDocs) ? providerDocs : [];
|
||||
return (Array.isArray(providerDocs) ? providerDocs : []).filter(function(doc) {
|
||||
var id = String(doc && (doc.knowledgeBaseId || doc.knowledge_base_id || doc.providerKnowledgeBaseId || doc.provider_knowledge_base_id) || '').trim();
|
||||
return selected && id === selected;
|
||||
@@ -1482,17 +1497,18 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
if (!(listNode instanceof HTMLElement)) return;
|
||||
var bases = knowledgeRagKnowledgeBases(status || {});
|
||||
var selected = selectedKnowledgeRagProviderKnowledgeBaseId(status || {});
|
||||
var providerName = knowledgeRagProviderName(status || {});
|
||||
if (countNode instanceof HTMLElement) countNode.textContent = String(bases.length);
|
||||
if (!bases.length) {
|
||||
listNode.innerHTML = '<div class="wolai-page-settings-index-empty">还没有 WeKnora 知识库</div>';
|
||||
listNode.innerHTML = '<div class="wolai-page-settings-index-empty">还没有知识库来源</div>';
|
||||
return;
|
||||
}
|
||||
listNode.innerHTML = bases.map(function(base) {
|
||||
var stats = knowledgeRagBaseStats(base, entries, providerDocs);
|
||||
var isActive = base.providerKbId === selected;
|
||||
var status = String(base.status || 'active');
|
||||
var baseStatus = String(base.status || 'active');
|
||||
return '' +
|
||||
'<button type="button" class="mnote-weknora-kb-card' + (isActive ? ' is-active' : '') + '" data-knowledge-rag-action="select-kb" data-knowledge-rag-kb-id="' + escapeHtml(base.providerKbId) + '" data-knowledge-rag-kb-provider="weknora" data-knowledge-rag-processing-count="' + escapeHtml(String(stats.processingCount)) + '">' +
|
||||
'<button type="button" class="mnote-weknora-kb-card' + (isActive ? ' is-active' : '') + '" data-knowledge-rag-action="select-kb" data-knowledge-rag-kb-id="' + escapeHtml(base.providerKbId) + '" data-knowledge-rag-kb-provider="' + escapeHtml(providerName) + '" data-knowledge-rag-processing-count="' + escapeHtml(String(stats.processingCount)) + '">' +
|
||||
'<span class="mnote-weknora-kb-card-star"><span class="material-symbols-outlined" data-icon="folder" aria-hidden="true"></span></span>' +
|
||||
'<span class="mnote-weknora-kb-card-main">' +
|
||||
'<strong title="' + escapeHtml(base.name) + '">' + escapeHtml(base.name) + '</strong>' +
|
||||
@@ -1503,7 +1519,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
(stats.processingCount ? '<i class="is-processing"><span class="material-symbols-outlined" data-icon="sync" aria-hidden="true"></span>' + escapeHtml(String(stats.processingCount)) + '</i>' : '') +
|
||||
'</span>' +
|
||||
'</span>' +
|
||||
'<span class="mnote-weknora-kb-card-status" data-status="' + escapeHtml(status) + '">' + escapeHtml(status) + '</span>' +
|
||||
'<span class="mnote-weknora-kb-card-status" data-status="' + escapeHtml(baseStatus) + '">' + escapeHtml(baseStatus) + '</span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
}
|
||||
@@ -1518,7 +1534,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var base = bases.find(function(item) { return item.providerKbId === selected; }) || bases[0] || null;
|
||||
if (!base) {
|
||||
titleNode.textContent = '未配置知识库';
|
||||
descNode.textContent = '创建 WeKnora 知识库后,可在这里管理 MNote 授权的本地文件和引用。';
|
||||
descNode.textContent = '加入资料来源后,可在这里管理 MNote 授权的本地文件和引用。';
|
||||
if (breadcrumbNode instanceof HTMLElement) breadcrumbNode.textContent = '未配置';
|
||||
return;
|
||||
}
|
||||
@@ -1562,7 +1578,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var activeCount = Object.keys(activeKbIds).length || kbIds.length;
|
||||
var processingCount = knowledgeRagInFlightCount(entries, providerDocs);
|
||||
return '' +
|
||||
'<div class="mnote-weknora-kb-summary-card" data-testid="mnote-weknora-kb-summary" data-knowledge-rag-kb-id="' + escapeHtml(kbIds[0] || '') + '" data-knowledge-rag-kb-ids="' + escapeHtml(kbIds.join(',')) + '" data-knowledge-rag-kb-provider="weknora" data-knowledge-rag-kb-status="configured">' +
|
||||
'<div class="mnote-weknora-kb-summary-card" data-testid="mnote-weknora-kb-summary" data-knowledge-rag-kb-id="' + escapeHtml(kbIds[0] || '') + '" data-knowledge-rag-kb-ids="' + escapeHtml(kbIds.join(',')) + '" data-knowledge-rag-kb-provider="' + escapeHtml(knowledgeRagProviderName(status || {})) + '" data-knowledge-rag-kb-status="configured">' +
|
||||
'<div><span>知识库</span><strong>' + escapeHtml(activeCount ? String(activeCount) + ' 个' : '未配置') + '</strong></div>' +
|
||||
'<div><span>文档</span><strong>' + escapeHtml(String((entries || []).length)) + '</strong></div>' +
|
||||
'<div><span>Provider docs</span><strong>' + escapeHtml(String((providerDocs || []).length)) + '</strong></div>' +
|
||||
@@ -1873,6 +1889,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
return;
|
||||
}
|
||||
var pipelineProgress = knowledgeRagPipelineProgress(status || {});
|
||||
var providerName = knowledgeRagProviderName(status || {});
|
||||
var providerDocs = knowledgeRagProviderDocuments(status || {});
|
||||
var providerDocsById = new Map();
|
||||
var providerDocsByPath = new Map();
|
||||
@@ -1895,7 +1912,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var knowledgeId = knowledgeRagProviderKnowledgeId(doc);
|
||||
var providerTitle = providerPath || providerId || 'Provider 文档';
|
||||
var chunkCount = knowledgeRagProviderChunkCount(doc);
|
||||
var providerMeta = 'provider=weknora · 未映射到 MNote source registry · ' + docStatus
|
||||
var providerMeta = 'provider=' + providerName + ' · 未映射到 MNote source registry · ' + docStatus
|
||||
+ (providerId ? ' · provider id ' + providerId : '')
|
||||
+ (chunkCount ? ' · chunk ' + chunkCount : '')
|
||||
+ (providerPath ? '' : ' · 定位降级');
|
||||
@@ -1903,7 +1920,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
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-provider="weknora" data-knowledge-rag-provider-id="' + escapeHtml(providerId) + '" data-knowledge-rag-knowledge-id="' + escapeHtml(knowledgeId) + '" data-knowledge-rag-chunk-count="' + escapeHtml(String(chunkCount || 0)) + '" data-knowledge-rag-processing-status="' + escapeHtml(docStatus) + '" data-knowledge-rag-open-reference-mode="provider-reference-fallback" data-knowledge-rag-source-path="' + escapeHtml(providerPath || providerId) + '">' +
|
||||
'<div class="mnote-knowledge-rag-source-row" data-knowledge-rag-source-row="true" data-knowledge-rag-source-kind="provider" data-knowledge-rag-provider="' + escapeHtml(providerName) + '" data-knowledge-rag-provider-id="' + escapeHtml(providerId) + '" data-knowledge-rag-knowledge-id="' + escapeHtml(knowledgeId) + '" data-knowledge-rag-chunk-count="' + escapeHtml(String(chunkCount || 0)) + '" data-knowledge-rag-processing-status="' + escapeHtml(docStatus) + '" data-knowledge-rag-open-reference-mode="provider-reference-fallback" data-knowledge-rag-source-path="' + escapeHtml(providerPath || providerId) + '">' +
|
||||
'<span class="wolai-page-settings-index-status-dot" data-index-status="' + (failedDoc ? 'fault' : 'indexed') + '" title="' + escapeHtml(docStatus || 'Provider 文档') + '"></span>' +
|
||||
'<div class="mnote-knowledge-rag-source-main">' +
|
||||
'<strong title="' + escapeHtml(providerTitle) + '">' + escapeHtml(providerTitle) + '</strong>' +
|
||||
@@ -1918,7 +1935,8 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var deleted = entry && entry.deletedAtMs;
|
||||
var stale = entry && entry.stale;
|
||||
var providerStatus = knowledgeRagEntryProviderStatus(entry);
|
||||
var failed = providerStatus === 'failed';
|
||||
var providerMissing = providerStatus === 'provider_missing';
|
||||
var failed = providerStatus === 'failed' || providerMissing;
|
||||
var deleting = providerStatus === 'delete_submitted';
|
||||
var deletedDone = providerStatus === 'delete_completed';
|
||||
var docId = knowledgeRagEntryProviderDocId(entry);
|
||||
@@ -1930,7 +1948,8 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
: deleting ? '删除已提交'
|
||||
: deleted ? '已删除'
|
||||
: stale ? '已过期'
|
||||
: failed ? '索引失败'
|
||||
: providerMissing ? 'Provider 索引缺失'
|
||||
: failed ? '索引失败'
|
||||
: indexed ? '已索引'
|
||||
: (docId || providerStatus ? '正在索引' : '待提交');
|
||||
var providerDoc = (docId && providerDocsById.get(docId)) || providerDocsByPath.get(String(entry && entry.lightRagFilePath || '').trim()) || null;
|
||||
@@ -1939,7 +1958,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var kbId = knowledgeRagEntryProviderKnowledgeBaseId(entry);
|
||||
var metaParts = [
|
||||
statusLabel,
|
||||
'provider=weknora',
|
||||
'provider=' + providerName,
|
||||
'MNote source registry',
|
||||
kbId ? 'kb ' + kbId : '',
|
||||
providerIds.length ? 'provider ids ' + providerIds.join(', ') : '',
|
||||
@@ -1955,7 +1974,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
entryProgress = knowledgeRagIndeterminateProgress(status || {}, providerDoc, providerStatus);
|
||||
}
|
||||
return '' +
|
||||
'<div class="mnote-knowledge-rag-source-row" data-knowledge-rag-source-row="true" data-knowledge-rag-provider="weknora" data-knowledge-rag-provider-ids="' + escapeHtml(providerIds.join(',')) + '" data-knowledge-rag-kb-id="' + escapeHtml(kbId) + '" data-knowledge-rag-knowledge-id="' + escapeHtml(knowledgeId) + '" data-knowledge-rag-chunk-count="' + escapeHtml(String(chunkCount || 0)) + '" data-knowledge-rag-processing-status="' + escapeHtml(providerStatus || statusLabel) + '" data-knowledge-rag-open-reference-mode="source-registry-reference" data-knowledge-rag-source-path="' + escapeHtml(sourcePath) + '">' +
|
||||
'<div class="mnote-knowledge-rag-source-row" data-knowledge-rag-source-row="true" data-knowledge-rag-provider="' + escapeHtml(providerName) + '" data-knowledge-rag-provider-ids="' + escapeHtml(providerIds.join(',')) + '" data-knowledge-rag-kb-id="' + escapeHtml(kbId) + '" data-knowledge-rag-knowledge-id="' + escapeHtml(knowledgeId) + '" data-knowledge-rag-chunk-count="' + escapeHtml(String(chunkCount || 0)) + '" data-knowledge-rag-processing-status="' + escapeHtml(providerStatus || statusLabel) + '" data-knowledge-rag-open-reference-mode="source-registry-reference" data-knowledge-rag-source-path="' + escapeHtml(sourcePath) + '">' +
|
||||
'<span class="wolai-page-settings-index-status-dot" data-index-status="' + kind + '" title="' + escapeHtml(statusLabel) + '"></span>' +
|
||||
'<div class="mnote-knowledge-rag-source-main">' +
|
||||
'<strong title="' + escapeHtml(title) + '">' + escapeHtml(title || '未命名来源') + '</strong>' +
|
||||
@@ -1980,7 +1999,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
if (!(statusNode instanceof HTMLElement)) return;
|
||||
|
||||
if (!knowledgeRagIsAvailable()) {
|
||||
statusNode.textContent = '资料库问答仅在本地工作区可用';
|
||||
statusNode.textContent = '知识库仅在本地工作区可用';
|
||||
if (metaNode instanceof HTMLElement) metaNode.innerHTML = '';
|
||||
renderKnowledgeRagSourceFilters(popover, []);
|
||||
renderKnowledgeRagSources(popover, [], {});
|
||||
@@ -2013,7 +2032,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
if (metaNode instanceof HTMLElement) {
|
||||
metaNode.setAttribute('data-knowledge-rag-active-provider', knowledgeRagProviderName(status));
|
||||
metaNode.setAttribute('data-knowledge-rag-legacy-fallback-provider', 'lightrag');
|
||||
metaNode.setAttribute('data-knowledge-rag-reference-boundary', 'mnote-source-registry-weknora-kb-chunk-index');
|
||||
metaNode.setAttribute('data-knowledge-rag-reference-boundary', 'mnote-source-registry-lightrag-sidecar');
|
||||
}
|
||||
var healthStatus = String(healthBody.status || '');
|
||||
var healthy = health.ok === true && (!healthStatus || healthStatus === 'healthy' || healthStatus === 'ok');
|
||||
@@ -2054,7 +2073,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
metaNode.innerHTML = '' +
|
||||
knowledgeRagKbSummaryHtml(status, entries, providerDocs) +
|
||||
'<div><span>Provider</span><code>' + escapeHtml(providerLabel) + '</code></div>' +
|
||||
'<div><span>Boundary</span><code>MNote source registry / WeKnora KB chunk index</code></div>' +
|
||||
'<div><span>Boundary</span><code>MNote source registry / LightRAG sidecar locator</code></div>' +
|
||||
'<div><span>Dashboard</span><code>' + escapeHtml(dashboardUrl || '未配置') + '</code></div>' +
|
||||
'<div><span>Model</span><code>' + escapeHtml(llmModel || '未上报') + (vlmModel ? ' / VLM ' + escapeHtml(vlmModel) : '') + '</code></div>' +
|
||||
'<div><span>Rerank</span><code>' + escapeHtml(rerankLabel + (rerankBinding ? ' · ' + rerankBinding : '') + (rerankModel ? ' · ' + rerankModel : '')) + '</code></div>' +
|
||||
@@ -2067,8 +2086,12 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
|
||||
function publicKnowledgeRagDashboardUrl(status) {
|
||||
var config = status && status.providerConfig && typeof status.providerConfig === 'object' ? status.providerConfig : {};
|
||||
var active = knowledgeRagProviderName(status || {});
|
||||
var weknora = config.weknora && typeof config.weknora === 'object' ? config.weknora : {};
|
||||
var url = String((weknora && weknora.dashboardUrl) || (status && (status.dashboardUrl || status.endpoint)) || '').trim();
|
||||
var lightrag = config.lightrag && typeof config.lightrag === 'object' ? config.lightrag : {};
|
||||
var url = active === 'weknora'
|
||||
? String((weknora && weknora.dashboardUrl) || (status && (status.dashboardUrl || status.endpoint)) || '').trim()
|
||||
: String((status && (status.dashboardUrl || status.endpoint)) || (lightrag && lightrag.dashboardUrl) || '').trim();
|
||||
if (!url) return '';
|
||||
try {
|
||||
var parsed = new URL(url, window.location.href);
|
||||
@@ -2496,10 +2519,12 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
}
|
||||
|
||||
function openKnowledgeRagDashboard() {
|
||||
var status = currentKnowledgeRagSummary().status || {};
|
||||
var url = publicKnowledgeRagDashboardUrl(status);
|
||||
if (!url) return;
|
||||
window.open(url, '_blank', 'noopener');
|
||||
var url = publicKnowledgeRagDashboardUrl(currentKnowledgeRagSummary().status || {});
|
||||
if (url && typeof window.open === 'function') {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
}
|
||||
if (url) window.location.href = url;
|
||||
}
|
||||
|
||||
// --- 知识库详情 tab 切换 ---
|
||||
@@ -2612,7 +2637,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var providerKnowledgeId = String(providerIds.knowledgeId || r.providerKnowledgeId || '').trim();
|
||||
var providerChunkId = String(providerIds.chunkId || r.providerChunkId || r.chunkId || '').trim();
|
||||
var preview = String(content).substring(0, 200);
|
||||
html += '<div class="mnote-kb-search-result-item" data-kb-rag-search-result="true" data-knowledge-rag-provider="weknora" data-knowledge-rag-kb-id="' + escapeHtml(providerKnowledgeBaseId) + '" data-knowledge-rag-knowledge-id="' + escapeHtml(providerKnowledgeId) + '" data-knowledge-rag-chunk-id="' + escapeHtml(providerChunkId) + '" data-knowledge-rag-source-path="' + escapeHtml(String(source)) + '">' +
|
||||
html += '<div class="mnote-kb-search-result-item" data-kb-rag-search-result="true" data-knowledge-rag-provider="' + escapeHtml(knowledgeRagProviderName(currentKnowledgeRagSummary().status || {})) + '" data-knowledge-rag-kb-id="' + escapeHtml(providerKnowledgeBaseId) + '" data-knowledge-rag-knowledge-id="' + escapeHtml(providerKnowledgeId) + '" data-knowledge-rag-chunk-id="' + escapeHtml(providerChunkId) + '" data-knowledge-rag-source-path="' + escapeHtml(String(source)) + '">' +
|
||||
'<div class="mnote-kb-search-result-header">' +
|
||||
'<span class="mnote-kb-search-result-index">#' + (i + 1) + '</span>' +
|
||||
'<span class="mnote-kb-search-result-score" data-kb-rag-score-label="provider-ranking-score" data-kb-rag-score-is-similarity-percent="false">provider 排序分: ' + score + '</span>' +
|
||||
@@ -2809,14 +2834,20 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
var data = await resp.json();
|
||||
_knowledgeRagExtensionsCache = data;
|
||||
var indexedCount = (data.files && data.files.length) || data.total_files || 0;
|
||||
var statusText = data.ready ? '就绪' : '未就绪';
|
||||
var statusClass = data.ready ? 'mnote-status-ready' : 'mnote-status-pending';
|
||||
var docs = knowledgeRagProviderDocuments(data);
|
||||
var registry = data && data.registry && typeof data.registry === 'object' ? data.registry : {};
|
||||
var entries = Array.isArray(registry.entries) ? registry.entries : [];
|
||||
var indexedCount = entries.length || docs.length || data.total_files || 0;
|
||||
var health = data && data.health && typeof data.health === 'object' ? data.health : {};
|
||||
var body = health.health && typeof health.health === 'object' ? health.health : {};
|
||||
var ready = data && data.ok === true && health.ok === true && (!body.status || body.status === 'healthy' || body.status === 'ok');
|
||||
var statusText = ready ? '就绪' : '未就绪';
|
||||
var statusClass = ready ? 'mnote-status-ready' : 'mnote-status-pending';
|
||||
var lastIngest = data.last_ingest_time || '';
|
||||
statusEl.innerHTML = '' +
|
||||
'<div class="mnote-extensions-item">' +
|
||||
'<div class="mnote-extensions-item-row"><span class="mnote-extensions-label">知识库 provider 状态</span><span class="' + statusClass + '">' + escapeHtml(String(statusText)) + '</span></div>' +
|
||||
'<div class="mnote-extensions-item-row"><span class="mnote-extensions-label">已索引文件</span><span>' + indexedCount + ' 个</span></div>' +
|
||||
'<div class="mnote-extensions-item-row"><span class="mnote-extensions-label">已索引来源</span><span>' + indexedCount + ' 个</span></div>' +
|
||||
(lastIngest ? '<div class="mnote-extensions-item-row"><span class="mnote-extensions-label">最近更新</span><span>' + escapeHtml(String(lastIngest)) + '</span></div>' : '') +
|
||||
'<div class="mnote-extensions-item-actions">' +
|
||||
'<button type="button" class="wolai-page-settings-action" data-knowledge-rag-action="manage">管理知识库...</button>' +
|
||||
@@ -3166,9 +3197,15 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var resp = await fetch('/api/knowledge-rag/status');
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
var data = await resp.json();
|
||||
var count = (data.files && data.files.length) || data.total_files || 0;
|
||||
var statusText = data.ready ? '就绪' : '未就绪';
|
||||
el.textContent = count + ' 文件 / ' + statusText;
|
||||
var docs = knowledgeRagProviderDocuments(data);
|
||||
var registry = data && data.registry && typeof data.registry === 'object' ? data.registry : {};
|
||||
var entries = Array.isArray(registry.entries) ? registry.entries : [];
|
||||
var health = data && data.health && typeof data.health === 'object' ? data.health : {};
|
||||
var body = health.health && typeof health.health === 'object' ? health.health : {};
|
||||
var ready = data && data.ok === true && health.ok === true && (!body.status || body.status === 'healthy' || body.status === 'ok');
|
||||
var count = entries.length || docs.length || data.total_files || 0;
|
||||
var statusText = ready ? '就绪' : '未就绪';
|
||||
el.textContent = count + ' 来源 / ' + statusText;
|
||||
} catch (_) {
|
||||
el.textContent = '不可用';
|
||||
}
|
||||
|
||||
@@ -2157,12 +2157,12 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
'<div class="wolai-search-options-left">' +
|
||||
'<span class="wolai-search-switch-control"><span>仅匹配标题</span><button type="button" class="wolai-search-switch" data-search-switch="title" role="switch" aria-checked="false" aria-label="仅匹配标题"></button></span>' +
|
||||
'<span class="wolai-search-switch-control"><span>精确匹配</span><button type="button" class="wolai-search-switch" data-search-switch="exact" role="switch" aria-checked="false" aria-label="精确匹配"></button></span>' +
|
||||
'<span class="wolai-search-sort-control"><span>资料库模式</span><select class="wolai-search-sort-value" data-search-knowledge-mode aria-label="资料库检索模式"><option value="exact" selected>关键词</option><option value="mix">综合</option><option value="hybrid">图谱混合</option><option value="naive">向量</option><option value="local">实体</option><option value="global">关系</option></select></span>' +
|
||||
'<span class="wolai-search-sort-control"><span>知识库模式</span><select class="wolai-search-sort-value" data-search-knowledge-mode aria-label="知识库检索模式"><option value="exact" selected>关键词</option><option value="mix">综合</option><option value="hybrid">图谱混合</option><option value="naive">向量</option><option value="local">实体</option><option value="global">关系</option></select></span>' +
|
||||
'<span class="wolai-search-sort-control"><span>按编辑时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="updated" aria-label="按编辑时间范围">所有</button></span>' +
|
||||
'<span class="wolai-search-sort-control"><span>按创建时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="created" aria-label="按创建时间范围">所有</button></span>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-search-options-right">' +
|
||||
'<span class="wolai-search-switch-control"><span>全盘资料库</span><button type="button" class="wolai-search-switch is-on" data-search-switch="knowledge" role="switch" aria-checked="true" aria-label="全盘资料库检索"></button></span>' +
|
||||
'<span class="wolai-search-switch-control"><span>全盘知识库</span><button type="button" class="wolai-search-switch is-on" data-search-switch="knowledge" role="switch" aria-checked="true" aria-label="全盘知识库检索"></button></span>' +
|
||||
'<span class="wolai-search-switch-control"><span>页面内搜索</span><button type="button" class="wolai-search-switch" data-search-switch="page" role="switch" aria-checked="false" aria-label="页面内搜索"></button></span>' +
|
||||
'<span class="wolai-search-switch-control"><span>折叠同来源</span><button type="button" class="wolai-search-switch is-on" data-search-switch="collapseSource" role="switch" aria-checked="true" aria-label="默认折叠相同来源结果"></button></span>' +
|
||||
'</div>' +
|
||||
@@ -2703,7 +2703,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
var knowledgeMode = searchSwitchValue(overlay, 'knowledge');
|
||||
if (knowledgeMode && searchNonWhitespaceCharCount(query) < 2) {
|
||||
activeSearchRequestId += 1;
|
||||
meta.innerHTML = '<span>资料库检索</span><span>请输入至少 2 个字再搜索</span>';
|
||||
meta.innerHTML = '<span>知识库检索</span><span>请输入至少 2 个字再搜索</span>';
|
||||
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">请输入至少 2 个字再搜索</div>';
|
||||
window.__mnoteSearchResults = [];
|
||||
saveSearchUiState(overlay);
|
||||
@@ -2746,7 +2746,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
var payload = await response.json();
|
||||
if (requestId !== activeSearchRequestId) return;
|
||||
var items = Array.isArray(payload.results) ? payload.results : [];
|
||||
var ownerLabel = knowledgeMode ? ('资料库检索 · ' + knowledgeSearchModeLabel(payload.retrievalMode || knowledgeRequestMode)) : '工作区搜索';
|
||||
var ownerLabel = knowledgeMode ? ('知识库检索 · ' + knowledgeSearchModeLabel(payload.retrievalMode || knowledgeRequestMode)) : '工作区搜索';
|
||||
meta.innerHTML = '<span>' + ownerLabel + ' · 共 ' + items.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||||
if (!items.length) {
|
||||
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
|
||||
@@ -2988,6 +2988,13 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
|
||||
var knowledgeRagSettingsTrigger = closestAction(e.target, '[data-mnote-action="open-knowledge-rag-settings"]');
|
||||
if (knowledgeRagSettingsTrigger) {
|
||||
e.preventDefault();
|
||||
window.location.href = '/knowledge';
|
||||
return;
|
||||
}
|
||||
|
||||
var knowledgeRagDebugTrigger = closestAction(e.target, '[data-mnote-action="open-knowledge-rag-debug-settings"]');
|
||||
if (knowledgeRagDebugTrigger) {
|
||||
e.preventDefault();
|
||||
openKnowledgeRagSettingsPopover();
|
||||
return;
|
||||
@@ -3087,6 +3094,10 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
void ingestKnowledgeRagSources();
|
||||
return;
|
||||
}
|
||||
if (knowledgeRagActionName === 'ingest-active') {
|
||||
window.dispatchEvent(new CustomEvent('mnote:knowledge-rag-settings-action', { detail: { action: 'run-active' } }));
|
||||
return;
|
||||
}
|
||||
if (knowledgeRagActionName === 'reindex-source') {
|
||||
void ingestSingleKnowledgeRagSource(knowledgeRagAction.getAttribute('data-knowledge-rag-source-path') || '');
|
||||
return;
|
||||
@@ -3866,5 +3877,10 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
if (isPageSettingsOpen()) renderPageSettingsPopover();
|
||||
});
|
||||
restoreSidebarTreeTab();
|
||||
if (window.location.pathname === '/debug/knowledge-rag' || window.location.pathname === '/knowledge') {
|
||||
window.setTimeout(function() {
|
||||
openKnowledgeRagSettingsPopover();
|
||||
}, 0);
|
||||
}
|
||||
startLocalFolderSidebarWatch();
|
||||
})();
|
||||
|
||||
@@ -8,7 +8,9 @@ use axum::extract::Request;
|
||||
use axum::middleware::Next;
|
||||
use axum::response::Response;
|
||||
use axum::Router;
|
||||
use control_plane::{ControlPlaneStore, SqliteControlPlaneStore};
|
||||
use control_plane::ControlPlaneStore;
|
||||
#[cfg(test)]
|
||||
use control_plane::SqliteControlPlaneStore;
|
||||
#[cfg(not(test))]
|
||||
use control_plane::{TursoControlPlaneConfig, TursoControlPlaneMode, TursoControlPlaneStore};
|
||||
use std::env;
|
||||
@@ -201,7 +203,7 @@ impl AppState {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn open_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
||||
Arc::new(SqliteControlPlaneStore::in_memory().expect("初始化测试 SQLite 控制面"))
|
||||
Arc::new(SqliteControlPlaneStore::in_memory().expect("初始化测试 control-plane"))
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
@@ -210,9 +212,8 @@ pub(crate) fn open_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| "sqlite".to_string());
|
||||
.unwrap_or_else(|| "libsql-local".to_string());
|
||||
match backend.as_str() {
|
||||
"sqlite" => open_sqlite_control_plane_store(),
|
||||
"libsql-local" | "turso-local" | "turso" => open_turso_local_control_plane_store(),
|
||||
"turso-remote" => open_turso_remote_control_plane_store(),
|
||||
"turso-local-replica" | "turso-remote-replica" => {
|
||||
@@ -220,33 +221,16 @@ pub(crate) fn open_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
||||
}
|
||||
"turso-synced" => open_turso_synced_control_plane_store(),
|
||||
other => panic!(
|
||||
"不支持的控制面后端 {other},支持 sqlite/libsql-local/turso-remote/turso-local-replica/turso-synced"
|
||||
"不支持的控制面后端 {other},mnote-web 运行时只支持 libsql-local/turso-remote/turso-local-replica/turso-synced;SQLite 仅保留给 control-plane-admin 迁移/导出和测试"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn open_sqlite_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
||||
let db_path = env::var("MNOTE_CONTROL_PLANE_DB_PATH")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| "/mnt/Data1T/Mnote_data/control-plane/control-plane.db".to_string());
|
||||
if let Some(parent) = std::path::Path::new(&db_path).parent() {
|
||||
fs::create_dir_all(parent).expect("创建 SQLite 控制面目录");
|
||||
}
|
||||
Arc::new(SqliteControlPlaneStore::open(&db_path).expect("初始化 SQLite 控制面"))
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn open_turso_local_control_plane_store() -> Arc<dyn ControlPlaneStore> {
|
||||
let db_path = env::var("MNOTE_TURSO_LOCAL_PATH")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.or_else(|| {
|
||||
env::var("MNOTE_CONTROL_PLANE_DB_PATH")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
"/mnt/Data1T/Mnote_data/control-plane/control-plane-libsql.db".to_string()
|
||||
});
|
||||
@@ -412,7 +396,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_state_initializes_sqlite_control_plane_store() {
|
||||
fn app_state_initializes_control_plane_store_for_tests() {
|
||||
let state = AppState::new(test_config());
|
||||
|
||||
state
|
||||
|
||||
@@ -70,7 +70,7 @@ pub async fn query(
|
||||
let body = serde_json::from_value::<KnowledgeRagQueryRequest>(args).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"mnote_knowledge_rag_query_payload_invalid",
|
||||
format!("资料库问答参数无效: {error}"),
|
||||
format!("知识库问答参数无效: {error}"),
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
@@ -95,7 +95,7 @@ pub async fn open_reference(
|
||||
serde_json::from_value::<KnowledgeRagOpenReferenceRequest>(args).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"mnote_knowledge_rag_open_reference_payload_invalid",
|
||||
format!("资料库引用打开参数无效: {error}"),
|
||||
format!("知识库引用打开参数无效: {error}"),
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
@@ -119,7 +119,7 @@ pub async fn section_context(
|
||||
serde_json::from_value::<KnowledgeRagSectionContextRequest>(args).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"mnote_knowledge_rag_section_context_payload_invalid",
|
||||
format!("资料库章节上下文参数无效: {error}"),
|
||||
format!("知识库章节上下文参数无效: {error}"),
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
|
||||
@@ -57,10 +57,6 @@ 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(),
|
||||
@@ -359,7 +355,7 @@ fn knowledge_rag_status_tool() -> Value {
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.knowledge_rag.status",
|
||||
"description": "查看当前资料库 provider 状态、dashboard 地址、source registry 和同步状态。默认 provider 是 WeKnora;LightRAG 仅作为 legacy fallback。",
|
||||
"description": "查看当前知识库 provider 状态、dashboard 地址、source registry 和同步状态。默认 provider 是 RAGFlow;WeKnora/LightRAG 仅作为 legacy fallback。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
@@ -371,114 +367,6 @@ 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 {
|
||||
@@ -517,7 +405,7 @@ fn knowledge_rag_query_tool() -> Value {
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.knowledge_rag.query",
|
||||
"description": "向当前资料库 provider 提问,返回 provider 原始结果和经 MNote registry 映射、sourcePaths 后过滤的 references/citations。默认 provider 是 WeKnora,LightRAG 仅 legacy fallback。用户要求链接、来源、引用或证据时优先调用;返回的 citationMarkdown 由 MNote 前端自动追加成可点击来源定位,agent 不应在最终回答中手写 /documents、mnote:// 或搜索引擎包装链接。",
|
||||
"description": "向当前知识库 provider 提问,返回 provider 原始结果和经 MNote registry 映射、sourcePaths 后过滤的 references/citations。默认 provider 是 RAGFlow,WeKnora/LightRAG 仅 legacy fallback。用户要求链接、来源、引用或证据时优先调用;返回的 citationMarkdown 由 MNote 前端自动追加成可点击来源定位,agent 不应在最终回答中手写 /documents、mnote:// 或搜索引擎包装链接。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
|
||||
"status": "available",
|
||||
|
||||
@@ -37,8 +37,8 @@ const CAPABILITY_PACKS: &[MnoteCapabilityPack] = &[
|
||||
},
|
||||
MnoteCapabilityPack {
|
||||
id: "mnote-knowledge-rag",
|
||||
title: "资料库问答",
|
||||
description: "通过当前知识库 provider 检索多本书、论文、PDF 和附件资料库,并返回可回跳来源;默认 provider 是 WeKnora。",
|
||||
title: "知识库问答",
|
||||
description: "通过当前知识库 provider 检索多本书、论文、PDF 和附件,并返回可回跳来源;默认 provider 是 RAGFlow。",
|
||||
category: "knowledge",
|
||||
agent_ids: &["hermes", "reasonix"],
|
||||
read_only: true,
|
||||
@@ -46,10 +46,6 @@ 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",
|
||||
|
||||
@@ -134,10 +134,10 @@ pub async fn auth_api(
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web"));
|
||||
}
|
||||
if action == "auth:signOut" {
|
||||
return Ok(build_sqlite_sign_out_response(&state, &context));
|
||||
return Ok(build_control_plane_sign_out_response(&state, &context));
|
||||
}
|
||||
|
||||
handle_sqlite_auth_action(&state, &context, &payload).await
|
||||
handle_control_plane_auth_action(&state, &context, &payload).await
|
||||
}
|
||||
|
||||
pub async fn auth_entry(
|
||||
@@ -2070,7 +2070,7 @@ fn has_real_auth_context(state: &AppState, context: &RequestContext) -> bool {
|
||||
|| extract_cookie_value(context, COOKIE_MNOTE_WEB_CONVEX_TOKEN).is_some()
|
||||
}
|
||||
|
||||
async fn handle_sqlite_auth_action(
|
||||
async fn handle_control_plane_auth_action(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
payload: &serde_json::Value,
|
||||
@@ -2131,7 +2131,7 @@ async fn handle_sqlite_auth_action(
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.map_err(|error| sqlite_auth_error(context, error))?;
|
||||
.map_err(|error| control_plane_auth_error(context, error))?;
|
||||
state
|
||||
.control_plane()
|
||||
.create_password_identity(CreatePasswordIdentityInput {
|
||||
@@ -2140,11 +2140,11 @@ async fn handle_sqlite_auth_action(
|
||||
username: user.username.clone(),
|
||||
password: password.clone(),
|
||||
})
|
||||
.map_err(|error| sqlite_auth_error(context, error))?;
|
||||
.map_err(|error| control_plane_auth_error(context, error))?;
|
||||
state
|
||||
.control_plane()
|
||||
.ensure_default_workspace(&user.id)
|
||||
.map_err(|error| sqlite_auth_error(context, error))?;
|
||||
.map_err(|error| control_plane_auth_error(context, error))?;
|
||||
state
|
||||
.control_plane()
|
||||
.authenticate_password(AuthenticatePasswordInput {
|
||||
@@ -2156,7 +2156,7 @@ async fn handle_sqlite_auth_action(
|
||||
ip_hash: None,
|
||||
expires_at: None,
|
||||
})
|
||||
.map_err(|error| sqlite_auth_error(context, error))?
|
||||
.map_err(|error| control_plane_auth_error(context, error))?
|
||||
} else {
|
||||
let account = params
|
||||
.get("account")
|
||||
@@ -2181,7 +2181,7 @@ async fn handle_sqlite_auth_action(
|
||||
ip_hash: None,
|
||||
expires_at: None,
|
||||
})
|
||||
.map_err(|error| sqlite_auth_error(context, error))?
|
||||
.map_err(|error| control_plane_auth_error(context, error))?
|
||||
};
|
||||
|
||||
let audit_action = if flow == "signUp" {
|
||||
@@ -2202,7 +2202,7 @@ async fn handle_sqlite_auth_action(
|
||||
.unwrap_or_else(|_| "{}".to_string()),
|
||||
});
|
||||
|
||||
let provider_sync_results = if flow == "signUp" {
|
||||
let provider_sync_results = if matches!(flow, "signUp" | "signIn") {
|
||||
let directory_grants = state
|
||||
.control_plane()
|
||||
.list_directory_grants_for_actor(&resolved.user.id)
|
||||
@@ -2233,13 +2233,13 @@ async fn handle_sqlite_auth_action(
|
||||
None
|
||||
};
|
||||
|
||||
let mut response = build_sqlite_auth_response(
|
||||
let mut response = build_control_plane_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),
|
||||
&effective_control_plane_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);
|
||||
@@ -2253,7 +2253,7 @@ async fn handle_sqlite_auth_action(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn sqlite_auth_error(
|
||||
fn control_plane_auth_error(
|
||||
context: &RequestContext,
|
||||
error: control_plane::ControlPlaneError,
|
||||
) -> WebError {
|
||||
@@ -2273,7 +2273,7 @@ fn sqlite_auth_error(
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
}
|
||||
|
||||
fn build_sqlite_auth_response(
|
||||
fn build_control_plane_auth_response(
|
||||
context: &RequestContext,
|
||||
session_token: &str,
|
||||
user_id: &str,
|
||||
@@ -2286,7 +2286,7 @@ fn build_sqlite_auth_response(
|
||||
"userId": user_id,
|
||||
"email": email,
|
||||
"name": name,
|
||||
"authMode": "sqliteSession",
|
||||
"authMode": "controlPlaneSession",
|
||||
"actorType": actor_type
|
||||
}))
|
||||
.into_response();
|
||||
@@ -2307,7 +2307,7 @@ fn build_sqlite_auth_response(
|
||||
response
|
||||
}
|
||||
|
||||
fn effective_sqlite_auth_actor_type(user_id: &str, stored_role: &str) -> String {
|
||||
fn effective_control_plane_auth_actor_type(user_id: &str, stored_role: &str) -> String {
|
||||
let role = stored_role.trim();
|
||||
let fallback_role = if role.is_empty() { "user" } else { role };
|
||||
if crate::routes::local_folder_source::is_local_access_policy_admin_actor(
|
||||
@@ -2320,7 +2320,7 @@ fn effective_sqlite_auth_actor_type(user_id: &str, stored_role: &str) -> String
|
||||
}
|
||||
}
|
||||
|
||||
fn build_sqlite_sign_out_response(state: &AppState, context: &RequestContext) -> Response {
|
||||
fn build_control_plane_sign_out_response(state: &AppState, context: &RequestContext) -> Response {
|
||||
if let Some(raw_token) = extract_cookie_value(context, COOKIE_MNOTE_SESSION) {
|
||||
let token_hash = session_token_hash(&raw_token);
|
||||
if let Ok(Some(resolved)) = state.control_plane().get_session_by_token_hash(&token_hash) {
|
||||
@@ -3062,7 +3062,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn root_entry_uses_sqlite_session_display_name_for_workspace_label() {
|
||||
async fn root_entry_uses_control_plane_session_display_name_for_workspace_label() {
|
||||
use control_plane::{session_token_hash, CreateSessionInput, UpsertUserInput};
|
||||
|
||||
let app_state = AppState::new(AppConfig {
|
||||
@@ -3095,7 +3095,7 @@ mod tests {
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert sqlite user");
|
||||
.expect("upsert control-plane user");
|
||||
app_state
|
||||
.control_plane()
|
||||
.create_session(CreateSessionInput {
|
||||
@@ -3106,7 +3106,7 @@ mod tests {
|
||||
ip_hash: None,
|
||||
expires_at: None,
|
||||
})
|
||||
.expect("create sqlite session");
|
||||
.expect("create control-plane session");
|
||||
|
||||
let response = build_app(app_state)
|
||||
.oneshot(
|
||||
@@ -3745,7 +3745,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_api_signup_sets_sqlite_session_cookie_when_compat_disabled() {
|
||||
async fn auth_api_signup_sets_control_plane_session_cookie_when_compat_disabled() {
|
||||
let response = app_with_config("http://127.0.0.1:3100".into(), false)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
@@ -3786,11 +3786,11 @@ mod tests {
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["userId"], "new-user");
|
||||
assert_eq!(payload["email"], "new-user@example.com");
|
||||
assert_eq!(payload["authMode"], "sqliteSession");
|
||||
assert_eq!(payload["authMode"], "controlPlaneSession");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_api_signin_accepts_sqlite_username_after_signup() {
|
||||
async fn auth_api_signin_accepts_control_plane_username_after_signup() {
|
||||
let app = app_with_config("http://127.0.0.1:3100".into(), false);
|
||||
let signup = app
|
||||
.clone()
|
||||
@@ -3829,7 +3829,7 @@ mod tests {
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["userId"], "mnote-e2e");
|
||||
assert_eq!(payload["email"], "mnote.e2e@example.com");
|
||||
assert_eq!(payload["authMode"], "sqliteSession");
|
||||
assert_eq!(payload["authMode"], "controlPlaneSession");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -3865,7 +3865,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_api_signout_clears_sqlite_session_cookie() {
|
||||
async fn auth_api_signout_clears_control_plane_session_cookie() {
|
||||
let response = app_with_config("http://127.0.0.1:3100".into(), false)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
|
||||
@@ -34,7 +34,7 @@ use tracing::{info, warn};
|
||||
|
||||
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||||
const HEADER_HERMES_CLIENT_OWNER: &str = "x-mnote-hermes-client-owner";
|
||||
const ACP_RUNTIME_SQLITE_STORE: &str = "sqlite_acp_runtime_store";
|
||||
const ACP_RUNTIME_CONTROL_PLANE_STORE: &str = "control_plane_acp_runtime_store";
|
||||
const ACP_ABORT_NOTIFICATION_TIMEOUT_MS: u64 = 2_500;
|
||||
const LOCAL_SHARE_GRANTS_JSON: &str = "/mnt/Data1T/Mnote_data/control-plane/share-grants.json";
|
||||
const ENV_LOCAL_SHARE_GRANTS_FILE: &str = "MNOTE_SHARE_GRANTS_FILE";
|
||||
@@ -493,7 +493,7 @@ pub async fn search_sessions(
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"traceId": context.trace.trace_id,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"results": results
|
||||
})),
|
||||
));
|
||||
@@ -590,24 +590,24 @@ async fn list_acp_sessions(
|
||||
.map(String::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
let sqlite_runs = state
|
||||
let control_plane_runs = state
|
||||
.control_plane()
|
||||
.list_ai_runtime_runs(&user_id, workspace_id, document_id, session_id, limit)
|
||||
.map_err(|error| {
|
||||
WebError::internal(format!("SQLite ACP session 列表读取失败: {error}"))
|
||||
.with_context(context)
|
||||
})?;
|
||||
if !sqlite_runs.is_empty() {
|
||||
if !control_plane_runs.is_empty() {
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
stamp_client_headers(),
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"traceId": context.trace.trace_id,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"sessionStorage": "sqlite_control_plane",
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessionStorage": "control_plane",
|
||||
"legacyPersistence": "local_ai_session_jsonl",
|
||||
"sessions": sqlite_runs.iter().map(ai_runtime_run_to_json).collect::<Vec<_>>()
|
||||
"sessions": control_plane_runs.iter().map(ai_runtime_run_to_json).collect::<Vec<_>>()
|
||||
})),
|
||||
));
|
||||
}
|
||||
@@ -669,7 +669,7 @@ async fn list_acp_sessions(
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"traceId": context.trace.trace_id,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessions": runs.iter().map(ai_runtime_run_to_json).collect::<Vec<_>>()
|
||||
})),
|
||||
));
|
||||
@@ -897,8 +897,8 @@ pub async fn create_session(
|
||||
"profile": profile,
|
||||
"title": payload.title.unwrap_or_else(|| "当前页问答".into()),
|
||||
"traceId": trace_id,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"sessionStorage": "sqlite_control_plane",
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessionStorage": "control_plane",
|
||||
"legacyPersistence": "local_ai_session_jsonl",
|
||||
"legacySessionStorage": session_storage,
|
||||
"permissionLevel": permission_level,
|
||||
@@ -973,8 +973,8 @@ pub async fn create_session(
|
||||
"providerKind": "api-chat",
|
||||
"title": payload.title.unwrap_or_else(|| "当前页问答".into()),
|
||||
"traceId": trace_id,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"sessionStorage": "sqlite_control_plane"
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessionStorage": "control_plane"
|
||||
})),
|
||||
));
|
||||
}
|
||||
@@ -1004,7 +1004,7 @@ pub async fn create_session(
|
||||
&runtime_payload,
|
||||
)
|
||||
.await?;
|
||||
persistence = ACP_RUNTIME_SQLITE_STORE;
|
||||
persistence = ACP_RUNTIME_CONTROL_PLANE_STORE;
|
||||
}
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
@@ -1450,7 +1450,7 @@ pub async fn toggle_skill(
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
stamp_client_headers(),
|
||||
Json(json!({"ok": true, "skillKind": "mnote_builtin", "configScope": "user_sqlite"})),
|
||||
Json(json!({"ok": true, "skillKind": "mnote_builtin", "configScope": "user_control_plane"})),
|
||||
));
|
||||
}
|
||||
let access =
|
||||
@@ -2220,7 +2220,7 @@ pub async fn toggle_capability(
|
||||
"id": capability_id,
|
||||
"enabled": enabled,
|
||||
"profile": profile,
|
||||
"configScope": "user_sqlite+profile_tool_policy"
|
||||
"configScope": "user_control_plane+profile_tool_policy"
|
||||
})),
|
||||
))
|
||||
}
|
||||
@@ -2324,7 +2324,7 @@ pub async fn resume_session(
|
||||
payload["resumeSource"] = payload
|
||||
.get("persistence")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::String(ACP_RUNTIME_SQLITE_STORE.into()));
|
||||
.unwrap_or_else(|| Value::String(ACP_RUNTIME_CONTROL_PLANE_STORE.into()));
|
||||
return Ok((result.0, result.1, Json(payload)));
|
||||
}
|
||||
get_session(
|
||||
@@ -2422,7 +2422,7 @@ pub async fn delete_session(
|
||||
stamp_client_headers(),
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"result": {
|
||||
"ok": true,
|
||||
"sessionId": session_id,
|
||||
@@ -2494,7 +2494,7 @@ pub async fn rename_session(
|
||||
stamp_client_headers(),
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"result": {
|
||||
"ok": true,
|
||||
"sessionId": session_id,
|
||||
@@ -2579,7 +2579,7 @@ pub async fn auto_title_session(
|
||||
stamp_client_headers(),
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"result": {
|
||||
"ok": true,
|
||||
"sessionId": session_id,
|
||||
@@ -2720,8 +2720,8 @@ async fn get_acp_session(
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"traceId": context.trace.trace_id,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"sessionStorage": "sqlite_control_plane",
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessionStorage": "control_plane",
|
||||
"legacyPersistence": "local_ai_session_jsonl",
|
||||
"sessionId": session_id,
|
||||
"session": {
|
||||
@@ -2804,7 +2804,7 @@ async fn get_acp_session(
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"traceId": context.trace.trace_id,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessionId": session_id,
|
||||
"session": {
|
||||
"sessionId": session_id,
|
||||
@@ -2983,7 +2983,7 @@ pub async fn create_run(
|
||||
"persistence": persistence_result
|
||||
.get("persistence")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or(ACP_RUNTIME_SQLITE_STORE),
|
||||
.unwrap_or(ACP_RUNTIME_CONTROL_PLANE_STORE),
|
||||
"sessionStorage": persistence_result
|
||||
.get("sessionStorage")
|
||||
.cloned()
|
||||
@@ -3050,7 +3050,7 @@ pub async fn create_run(
|
||||
"persistence": persistence_result
|
||||
.get("persistence")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or(ACP_RUNTIME_SQLITE_STORE),
|
||||
.unwrap_or(ACP_RUNTIME_CONTROL_PLANE_STORE),
|
||||
"sessionStorage": persistence_result
|
||||
.get("sessionStorage")
|
||||
.cloned()
|
||||
@@ -4997,7 +4997,7 @@ fn stamp_mnote_builtin_skill_payload_policy(
|
||||
Value::Bool(ai_preference_bool(state, &actor_id, &key, true)?);
|
||||
skill["builtin"] = Value::Bool(true);
|
||||
skill["configurable"] = Value::Bool(true);
|
||||
skill["configScope"] = Value::String("user_sqlite".to_string());
|
||||
skill["configScope"] = Value::String("user_control_plane".to_string());
|
||||
skill["skillKind"] = Value::String("mnote_builtin".to_string());
|
||||
}
|
||||
}
|
||||
@@ -5411,7 +5411,7 @@ fn mnote_capabilities_payload(
|
||||
"toggleable": skill.get("toggleable").cloned().unwrap_or_else(|| json!(true)),
|
||||
"builtin": true,
|
||||
"configurable": true,
|
||||
"configScope": "user_sqlite+profile_tool_policy",
|
||||
"configScope": "user_control_plane+profile_tool_policy",
|
||||
"skillKind": "mnote_capability",
|
||||
"source": "mnote",
|
||||
"origin": "builtin",
|
||||
@@ -6319,7 +6319,7 @@ fn mnote_builtin_skills_payload(agent_id: Option<&str>) -> Value {
|
||||
"toggleable": true,
|
||||
"builtin": true,
|
||||
"configurable": true,
|
||||
"configScope": "user_sqlite",
|
||||
"configScope": "user_control_plane",
|
||||
"skillKind": "mnote_builtin",
|
||||
"source": "mnote",
|
||||
"origin": "builtin",
|
||||
@@ -6853,7 +6853,7 @@ fn enforce_local_ai_run_access(
|
||||
payload["allowedRoots"] = json!([{
|
||||
"rootUri": root_uri,
|
||||
"permission": if chat_only { "read" } else { "write" },
|
||||
"source": "sqlite_directory_grant",
|
||||
"source": "control_plane_directory_grant",
|
||||
"grantIds": access.grant_ids
|
||||
}]);
|
||||
Ok(())
|
||||
@@ -9903,7 +9903,7 @@ fn ai_runtime_run_to_json(record: &control_plane::AiRuntimeRunRecord) -> Value {
|
||||
"payload": payload,
|
||||
"createdAt": record.created_at,
|
||||
"updatedAt": record.updated_at,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10078,7 +10078,7 @@ fn ai_runtime_event_to_json(record: &control_plane::AiRuntimeEventRecord) -> Val
|
||||
"eventType": record.event_type,
|
||||
"payload": payload,
|
||||
"createdAt": record.created_at,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10210,7 +10210,7 @@ fn page_ai_run_to_journal_json(record: &control_plane::AiRuntimeRunRecord) -> Va
|
||||
"payload": payload,
|
||||
"createdAt": record.created_at,
|
||||
"updatedAt": record.updated_at,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10587,7 +10587,7 @@ fn page_ai_journal_event_to_json(record: &control_plane::AiRuntimeJournalEventRe
|
||||
"acpRuntime": event.acp_runtime,
|
||||
"payload": payload,
|
||||
"createdAt": event.created_at,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10615,7 +10615,7 @@ fn page_ai_synthetic_terminal_event_to_json(
|
||||
"reason": "terminal_status_without_terminal_event"
|
||||
},
|
||||
"createdAt": run.updated_at.clone(),
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"synthetic": true,
|
||||
})
|
||||
}
|
||||
@@ -11645,8 +11645,8 @@ async fn persist_acp_runtime_run(
|
||||
})?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"sessionStorage": "sqlite_control_plane",
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessionStorage": "control_plane",
|
||||
"legacyPersistence": legacy_persistence,
|
||||
"legacySessionStorage": legacy_session_storage,
|
||||
"sessionId": record.session_id,
|
||||
@@ -11824,8 +11824,8 @@ async fn persist_acp_runtime_event(
|
||||
}
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"persistence": ACP_RUNTIME_SQLITE_STORE,
|
||||
"sessionStorage": "sqlite_control_plane",
|
||||
"persistence": ACP_RUNTIME_CONTROL_PLANE_STORE,
|
||||
"sessionStorage": "control_plane",
|
||||
"legacyPersistence": legacy_persistence,
|
||||
"legacySessionStorage": legacy_session_storage,
|
||||
"sessionId": registration.session_id,
|
||||
@@ -12636,7 +12636,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn page_ai_agent_profiles_are_sqlite_user_scoped() {
|
||||
async fn page_ai_agent_profiles_are_control_plane_user_scoped() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
@@ -12743,7 +12743,7 @@ mod tests {
|
||||
.find(|skill| skill["id"] == "mnote-current-page")
|
||||
.expect("current page skill");
|
||||
assert_eq!(current_page["enabled"], false);
|
||||
assert_eq!(current_page["configScope"], "user_sqlite");
|
||||
assert_eq!(current_page["configScope"], "user_control_plane");
|
||||
let mindmap_skill = payload["categories"]
|
||||
.as_array()
|
||||
.expect("categories")
|
||||
@@ -14830,7 +14830,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_chat_create_session_persists_sqlite_without_acp_session() {
|
||||
async fn api_chat_create_session_persists_control_plane_without_acp_session() {
|
||||
let _runtime_guard = runtime_lock().lock().expect("runtime lock");
|
||||
clear_runtime_registry();
|
||||
clear_acp_run_payloads();
|
||||
@@ -14861,8 +14861,8 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "sqlite_control_plane");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "control_plane");
|
||||
assert_eq!(payload["providerKind"], "api-chat");
|
||||
assert_eq!(payload["profile"], "api-gpt-chat");
|
||||
}
|
||||
@@ -14998,7 +14998,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_chat_session_detail_restores_messages_from_sqlite_events() {
|
||||
async fn api_chat_session_detail_restores_messages_from_control_plane_events() {
|
||||
let _env_guard = env_lock().lock().expect("env lock");
|
||||
let _runtime_guard = runtime_lock().lock().expect("runtime lock");
|
||||
clear_runtime_registry();
|
||||
@@ -15855,12 +15855,12 @@ mod tests {
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["ok"], true);
|
||||
assert_eq!(payload["sessionId"], "mnote_doc_1_trace_1");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert!(payload.get("messages").is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_local_acp_session_create_writes_sqlite_and_private_jsonl() {
|
||||
async fn hermes_client_local_acp_session_create_writes_control_plane_and_private_jsonl() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-ai-session-private-{}",
|
||||
std::process::id()
|
||||
@@ -15931,8 +15931,8 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "sqlite_control_plane");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "control_plane");
|
||||
assert_eq!(payload["legacyPersistence"], "local_ai_session_jsonl");
|
||||
assert_eq!(payload["legacySessionStorage"], "local_private");
|
||||
let session_id = payload["sessionId"].as_str().expect("session id");
|
||||
@@ -15965,8 +15965,8 @@ mod tests {
|
||||
.await
|
||||
.expect("list body");
|
||||
let list_payload: Value = serde_json::from_slice(&list_body).expect("list json");
|
||||
assert_eq!(list_payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(list_payload["sessionStorage"], "sqlite_control_plane");
|
||||
assert_eq!(list_payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(list_payload["sessionStorage"], "control_plane");
|
||||
assert_eq!(list_payload["sessions"][0]["sessionId"], session_id);
|
||||
|
||||
let detail_uri = format!(
|
||||
@@ -15991,7 +15991,7 @@ mod tests {
|
||||
.await
|
||||
.expect("detail body");
|
||||
let detail_payload: Value = serde_json::from_slice(&detail_body).expect("detail json");
|
||||
assert_eq!(detail_payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(detail_payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(
|
||||
detail_payload["session"]["runs"][0]["sessionId"],
|
||||
session_id
|
||||
@@ -16001,7 +16001,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_local_acp_run_writes_sqlite_and_private_jsonl_without_convex() {
|
||||
async fn hermes_client_local_acp_run_writes_control_plane_and_private_jsonl_without_convex() {
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("mnote-local-ai-run-private-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
@@ -16069,8 +16069,8 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "sqlite_control_plane");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "control_plane");
|
||||
assert_eq!(payload["legacyPersistence"], "local_ai_session_jsonl");
|
||||
assert_eq!(payload["legacySessionStorage"], "local_private");
|
||||
let jsonl_path = root
|
||||
@@ -16085,9 +16085,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_local_acp_session_create_accepts_sqlite_directory_grant() {
|
||||
async fn hermes_client_local_acp_session_create_accepts_control_plane_directory_grant() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-ai-session-sqlite-grant-{}",
|
||||
"mnote-local-ai-session-control-plane-grant-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
@@ -16136,7 +16136,7 @@ mod tests {
|
||||
"documentId": "local-md:README.md",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"traceId": "trace_sqlite_grant_session_1",
|
||||
"traceId": "trace_control_plane_grant_session_1",
|
||||
"profile": "reasonix",
|
||||
"title": "授权本地会话"
|
||||
})
|
||||
@@ -16152,8 +16152,8 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "sqlite_control_plane");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(payload["sessionStorage"], "control_plane");
|
||||
assert_eq!(payload["legacyPersistence"], "local_ai_session_jsonl");
|
||||
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
@@ -16234,7 +16234,7 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["sessionStorage"], "sqlite_control_plane");
|
||||
assert_eq!(payload["sessionStorage"], "control_plane");
|
||||
assert_eq!(payload["legacySessionStorage"], "local_shared");
|
||||
let session_id = payload["sessionId"].as_str().expect("session id");
|
||||
let jsonl_path = root
|
||||
@@ -16435,7 +16435,7 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
|
||||
assert_eq!(
|
||||
captured_body.lock().expect("captured convex body").clone(),
|
||||
@@ -16444,7 +16444,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_acp_run_registers_scoped_runtime_record_in_sqlite() {
|
||||
async fn hermes_client_acp_run_registers_scoped_runtime_record_in_control_plane() {
|
||||
let _guard = runtime_lock().lock().expect("runtime lock");
|
||||
clear_runtime_registry();
|
||||
clear_run_queue();
|
||||
@@ -16482,7 +16482,7 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
|
||||
let response = router
|
||||
.oneshot(
|
||||
@@ -16500,7 +16500,7 @@ mod tests {
|
||||
.await
|
||||
.expect("list body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
let run = &payload["sessions"][0];
|
||||
assert_eq!(run["sessionId"], "sess_1");
|
||||
assert_eq!(run["runId"], "run_trace_1");
|
||||
@@ -16753,7 +16753,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_acp_session_rename_uses_sqlite_store() {
|
||||
async fn hermes_client_acp_session_rename_uses_control_plane_store() {
|
||||
let response = build_app(seeded_acp_state())
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
@@ -16772,13 +16772,13 @@ mod tests {
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["ok"], true);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(payload["result"]["title"], "新标题");
|
||||
assert_eq!(payload["result"]["runs"][0]["sessionId"], "sess_1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_acp_session_export_uses_sqlite_store() {
|
||||
async fn hermes_client_acp_session_export_uses_control_plane_store() {
|
||||
let response = build_app(seeded_acp_state())
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
@@ -16796,7 +16796,7 @@ mod tests {
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["ok"], true);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(
|
||||
payload["export"]["schema"],
|
||||
"mnote.page_ai_session_export.v1"
|
||||
@@ -16818,7 +16818,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_client_acp_session_delete_and_auto_title_use_sqlite_store() {
|
||||
async fn hermes_client_acp_session_delete_and_auto_title_use_control_plane_store() {
|
||||
let router = build_app(seeded_acp_state());
|
||||
|
||||
let response = router
|
||||
@@ -16840,7 +16840,7 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(payload["result"]["title"], "自动标题");
|
||||
|
||||
let response = router
|
||||
@@ -16859,7 +16859,7 @@ mod tests {
|
||||
.await
|
||||
.expect("delete body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_SQLITE_STORE);
|
||||
assert_eq!(payload["persistence"], ACP_RUNTIME_CONTROL_PLANE_STORE);
|
||||
assert_eq!(payload["result"]["deleted"], 1);
|
||||
}
|
||||
|
||||
@@ -17687,7 +17687,7 @@ mod tests {
|
||||
"rootUri": "file:///mnt/Data1T/Mnote_data/users/user_1/我的空间",
|
||||
"permission": "write",
|
||||
"recursive": true,
|
||||
"source": "sqlite_directory_grant",
|
||||
"source": "control_plane_directory_grant",
|
||||
"absolutePath": "/mnt/Data1T/Mnote_data/users/user_1/我的空间"
|
||||
}],
|
||||
"editorTarget": {
|
||||
@@ -18069,7 +18069,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_ai_run_access_rewrites_allowed_roots_from_sqlite_grant() {
|
||||
fn local_ai_run_access_rewrites_allowed_roots_from_control_plane_grant() {
|
||||
let state = test_state();
|
||||
state
|
||||
.control_plane()
|
||||
@@ -18107,7 +18107,7 @@ mod tests {
|
||||
let mut payload = json!({
|
||||
"workspaceId": "local-ws:alice:test",
|
||||
"documentId": "local-md:README.md",
|
||||
"sessionId": "sess_sqlite_roots",
|
||||
"sessionId": "sess_control_plane_roots",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": "file:///tmp/mnote-ai-allowed",
|
||||
"allowedRoots": [{"rootUri": "file:///tmp/evil", "permission": "write", "source": "client"}]
|
||||
@@ -18124,7 +18124,7 @@ mod tests {
|
||||
assert_eq!(payload["allowedRoots"][0]["permission"], "write");
|
||||
assert_eq!(
|
||||
payload["allowedRoots"][0]["source"],
|
||||
"sqlite_directory_grant"
|
||||
"control_plane_directory_grant"
|
||||
);
|
||||
assert!(payload["allowedRoots"][0]["grantIds"]
|
||||
.as_array()
|
||||
@@ -18134,7 +18134,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_ai_run_access_rejects_other_user_without_sqlite_grant() {
|
||||
fn local_ai_run_access_rejects_other_user_without_control_plane_grant() {
|
||||
let state = test_state();
|
||||
state
|
||||
.control_plane()
|
||||
|
||||
@@ -368,21 +368,20 @@ pub(crate) async fn execute_mnote_tool_call(
|
||||
Err(WebError::new(
|
||||
StatusCode::GONE,
|
||||
"mnote_evidence_tools_retired",
|
||||
"旧 docs/evidence/LiteParse tools 已退役;请使用 mnote.weknora.search/open_reference 或兼容 mnote.knowledge_rag.query/open_reference",
|
||||
"旧 docs/evidence/LiteParse tools 已退役;请使用兼容 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.weknora.search"
|
||||
| "mnote.weknora.list_sources"
|
||||
| "mnote.weknora.get_source_status"
|
||||
| "mnote.weknora.open_reference" => Err(WebError::new(
|
||||
StatusCode::GONE,
|
||||
"mnote_weknora_tools_retired",
|
||||
"WeKnora 专用工具已从默认 agent manifest 移除;请改用 provider-neutral mnote.knowledge_rag.*,或显式启动 legacy WeKnora provider 调试。",
|
||||
)
|
||||
.with_context(&context)),
|
||||
"mnote.knowledge_rag.query" => knowledge_rag::query(&state, &context, &input).await,
|
||||
"mnote.knowledge_rag.section_context" => {
|
||||
knowledge_rag::section_context(&state, &context, &input).await
|
||||
@@ -738,10 +737,6 @@ 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"
|
||||
@@ -775,10 +770,6 @@ 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
@@ -264,11 +264,14 @@ fn build_local_folder_watch_batch_payload(
|
||||
root_uri.hash(&mut hasher);
|
||||
workspace_id.hash(&mut hasher);
|
||||
for payload in watcher_payloads {
|
||||
let relative_path = payload
|
||||
let Some(relative_path) = payload
|
||||
.get("relativePath")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let event_kind = payload
|
||||
.get("eventKind")
|
||||
.and_then(Value::as_str)
|
||||
@@ -286,7 +289,36 @@ fn build_local_folder_watch_batch_payload(
|
||||
changed_paths.push(json!({
|
||||
"relativePath": relative_path,
|
||||
"kind": event_kind,
|
||||
"eventKind": event_kind,
|
||||
"changeType": event_kind,
|
||||
"revision": event_revision,
|
||||
"documentId": payload.get("documentId").cloned().unwrap_or(Value::Null),
|
||||
"sourceKind": payload.get("sourceKind").cloned().unwrap_or(Value::Null),
|
||||
"observedFileVersion": payload
|
||||
.get("observedFileVersion")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null),
|
||||
"bufferFileVersion": payload
|
||||
.get("bufferFileVersion")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null),
|
||||
"fileVersion": payload
|
||||
.get("observedFileVersion")
|
||||
.cloned()
|
||||
.or_else(|| payload.get("bufferFileVersion").cloned())
|
||||
.unwrap_or(Value::Null),
|
||||
"lastWriteIntentId": payload
|
||||
.get("lastWriteIntentId")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null),
|
||||
"lastSaveOperationId": payload
|
||||
.get("lastSaveOperationId")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null),
|
||||
"selfWriteEcho": payload
|
||||
.get("selfWriteEcho")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
}));
|
||||
}
|
||||
if seen_kinds.insert(event_kind.to_string()) {
|
||||
@@ -655,7 +687,13 @@ mod tests {
|
||||
vec![
|
||||
json!({
|
||||
"relativePath": "docs/README.md",
|
||||
"documentId": "local-md:docs~2FREADME.md",
|
||||
"eventKind": "Modify(Data)",
|
||||
"observedFileVersion": "sha256:observed",
|
||||
"bufferFileVersion": "sha256:buffer",
|
||||
"lastWriteIntentId": "write-1",
|
||||
"lastSaveOperationId": "save-1",
|
||||
"selfWriteEcho": true,
|
||||
}),
|
||||
json!({
|
||||
"relativePath": "docs/New.md",
|
||||
@@ -671,6 +709,15 @@ mod tests {
|
||||
assert_eq!(payload["watchRevision"]["scope"], "changed_paths");
|
||||
assert_eq!(payload["watchRevision"]["entryCount"], 2);
|
||||
assert_eq!(payload["changedPaths"].as_array().map(Vec::len), Some(2));
|
||||
let first_path = &payload["changedPaths"][0];
|
||||
assert_eq!(first_path["documentId"], "local-md:docs~2FREADME.md");
|
||||
assert_eq!(first_path["eventKind"], "Modify(Data)");
|
||||
assert_eq!(first_path["observedFileVersion"], "sha256:observed");
|
||||
assert_eq!(first_path["bufferFileVersion"], "sha256:buffer");
|
||||
assert_eq!(first_path["fileVersion"], "sha256:observed");
|
||||
assert_eq!(first_path["lastWriteIntentId"], "write-1");
|
||||
assert_eq!(first_path["lastSaveOperationId"], "save-1");
|
||||
assert_eq!(first_path["selfWriteEcho"], true);
|
||||
assert!(
|
||||
payload["affectedParents"]
|
||||
.as_array()
|
||||
|
||||
@@ -658,7 +658,7 @@ pub(crate) fn ensure_local_workspace_access_with_state(
|
||||
}
|
||||
let actor_id = context.auth.actor_id.trim();
|
||||
let canonical_root_uri = file_uri_for_path(&canonical_root);
|
||||
let sqlite_access = state
|
||||
let control_plane_access = state
|
||||
.control_plane()
|
||||
.resolve_access(actor_id, &canonical_root_uri)
|
||||
.unwrap_or_else(|error| {
|
||||
@@ -675,7 +675,7 @@ pub(crate) fn ensure_local_workspace_access_with_state(
|
||||
grant_ids: Vec::new(),
|
||||
}
|
||||
});
|
||||
if local_access_permission_allows(&sqlite_access.permission, mode) {
|
||||
if local_access_permission_allows(&control_plane_access.permission, mode) {
|
||||
return Ok(canonical_root);
|
||||
}
|
||||
ensure_local_workspace_access_for_actor_with_mode(
|
||||
@@ -785,22 +785,8 @@ pub(crate) fn control_plane_status_display() -> String {
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| "sqlite".to_string());
|
||||
.unwrap_or_else(|| "libsql-local".to_string());
|
||||
match backend.as_str() {
|
||||
"sqlite" => {
|
||||
let db_path = std::env::var("MNOTE_CONTROL_PLANE_DB_PATH")
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| {
|
||||
default_local_workspace_base_dir()
|
||||
.join("control-plane")
|
||||
.join("control-plane.db")
|
||||
.display()
|
||||
.to_string()
|
||||
});
|
||||
format!("backend=sqlite; db={db_path}")
|
||||
}
|
||||
"libsql-local" | "turso-local" | "turso" => {
|
||||
let db_path = std::env::var("MNOTE_TURSO_LOCAL_PATH")
|
||||
.ok()
|
||||
@@ -850,6 +836,7 @@ pub(crate) fn control_plane_status_display() -> String {
|
||||
});
|
||||
format!("backend=turso-synced; local={db_path}; remote=env:MNOTE_TURSO_DATABASE_URL")
|
||||
}
|
||||
"sqlite" => "backend=sqlite; unsupported-runtime; use control-plane-admin only".to_string(),
|
||||
other => format!("backend={other}; unsupported"),
|
||||
}
|
||||
}
|
||||
@@ -1211,7 +1198,7 @@ fn append_control_plane_outbox_event(state: &AppState, event_type: &str, payload
|
||||
let _ = state.stream_delta_tx.send(delta);
|
||||
}
|
||||
|
||||
fn list_sqlite_share_links_for_context(
|
||||
fn list_control_plane_share_links_for_context(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
query: ShareLinkListQuery,
|
||||
@@ -1227,15 +1214,15 @@ fn list_sqlite_share_links_for_context(
|
||||
let links = state
|
||||
.control_plane()
|
||||
.list_share_links(workspace_id)
|
||||
.map_err(|error| WebError::internal(format!("SQLite 分享链接读取失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane 分享链接读取失败: {error}")))?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"links": links.iter().map(share_link_payload).collect::<Vec<_>>(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn create_sqlite_share_link_for_context(
|
||||
fn create_control_plane_share_link_for_context(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
request: LocalShareLinkRequest,
|
||||
@@ -1263,7 +1250,7 @@ fn create_sqlite_share_link_for_context(
|
||||
created_by: context.auth.actor_id.trim().to_string(),
|
||||
expires_at: request.expires_at,
|
||||
})
|
||||
.map_err(|error| WebError::internal(format!("SQLite 分享链接写入失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane 分享链接写入失败: {error}")))?;
|
||||
let _ = state.control_plane().append_audit(AppendAuditInput {
|
||||
actor_user_id: Some(context.auth.actor_id.trim().to_string()),
|
||||
action: "control.share.created".to_string(),
|
||||
@@ -1286,13 +1273,13 @@ fn create_sqlite_share_link_for_context(
|
||||
);
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"token": created.token,
|
||||
"link": share_link_payload(&created.link),
|
||||
}))
|
||||
}
|
||||
|
||||
fn revoke_sqlite_share_link_for_context(
|
||||
fn revoke_control_plane_share_link_for_context(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
link_id: &str,
|
||||
@@ -1312,7 +1299,7 @@ fn revoke_sqlite_share_link_for_context(
|
||||
control_plane::ControlPlaneError::NotFound(message) => {
|
||||
WebError::new(StatusCode::NOT_FOUND, "share_link_not_found", message)
|
||||
}
|
||||
other => WebError::internal(format!("SQLite 分享链接撤销失败: {other}")),
|
||||
other => WebError::internal(format!("control-plane 分享链接撤销失败: {other}")),
|
||||
})?;
|
||||
let _ = state.control_plane().append_audit(AppendAuditInput {
|
||||
actor_user_id: Some(context.auth.actor_id.trim().to_string()),
|
||||
@@ -1330,7 +1317,7 @@ fn revoke_sqlite_share_link_for_context(
|
||||
);
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"revokedLinkId": link_id,
|
||||
}))
|
||||
}
|
||||
@@ -1879,7 +1866,7 @@ fn is_default_workspace_auto_grant(grant: &DirectoryGrantRecord) -> bool {
|
||||
&& grant.root_uri.ends_with("/workspaces/my-space")
|
||||
}
|
||||
|
||||
fn sqlite_access_policy_payload(
|
||||
fn control_plane_access_policy_payload(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
) -> Result<Value, WebError> {
|
||||
@@ -1887,14 +1874,14 @@ fn sqlite_access_policy_payload(
|
||||
let grants = state
|
||||
.control_plane()
|
||||
.list_directory_grants()
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权列表读取失败: {error}")))?;
|
||||
let grant_values = grants
|
||||
.iter()
|
||||
.map(control_plane_grant_payload)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"policyPath": local_access_policy_path().display().to_string(),
|
||||
"policy": {
|
||||
"admins": policy.admins,
|
||||
@@ -1907,7 +1894,7 @@ fn sqlite_access_policy_payload(
|
||||
}))
|
||||
}
|
||||
|
||||
fn sqlite_user_access_policy_payload(
|
||||
fn control_plane_user_access_policy_payload(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
) -> Result<Value, WebError> {
|
||||
@@ -1922,7 +1909,7 @@ fn sqlite_user_access_policy_payload(
|
||||
let grants = state
|
||||
.control_plane()
|
||||
.list_directory_grants()
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权列表读取失败: {error}")))?;
|
||||
let grant_values = grants
|
||||
.iter()
|
||||
.filter(|grant| {
|
||||
@@ -1933,7 +1920,7 @@ fn sqlite_user_access_policy_payload(
|
||||
.collect::<Vec<_>>();
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"policy": {
|
||||
"grants": grant_values,
|
||||
},
|
||||
@@ -1956,7 +1943,7 @@ fn validate_local_access_root_for_context(
|
||||
}))
|
||||
}
|
||||
|
||||
fn add_sqlite_user_access_grant_for_context(
|
||||
fn add_control_plane_user_access_grant_for_context(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
mut request: LocalAccessGrantRequest,
|
||||
@@ -1973,19 +1960,19 @@ fn add_sqlite_user_access_grant_for_context(
|
||||
ensure_path_inside_owned_local_workspace(actor_id, &canonical)?;
|
||||
request.root_uri = file_uri_for_path(&canonical);
|
||||
request.root_path = canonical.display().to_string();
|
||||
add_sqlite_local_access_grant_for_context_inner(state, context, request, "user")
|
||||
add_control_plane_local_access_grant_for_context_inner(state, context, request, "user")
|
||||
}
|
||||
|
||||
fn add_sqlite_local_access_grant_for_context(
|
||||
fn add_control_plane_local_access_grant_for_context(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
request: LocalAccessGrantRequest,
|
||||
) -> Result<Value, WebError> {
|
||||
let _policy = require_local_access_policy_admin(context)?;
|
||||
add_sqlite_local_access_grant_for_context_inner(state, context, request, "admin")
|
||||
add_control_plane_local_access_grant_for_context_inner(state, context, request, "admin")
|
||||
}
|
||||
|
||||
fn add_sqlite_local_access_grant_for_context_inner(
|
||||
fn add_control_plane_local_access_grant_for_context_inner(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
request: LocalAccessGrantRequest,
|
||||
@@ -2010,7 +1997,7 @@ fn add_sqlite_local_access_grant_for_context_inner(
|
||||
root_uri: Some(root_uri.clone()),
|
||||
include_revoked: false,
|
||||
})
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权查重失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权查重失败: {error}")))?;
|
||||
if duplicates.iter().any(|grant| {
|
||||
grant.permission.trim() == permission
|
||||
&& grant.recursive == request.recursive
|
||||
@@ -2035,7 +2022,7 @@ fn add_sqlite_local_access_grant_for_context_inner(
|
||||
source: source.to_string(),
|
||||
created_by: Some(context.auth.actor_id.trim().to_string()),
|
||||
})
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权写入失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权写入失败: {error}")))?;
|
||||
let _ = state.control_plane().append_audit(AppendAuditInput {
|
||||
actor_user_id: Some(context.auth.actor_id.trim().to_string()),
|
||||
action: "control.grant.created".to_string(),
|
||||
@@ -2058,14 +2045,14 @@ fn add_sqlite_local_access_grant_for_context_inner(
|
||||
let grants = state
|
||||
.control_plane()
|
||||
.list_directory_grants()
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权列表读取失败: {error}")))?;
|
||||
let grant_values = grants
|
||||
.iter()
|
||||
.map(control_plane_grant_payload)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"policyPath": local_access_policy_path().display().to_string(),
|
||||
"grant": control_plane_grant_payload(&grant),
|
||||
"policy": {
|
||||
@@ -2074,7 +2061,7 @@ fn add_sqlite_local_access_grant_for_context_inner(
|
||||
}))
|
||||
}
|
||||
|
||||
fn delete_sqlite_user_access_grant_for_context(
|
||||
fn delete_control_plane_user_access_grant_for_context(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
grant_id: &str,
|
||||
@@ -2102,7 +2089,7 @@ fn delete_sqlite_user_access_grant_for_context(
|
||||
root_uri: None,
|
||||
include_revoked: false,
|
||||
})
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权查找失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权查找失败: {error}")))?;
|
||||
let grant = grants.first().ok_or_else(|| {
|
||||
WebError::new(
|
||||
StatusCode::NOT_FOUND,
|
||||
@@ -2134,7 +2121,7 @@ fn delete_sqlite_user_access_grant_for_context(
|
||||
"local_access_policy_grant_not_found",
|
||||
message,
|
||||
),
|
||||
other => WebError::internal(format!("SQLite 控制面授权撤销失败: {other}")),
|
||||
other => WebError::internal(format!("control-plane授权撤销失败: {other}")),
|
||||
})?;
|
||||
let _ = state.control_plane().append_audit(AppendAuditInput {
|
||||
actor_user_id: Some(actor_id.to_string()),
|
||||
@@ -2152,14 +2139,14 @@ fn delete_sqlite_user_access_grant_for_context(
|
||||
);
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"deletedGrantId": grant_id,
|
||||
"revokedUserId": revoked_user_id,
|
||||
"policy": {
|
||||
"grants": state
|
||||
.control_plane()
|
||||
.list_directory_grants()
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权列表读取失败: {error}")))?
|
||||
.iter()
|
||||
.filter(|grant| grant.created_by.as_deref().map(str::trim) == Some(actor_id))
|
||||
.map(control_plane_grant_payload)
|
||||
@@ -2168,7 +2155,7 @@ fn delete_sqlite_user_access_grant_for_context(
|
||||
}))
|
||||
}
|
||||
|
||||
fn delete_sqlite_local_access_grant_for_context(
|
||||
fn delete_control_plane_local_access_grant_for_context(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
grant_id: &str,
|
||||
@@ -2189,7 +2176,7 @@ fn delete_sqlite_local_access_grant_for_context(
|
||||
root_uri: None,
|
||||
include_revoked: false,
|
||||
})
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权查找失败: {error}")))?;
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权查找失败: {error}")))?;
|
||||
if grants
|
||||
.first()
|
||||
.map(is_default_workspace_auto_grant)
|
||||
@@ -2211,7 +2198,7 @@ fn delete_sqlite_local_access_grant_for_context(
|
||||
"local_access_policy_grant_not_found",
|
||||
message,
|
||||
),
|
||||
other => WebError::internal(format!("SQLite 控制面授权撤销失败: {other}")),
|
||||
other => WebError::internal(format!("control-plane授权撤销失败: {other}")),
|
||||
})?;
|
||||
let _ = state.control_plane().append_audit(AppendAuditInput {
|
||||
actor_user_id: Some(context.auth.actor_id.trim().to_string()),
|
||||
@@ -2229,7 +2216,7 @@ fn delete_sqlite_local_access_grant_for_context(
|
||||
);
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"controlPlane": "sqlite",
|
||||
"controlPlane": "control-plane",
|
||||
"policyPath": local_access_policy_path().display().to_string(),
|
||||
"deletedGrantId": grant_id,
|
||||
"revokedUserId": revoked_user_id,
|
||||
@@ -2237,7 +2224,7 @@ fn delete_sqlite_local_access_grant_for_context(
|
||||
"grants": state
|
||||
.control_plane()
|
||||
.list_directory_grants()
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?
|
||||
.map_err(|error| WebError::internal(format!("control-plane授权列表读取失败: {error}")))?
|
||||
.iter()
|
||||
.map(control_plane_grant_payload)
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -2420,7 +2407,7 @@ pub async fn get_local_access_policy(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = sqlite_access_policy_payload(&state, &context)
|
||||
let payload = control_plane_access_policy_payload(&state, &context)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
Ok((StatusCode::OK, Json(payload)))
|
||||
}
|
||||
@@ -2429,7 +2416,7 @@ pub async fn get_user_access_policy(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = sqlite_user_access_policy_payload(&state, &context)
|
||||
let payload = control_plane_user_access_policy_payload(&state, &context)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
Ok((StatusCode::OK, Json(payload)))
|
||||
}
|
||||
@@ -2448,7 +2435,7 @@ pub async fn create_local_access_grant(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(request): Json<LocalAccessGrantRequest>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = add_sqlite_local_access_grant_for_context(&state, &context, request)
|
||||
let payload = add_control_plane_local_access_grant_for_context(&state, &context, request)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
if let Some(user_id) = payload
|
||||
.pointer("/grant/userId")
|
||||
@@ -2466,7 +2453,7 @@ pub async fn create_user_access_grant(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(request): Json<LocalAccessGrantRequest>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = add_sqlite_user_access_grant_for_context(&state, &context, request)
|
||||
let payload = add_control_plane_user_access_grant_for_context(&state, &context, request)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
if let Some(user_id) = payload
|
||||
.pointer("/grant/userId")
|
||||
@@ -2484,7 +2471,7 @@ pub async fn delete_local_access_grant(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
AxumPath(grant_id): AxumPath<String>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = delete_sqlite_local_access_grant_for_context(&state, &context, &grant_id)
|
||||
let payload = delete_control_plane_local_access_grant_for_context(&state, &context, &grant_id)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
if let Some(user_id) = payload
|
||||
.get("revokedUserId")
|
||||
@@ -2502,7 +2489,7 @@ pub async fn delete_user_access_grant(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
AxumPath(grant_id): AxumPath<String>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = delete_sqlite_user_access_grant_for_context(&state, &context, &grant_id)
|
||||
let payload = delete_control_plane_user_access_grant_for_context(&state, &context, &grant_id)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
if let Some(user_id) = payload
|
||||
.get("revokedUserId")
|
||||
@@ -2536,7 +2523,7 @@ pub async fn get_share_links(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<ShareLinkListQuery>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = list_sqlite_share_links_for_context(&state, &context, query)
|
||||
let payload = list_control_plane_share_links_for_context(&state, &context, query)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
Ok((StatusCode::OK, Json(payload)))
|
||||
}
|
||||
@@ -2546,7 +2533,7 @@ pub async fn create_share_link(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(request): Json<LocalShareLinkRequest>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = create_sqlite_share_link_for_context(&state, &context, request)
|
||||
let payload = create_control_plane_share_link_for_context(&state, &context, request)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
Ok((StatusCode::OK, Json(payload)))
|
||||
}
|
||||
@@ -2556,7 +2543,7 @@ pub async fn delete_share_link(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
AxumPath(link_id): AxumPath<String>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let payload = revoke_sqlite_share_link_for_context(&state, &context, &link_id)
|
||||
let payload = revoke_control_plane_share_link_for_context(&state, &context, &link_id)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
Ok((StatusCode::OK, Json(payload)))
|
||||
}
|
||||
@@ -9578,7 +9565,7 @@ fn markdown_page_subtree(document_id: &str, title: &str, content: &Value) -> Val
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
add_sqlite_local_access_grant_for_context,
|
||||
add_control_plane_local_access_grant_for_context,
|
||||
create_default_local_workspace_for_actor_at_base, create_local_access_grant,
|
||||
create_share_grant, create_share_link, create_user_access_grant, create_user_share_grant,
|
||||
delete_share_link, delete_user_access_grant, editor_blocks_to_markdown_for_file,
|
||||
@@ -11054,7 +11041,7 @@ fn main() {}
|
||||
.expect("created grant id")
|
||||
.to_string();
|
||||
assert_eq!(created["grant"]["permission"], "read");
|
||||
assert_eq!(created["controlPlane"], "sqlite");
|
||||
assert_eq!(created["controlPlane"], "control-plane");
|
||||
let created_events = state
|
||||
.control_plane()
|
||||
.drain_outbox(10)
|
||||
@@ -11080,7 +11067,7 @@ fn main() {}
|
||||
.expect_err("created read grant cannot write");
|
||||
assert_eq!(write_error.status(), StatusCode::FORBIDDEN);
|
||||
|
||||
let duplicate_error = add_sqlite_local_access_grant_for_context(
|
||||
let duplicate_error = add_control_plane_local_access_grant_for_context(
|
||||
&state,
|
||||
&request_context("admin_1", "user"),
|
||||
LocalAccessGrantRequest {
|
||||
@@ -11262,7 +11249,7 @@ fn main() {}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_access_policy_grant_uses_sqlite_directory_grants_for_owned_folder() {
|
||||
async fn user_access_policy_grant_uses_control_plane_directory_grants_for_owned_folder() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
let owned_root = temp_root("mnote-user-access-policy-owned-root");
|
||||
let outside_root = temp_root("mnote-user-access-policy-outside-root");
|
||||
@@ -11304,7 +11291,7 @@ fn main() {}
|
||||
.as_str()
|
||||
.expect("grant id")
|
||||
.to_string();
|
||||
assert_eq!(created["controlPlane"], "sqlite");
|
||||
assert_eq!(created["controlPlane"], "control-plane");
|
||||
assert_eq!(created["grant"]["userId"], "user_target");
|
||||
assert_eq!(created["grant"]["createdBy"], "user_owner");
|
||||
assert_eq!(created["grant"]["permission"], "write");
|
||||
@@ -11324,7 +11311,7 @@ fn main() {}
|
||||
)
|
||||
.await
|
||||
.expect("owner can list self-created directory grants");
|
||||
assert_eq!(listed["controlPlane"], "sqlite");
|
||||
assert_eq!(listed["controlPlane"], "control-plane");
|
||||
assert_eq!(listed["grants"][0]["id"], grant_id);
|
||||
assert_eq!(listed["grants"][0]["permission"], "write");
|
||||
|
||||
@@ -11334,7 +11321,7 @@ fn main() {}
|
||||
)
|
||||
.await
|
||||
.expect("target can list incoming directory grants");
|
||||
assert_eq!(target_listed["controlPlane"], "sqlite");
|
||||
assert_eq!(target_listed["controlPlane"], "control-plane");
|
||||
assert_eq!(target_listed["grants"][0]["id"], grant_id);
|
||||
assert_eq!(target_listed["grants"][0]["userId"], "user_target");
|
||||
|
||||
@@ -11452,7 +11439,7 @@ fn main() {}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn share_link_api_creates_lists_and_revokes_sqlite_record() {
|
||||
async fn share_link_api_creates_lists_and_revokes_control_plane_record() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
let state = test_state();
|
||||
std::env::set_var("MNOTE_ADMIN_USER_IDS", "admin_1");
|
||||
@@ -11488,7 +11475,7 @@ fn main() {}
|
||||
.await
|
||||
.expect("admin can create share link");
|
||||
let link_id = created["link"]["id"].as_str().expect("link id").to_string();
|
||||
assert_eq!(created["controlPlane"], "sqlite");
|
||||
assert_eq!(created["controlPlane"], "control-plane");
|
||||
assert_eq!(created["token"], "visible-token");
|
||||
assert!(created["link"].get("tokenHash").is_none());
|
||||
assert_eq!(created["link"]["permission"], "read");
|
||||
@@ -11507,7 +11494,7 @@ fn main() {}
|
||||
let stored = state
|
||||
.control_plane()
|
||||
.list_share_links(&workspace.id)
|
||||
.expect("list sqlite share links");
|
||||
.expect("list control-plane share links");
|
||||
assert_eq!(stored.len(), 1);
|
||||
assert_eq!(stored[0].id, link_id);
|
||||
assert_ne!(stored[0].token_hash, "visible-token");
|
||||
@@ -12691,30 +12678,30 @@ fn main() {}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_file_open_allows_sqlite_directory_read_grant_without_json_policy() {
|
||||
async fn local_file_open_allows_control_plane_directory_read_grant_without_json_policy() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
let root = temp_root("mnote-local-file-open-sqlite-read-grant-root");
|
||||
let policy_root = temp_root("mnote-local-file-open-sqlite-read-grant-config");
|
||||
let root = temp_root("mnote-local-file-open-control-plane-read-grant-root");
|
||||
let policy_root = temp_root("mnote-local-file-open-control-plane-read-grant-config");
|
||||
let policy_file = policy_root.join("missing-access-policy.json");
|
||||
let state = test_state();
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
std::fs::write(root.join("README.txt"), "hello sqlite").expect("write file");
|
||||
std::fs::write(root.join("README.txt"), "hello control plane").expect("write file");
|
||||
std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &policy_file);
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some("sqlite_reader".into()),
|
||||
email: Some("sqlite-reader@example.com".into()),
|
||||
username: "sqlite_reader".into(),
|
||||
display_name: "sqlite_reader".into(),
|
||||
id: Some("control_plane_reader".into()),
|
||||
email: Some("control-plane-reader@example.com".into()),
|
||||
username: "control_plane_reader".into(),
|
||||
display_name: "control_plane_reader".into(),
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert sqlite reader");
|
||||
.expect("upsert control-plane reader");
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
user_id: "sqlite_reader".into(),
|
||||
user_id: "control_plane_reader".into(),
|
||||
workspace_id: None,
|
||||
root_uri: root_uri.clone(),
|
||||
root_path: root
|
||||
@@ -12728,9 +12715,9 @@ fn main() {}
|
||||
source: "test".into(),
|
||||
created_by: None,
|
||||
})
|
||||
.expect("grant sqlite read");
|
||||
.expect("grant control-plane read");
|
||||
|
||||
let context = request_context("sqlite_reader", "user");
|
||||
let context = request_context("control_plane_reader", "user");
|
||||
let (_, _, bytes) = open_local_file(
|
||||
State(state),
|
||||
Extension(context),
|
||||
@@ -12741,8 +12728,8 @@ fn main() {}
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("sqlite read grant can open local file");
|
||||
assert_eq!(bytes, b"hello sqlite");
|
||||
.expect("control-plane read grant can open local file");
|
||||
assert_eq!(bytes, b"hello control plane");
|
||||
assert!(!policy_file.exists(), "SQLite grant 不应写旧 JSON policy");
|
||||
|
||||
std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
|
||||
@@ -12751,32 +12738,32 @@ fn main() {}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_file_open_allows_legacy_sqlite_directory_grant_with_path_root_uri() {
|
||||
async fn local_file_open_allows_legacy_control_plane_directory_grant_with_path_root_uri() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
let root = temp_root("mnote-local-file-open-legacy-sqlite-grant-root");
|
||||
let policy_root = temp_root("mnote-local-file-open-legacy-sqlite-grant-config");
|
||||
let root = temp_root("mnote-local-file-open-legacy-control-plane-grant-root");
|
||||
let policy_root = temp_root("mnote-local-file-open-legacy-control-plane-grant-config");
|
||||
let policy_file = policy_root.join("missing-access-policy.json");
|
||||
let state = test_state();
|
||||
let canonical_root = root.canonicalize().expect("canonical root");
|
||||
let canonical_root_path = canonical_root.display().to_string();
|
||||
let root_uri = format!("file://{canonical_root_path}");
|
||||
std::fs::write(root.join("README.txt"), "hello legacy sqlite").expect("write file");
|
||||
std::fs::write(root.join("README.txt"), "hello legacy control plane").expect("write file");
|
||||
std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &policy_file);
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some("legacy_sqlite_reader".into()),
|
||||
email: Some("legacy-sqlite-reader@example.com".into()),
|
||||
username: "legacy_sqlite_reader".into(),
|
||||
display_name: "legacy_sqlite_reader".into(),
|
||||
id: Some("legacy_control_plane_reader".into()),
|
||||
email: Some("legacy-control-plane-reader@example.com".into()),
|
||||
username: "legacy_control_plane_reader".into(),
|
||||
display_name: "legacy_control_plane_reader".into(),
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert sqlite reader");
|
||||
.expect("upsert control-plane reader");
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
user_id: "legacy_sqlite_reader".into(),
|
||||
user_id: "legacy_control_plane_reader".into(),
|
||||
workspace_id: None,
|
||||
root_uri: canonical_root_path.clone(),
|
||||
root_path: canonical_root_path,
|
||||
@@ -12786,9 +12773,9 @@ fn main() {}
|
||||
source: "legacy-test".into(),
|
||||
created_by: None,
|
||||
})
|
||||
.expect("grant legacy sqlite read");
|
||||
.expect("grant legacy control-plane read");
|
||||
|
||||
let context = request_context("legacy_sqlite_reader", "user");
|
||||
let context = request_context("legacy_control_plane_reader", "user");
|
||||
let (_, _, bytes) = open_local_file(
|
||||
State(state),
|
||||
Extension(context),
|
||||
@@ -12799,8 +12786,8 @@ fn main() {}
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("legacy sqlite read grant can open local file");
|
||||
assert_eq!(bytes, b"hello legacy sqlite");
|
||||
.expect("legacy control-plane read grant can open local file");
|
||||
assert_eq!(bytes, b"hello legacy control plane");
|
||||
assert!(!policy_file.exists(), "SQLite grant 不应写旧 JSON policy");
|
||||
|
||||
std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
|
||||
@@ -12809,10 +12796,10 @@ fn main() {}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_resource_write_allows_sqlite_directory_write_grant_without_json_policy() {
|
||||
async fn local_resource_write_allows_control_plane_directory_write_grant_without_json_policy() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
let root = temp_root("mnote-local-resource-write-sqlite-grant-root");
|
||||
let policy_root = temp_root("mnote-local-resource-write-sqlite-grant-config");
|
||||
let root = temp_root("mnote-local-resource-write-control-plane-grant-root");
|
||||
let policy_root = temp_root("mnote-local-resource-write-control-plane-grant-config");
|
||||
let policy_file = policy_root.join("missing-access-policy.json");
|
||||
let state = test_state();
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
@@ -12822,18 +12809,18 @@ fn main() {}
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some("sqlite_writer".into()),
|
||||
email: Some("sqlite-writer@example.com".into()),
|
||||
username: "sqlite_writer".into(),
|
||||
display_name: "sqlite_writer".into(),
|
||||
id: Some("control_plane_writer".into()),
|
||||
email: Some("control-plane-writer@example.com".into()),
|
||||
username: "control_plane_writer".into(),
|
||||
display_name: "control_plane_writer".into(),
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert sqlite writer");
|
||||
.expect("upsert control-plane writer");
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
user_id: "sqlite_writer".into(),
|
||||
user_id: "control_plane_writer".into(),
|
||||
workspace_id: None,
|
||||
root_uri: root_uri.clone(),
|
||||
root_path: root
|
||||
@@ -12847,9 +12834,9 @@ fn main() {}
|
||||
source: "test".into(),
|
||||
created_by: None,
|
||||
})
|
||||
.expect("grant sqlite write");
|
||||
.expect("grant control-plane write");
|
||||
|
||||
let context = request_context("sqlite_writer", "user");
|
||||
let context = request_context("control_plane_writer", "user");
|
||||
let (_, _, payload) = write_local_resource(
|
||||
State(state),
|
||||
Extension(context),
|
||||
@@ -12868,7 +12855,7 @@ fn main() {}
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("sqlite write grant can write local resource");
|
||||
.expect("control-plane write grant can write local resource");
|
||||
assert_eq!(payload["ok"], true);
|
||||
let written = std::fs::read_to_string(root.join("README.md")).expect("read file");
|
||||
assert!(written.contains("new content"));
|
||||
|
||||
@@ -9,12 +9,15 @@ use control_plane::{ControlPlaneStore, UpsertUserInput, UpsertUserUiPreferenceIn
|
||||
use core_protocol::EvidenceSearchMatchInfo;
|
||||
#[cfg(test)]
|
||||
use core_protocol::{EvidenceLocator, EvidenceSearchResult};
|
||||
#[cfg(test)]
|
||||
use core_protocol::{
|
||||
ParsedResourceArtifact, ResourceSourceMap, SourceMapBlock, PARSED_RESOURCE_ARTIFACT_SCHEMA,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
#[cfg(test)]
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
@@ -28,6 +31,7 @@ const LOCAL_INDEX_SETTINGS_SCOPE_KIND: &str = "localIndex";
|
||||
const LOCAL_INDEX_SOURCE_KIND: &str = "local_folder";
|
||||
const DEFAULT_INDEX_SCHEDULE_MODE: &str = "daily";
|
||||
const DEFAULT_INDEX_SCHEDULE_TIME: &str = "02:00";
|
||||
#[cfg(test)]
|
||||
const EVIDENCE_SQLITE_SCHEMA_VERSION: u32 = 1;
|
||||
|
||||
fn local_index_scope_id(root_path: &Path) -> String {
|
||||
@@ -460,7 +464,6 @@ fn local_index_status_for_settings(
|
||||
.join(".mnote")
|
||||
.join("index")
|
||||
.join("search-index.json");
|
||||
let evidence_path = evidence_sqlite_path(root_path);
|
||||
let mut document_count = 0usize;
|
||||
let mut resource_count = 0usize;
|
||||
let mut built_at = Value::Null;
|
||||
@@ -478,21 +481,15 @@ fn local_index_status_for_settings(
|
||||
cache_matches_settings = settings.include_paths.is_empty();
|
||||
local_index_schedule_is_due(&settings, 0)
|
||||
};
|
||||
let evidence_block_count = if evidence_path.exists() {
|
||||
count_evidence_blocks(&evidence_path).unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
Ok(json!({
|
||||
"schema": "mnote.local_index.status.v1",
|
||||
"rootUri": root_uri,
|
||||
"workspaceId": workspace_id,
|
||||
"settings": settings,
|
||||
"indexPath": ".mnote/index/search-index.json",
|
||||
"evidenceIndexPath": ".mnote/index/evidence.sqlite",
|
||||
"settingsPath": ".mnote/index/local-index-settings.json",
|
||||
"indexExists": index_path.exists(),
|
||||
"evidenceIndexExists": evidence_path.exists(),
|
||||
"evidenceIndexRetired": true,
|
||||
"cacheMatchesSettings": cache_matches_settings,
|
||||
"scheduledDue": scheduled_due,
|
||||
"builtAt": built_at,
|
||||
@@ -502,7 +499,6 @@ fn local_index_status_for_settings(
|
||||
.unwrap_or_default(),
|
||||
"documentCount": document_count,
|
||||
"resourceCount": resource_count,
|
||||
"evidenceBlockCount": evidence_block_count,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1233,6 +1229,7 @@ pub(crate) fn refresh_local_search_index_for_path_with_settings(
|
||||
index.built_at = now_ms();
|
||||
write_local_search_index_json(root_path, &index)?;
|
||||
let _ = included;
|
||||
#[cfg(test)]
|
||||
remove_evidence_sqlite_index_for_path(root_path, &index, &relative_path)?;
|
||||
return Ok(json!({
|
||||
"version": index.version,
|
||||
@@ -1283,6 +1280,7 @@ pub(crate) fn refresh_local_search_index_for_path_with_settings(
|
||||
.sort_by(|left, right| left.path.cmp(&right.path));
|
||||
index.built_at = now_ms();
|
||||
write_local_search_index_json(root_path, &index)?;
|
||||
#[cfg(test)]
|
||||
if included {
|
||||
refresh_evidence_sqlite_index_for_path(root_path, &index, &relative_path)?;
|
||||
} else {
|
||||
@@ -1518,6 +1516,7 @@ fn load_or_rebuild_local_search_index_with_settings(
|
||||
settings,
|
||||
);
|
||||
}
|
||||
#[cfg(test)]
|
||||
ensure_evidence_sqlite_index(root_path, &index)?;
|
||||
Ok(index)
|
||||
}
|
||||
@@ -2108,7 +2107,11 @@ fn index_resource_file(root_path: &Path, path: &Path) -> Result<LocalSearchResou
|
||||
|
||||
fn write_local_search_index(root_path: &Path, index: &LocalSearchIndex) -> Result<(), WebError> {
|
||||
write_local_search_index_json(root_path, index)?;
|
||||
write_evidence_sqlite_index(root_path, index)
|
||||
#[cfg(test)]
|
||||
{
|
||||
write_evidence_sqlite_index(root_path, index)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_local_search_index_json(
|
||||
@@ -2138,15 +2141,32 @@ fn clear_local_search_index(root_path: &Path) -> Result<(), WebError> {
|
||||
.join(".mnote")
|
||||
.join("index")
|
||||
.join("search-index.json");
|
||||
let evidence_path = evidence_sqlite_path(root_path);
|
||||
for path in [index_path, evidence_path] {
|
||||
match fs::remove_file(&path) {
|
||||
match fs::remove_file(&index_path) {
|
||||
Ok(()) => {}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(error) => {
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_search_index_delete_failed",
|
||||
format!(
|
||||
"无法删除本地搜索索引文件 {}: {error}",
|
||||
index_path.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
{
|
||||
let evidence_path = evidence_sqlite_path(root_path);
|
||||
match fs::remove_file(&evidence_path) {
|
||||
Ok(()) => {}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(error) => {
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_search_index_delete_failed",
|
||||
format!("无法删除本地搜索索引文件 {}: {error}", path.display()),
|
||||
format!(
|
||||
"无法删除本地 evidence 测试索引文件 {}: {error}",
|
||||
evidence_path.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -2154,6 +2174,7 @@ fn clear_local_search_index(root_path: &Path) -> Result<(), WebError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn ensure_evidence_sqlite_index(
|
||||
root_path: &Path,
|
||||
index: &LocalSearchIndex,
|
||||
@@ -2165,6 +2186,7 @@ fn ensure_evidence_sqlite_index(
|
||||
write_evidence_sqlite_index(root_path, index)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn write_evidence_sqlite_index(root_path: &Path, index: &LocalSearchIndex) -> Result<(), WebError> {
|
||||
let Some(parent) = evidence_sqlite_path(root_path)
|
||||
.parent()
|
||||
@@ -2220,6 +2242,7 @@ fn write_evidence_sqlite_index(root_path: &Path, index: &LocalSearchIndex) -> Re
|
||||
tx.commit().map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn create_evidence_schema(connection: &Connection) -> Result<(), WebError> {
|
||||
connection
|
||||
.execute_batch(
|
||||
@@ -2279,6 +2302,7 @@ CREATE TABLE IF NOT EXISTS evidence_edge(
|
||||
.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn refresh_evidence_sqlite_index_for_path(
|
||||
root_path: &Path,
|
||||
index: &LocalSearchIndex,
|
||||
@@ -2338,6 +2362,7 @@ fn refresh_evidence_sqlite_index_for_path(
|
||||
tx.commit().map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn remove_evidence_sqlite_index_for_path(
|
||||
root_path: &Path,
|
||||
index: &LocalSearchIndex,
|
||||
@@ -2365,6 +2390,7 @@ fn remove_evidence_sqlite_index_for_path(
|
||||
tx.commit().map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn affected_evidence_resource_ids(index: &LocalSearchIndex, relative_path: &str) -> Vec<String> {
|
||||
let mut ids = Vec::new();
|
||||
let document_id = format!("local-md:{}", encode_local_id_segment(relative_path));
|
||||
@@ -2393,6 +2419,7 @@ fn affected_evidence_resource_ids(index: &LocalSearchIndex, relative_path: &str)
|
||||
ids
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn existing_evidence_resource_ids_for_path(
|
||||
connection: &Connection,
|
||||
relative_path: &str,
|
||||
@@ -2413,6 +2440,7 @@ fn existing_evidence_resource_ids_for_path(
|
||||
Ok(resource_ids)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn delete_evidence_resource_projection(
|
||||
connection: &Connection,
|
||||
resource_id: &str,
|
||||
@@ -2466,6 +2494,7 @@ fn delete_evidence_resource_projection(
|
||||
.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_document_evidence(
|
||||
connection: &Connection,
|
||||
index: &LocalSearchIndex,
|
||||
@@ -2521,6 +2550,7 @@ fn insert_document_evidence(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn markdown_parsed_artifact(document: &LocalSearchDocument) -> ParsedResourceArtifact {
|
||||
ParsedResourceArtifact {
|
||||
schema: PARSED_RESOURCE_ARTIFACT_SCHEMA.into(),
|
||||
@@ -2536,6 +2566,7 @@ fn markdown_parsed_artifact(document: &LocalSearchDocument) -> ParsedResourceArt
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_evidence_resource_from_artifact(
|
||||
connection: &Connection,
|
||||
resource_id: &str,
|
||||
@@ -2555,6 +2586,7 @@ fn insert_evidence_resource_from_artifact(
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_resource_evidence(
|
||||
connection: &Connection,
|
||||
root_path: &Path,
|
||||
@@ -2606,6 +2638,7 @@ fn insert_resource_evidence(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn parse_resource_evidence_artifact(
|
||||
_root_path: &Path,
|
||||
_index: &LocalSearchIndex,
|
||||
@@ -2614,10 +2647,12 @@ fn parse_resource_evidence_artifact(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn parsed_resource_id(resource: &LocalSearchResource) -> String {
|
||||
format!("{}#parse", resource.resource_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_source_map_artifact_evidence(
|
||||
connection: &Connection,
|
||||
index: &LocalSearchIndex,
|
||||
@@ -2682,6 +2717,7 @@ fn insert_source_map_artifact_evidence(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_source_map_evidence_section(
|
||||
connection: &Connection,
|
||||
resource_id: &str,
|
||||
@@ -2709,6 +2745,7 @@ fn insert_source_map_evidence_section(
|
||||
.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn source_map_section_path_for_block(
|
||||
source_map: &ResourceSourceMap,
|
||||
block_id: &str,
|
||||
@@ -2721,6 +2758,7 @@ fn source_map_section_path_for_block(
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn source_map_block_locator(
|
||||
index: &LocalSearchIndex,
|
||||
artifact: &ParsedResourceArtifact,
|
||||
@@ -2766,6 +2804,7 @@ fn source_map_block_locator(
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn empty_source_map_block(text: &str) -> SourceMapBlock {
|
||||
SourceMapBlock {
|
||||
id: "artifact".into(),
|
||||
@@ -2776,6 +2815,7 @@ fn empty_source_map_block(text: &str) -> SourceMapBlock {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_evidence_resource(
|
||||
connection: &Connection,
|
||||
resource_id: &str,
|
||||
@@ -2807,6 +2847,7 @@ fn insert_evidence_resource(
|
||||
.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_evidence_block(
|
||||
connection: &Connection,
|
||||
block_id: &str,
|
||||
@@ -2825,6 +2866,7 @@ fn insert_evidence_block(
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_evidence_block_with_metadata(
|
||||
connection: &Connection,
|
||||
block_id: &str,
|
||||
@@ -2871,6 +2913,7 @@ fn insert_evidence_block_with_metadata(
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(test)]
|
||||
struct MarkdownEvidenceBlock {
|
||||
block_id: String,
|
||||
text: String,
|
||||
@@ -2879,6 +2922,7 @@ struct MarkdownEvidenceBlock {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(test)]
|
||||
struct MarkdownEvidenceSection {
|
||||
section_id: String,
|
||||
title: String,
|
||||
@@ -2887,6 +2931,7 @@ struct MarkdownEvidenceSection {
|
||||
parent_section_id: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn markdown_evidence_blocks(document: &LocalSearchDocument) -> Vec<MarkdownEvidenceBlock> {
|
||||
let mut headings: Vec<String> = Vec::new();
|
||||
let mut blocks = Vec::new();
|
||||
@@ -2910,6 +2955,7 @@ fn markdown_evidence_blocks(document: &LocalSearchDocument) -> Vec<MarkdownEvide
|
||||
blocks
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn markdown_evidence_sections(document: &LocalSearchDocument) -> Vec<MarkdownEvidenceSection> {
|
||||
let mut headings: Vec<String> = Vec::new();
|
||||
let mut sections = Vec::new();
|
||||
@@ -2945,6 +2991,7 @@ fn markdown_evidence_sections(document: &LocalSearchDocument) -> Vec<MarkdownEvi
|
||||
sections
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn markdown_heading(line: &str) -> Option<(usize, String)> {
|
||||
let trimmed = line.trim_start();
|
||||
let marker_count = trimmed.chars().take_while(|value| *value == '#').count();
|
||||
@@ -2958,6 +3005,7 @@ fn markdown_heading(line: &str) -> Option<(usize, String)> {
|
||||
Some((marker_count, rest.trim_matches('#').trim().to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_markdown_evidence_section(
|
||||
connection: &Connection,
|
||||
resource_id: &str,
|
||||
@@ -2985,6 +3033,7 @@ fn insert_markdown_evidence_section(
|
||||
.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_document_graph_edges(
|
||||
connection: &Connection,
|
||||
document: &LocalSearchDocument,
|
||||
@@ -3029,6 +3078,7 @@ fn insert_document_graph_edges(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn first_markdown_evidence_block_id(document: &LocalSearchDocument) -> String {
|
||||
document
|
||||
.raw_text
|
||||
@@ -3039,6 +3089,7 @@ fn first_markdown_evidence_block_id(document: &LocalSearchDocument) -> String {
|
||||
.unwrap_or_else(|| format!("{}#line1", document.document_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn insert_evidence_edge(
|
||||
connection: &Connection,
|
||||
from_id: &str,
|
||||
@@ -3073,6 +3124,7 @@ fn insert_evidence_edge(
|
||||
.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn markdown_reference_target_id(owner_path: &str, target: &str) -> String {
|
||||
let target = target.trim();
|
||||
if target.starts_with("local-md:") {
|
||||
@@ -3087,6 +3139,7 @@ fn markdown_reference_target_id(owner_path: &str, target: &str) -> String {
|
||||
format!("markdown-title:{}", encode_local_id_segment(target))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn resource_reference_target_id(owner_path: &str, target: &str) -> String {
|
||||
format!(
|
||||
"local-resource:{}",
|
||||
@@ -3095,11 +3148,13 @@ fn resource_reference_target_id(owner_path: &str, target: &str) -> String {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(test)]
|
||||
struct MarkdownMentionEdge {
|
||||
target_id: String,
|
||||
source_block_id: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn markdown_mention_edges(document: &LocalSearchDocument) -> Vec<MarkdownMentionEdge> {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
let mut mentions = Vec::new();
|
||||
@@ -3118,6 +3173,7 @@ fn markdown_mention_edges(document: &LocalSearchDocument) -> Vec<MarkdownMention
|
||||
mentions
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn extract_line_mentions(line: &str) -> Vec<String> {
|
||||
let chars = line.chars().collect::<Vec<_>>();
|
||||
let mut mentions = Vec::new();
|
||||
@@ -3151,6 +3207,7 @@ fn extract_line_mentions(line: &str) -> Vec<String> {
|
||||
mentions
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn markdown_locator(
|
||||
index: &LocalSearchIndex,
|
||||
document_id: &str,
|
||||
@@ -3177,6 +3234,7 @@ fn markdown_locator(
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn evidence_sqlite_path(root_path: &Path) -> PathBuf {
|
||||
root_path
|
||||
.join(".mnote")
|
||||
@@ -3184,6 +3242,8 @@ fn evidence_sqlite_path(root_path: &Path) -> PathBuf {
|
||||
.join("evidence.sqlite")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
fn count_evidence_blocks(path: &Path) -> Result<u64, WebError> {
|
||||
let connection = Connection::open(path).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
@@ -3198,6 +3258,7 @@ fn count_evidence_blocks(path: &Path) -> Result<u64, WebError> {
|
||||
.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct LocalEvidenceSourceStatuses {
|
||||
@@ -3205,6 +3266,7 @@ pub(crate) struct LocalEvidenceSourceStatuses {
|
||||
pub(crate) failed_paths: BTreeSet<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn local_evidence_source_statuses(
|
||||
root_path: &Path,
|
||||
@@ -3251,6 +3313,7 @@ pub(crate) fn local_evidence_source_statuses(
|
||||
Ok(statuses)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn evidence_resource_kind_for_type(resource_type: &str) -> &'static str {
|
||||
match resource_type {
|
||||
"mindmap" => "mindmap",
|
||||
@@ -3262,6 +3325,7 @@ fn evidence_resource_kind_for_type(resource_type: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn evidence_resource_kind_for_path(path: &str) -> &'static str {
|
||||
match Path::new(path)
|
||||
.extension()
|
||||
@@ -3315,6 +3379,7 @@ fn is_local_evidence_sidecar_relative_path(relative_path: &str) -> bool {
|
||||
&& (file_name.ends_with(".parse.md") || file_name.ends_with(".source-map.json"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn normalize_local_reference_path(owner_path: &str, target: &str) -> String {
|
||||
let target = target
|
||||
.split('#')
|
||||
@@ -3333,6 +3398,7 @@ fn normalize_local_reference_path(owner_path: &str, target: &str) -> String {
|
||||
normalize_path_components(owner_parent.join(target))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn normalize_path_components(path: PathBuf) -> String {
|
||||
let mut parts = Vec::new();
|
||||
for component in path.components() {
|
||||
@@ -3348,6 +3414,7 @@ fn normalize_path_components(path: PathBuf) -> String {
|
||||
parts.join("/")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn sqlite_error(error: rusqlite::Error) -> WebError {
|
||||
WebError::bad_request_code(
|
||||
"evidence_index_sqlite_error",
|
||||
|
||||
@@ -86,6 +86,9 @@ pub fn build_router(state: AppState) -> Router {
|
||||
)
|
||||
.route("/auth", get(gateway::auth_entry).post(gateway::auth_entry))
|
||||
.route("/search", get(search::shell))
|
||||
.route("/knowledge", get(gateway::root_entry))
|
||||
.route("/knowledge/{*path}", get(gateway::root_entry))
|
||||
.route("/debug/knowledge-rag", get(gateway::root_entry))
|
||||
.route("/api/evidence/search", post(evidence::search))
|
||||
.route("/api/evidence/read", post(evidence::read))
|
||||
.route("/api/evidence/open", post(evidence::open))
|
||||
|
||||
@@ -9,6 +9,7 @@ use axum::response::Response;
|
||||
use axum::{Extension, Json};
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use control_plane::session_token_hash;
|
||||
use futures_util::TryStreamExt;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
@@ -364,6 +365,29 @@ fn mnote_user_id(state: &AppState, context: &RequestContext) -> String {
|
||||
"anonymous".to_string()
|
||||
}
|
||||
|
||||
fn build_allowed_roots(state: &AppState, user_id: &str) -> Vec<Value> {
|
||||
state
|
||||
.control_plane()
|
||||
.list_directory_grants_for_actor(user_id)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|grant| grant.status.trim() == "active")
|
||||
.filter(|grant| !grant.root_uri.trim().is_empty())
|
||||
.map(|grant| {
|
||||
json!({
|
||||
"grantId": grant.id,
|
||||
"workspaceId": grant.workspace_id,
|
||||
"rootUri": grant.root_uri,
|
||||
"rootPath": grant.root_path,
|
||||
"permission": grant.permission,
|
||||
"recursive": grant.recursive,
|
||||
"capabilities": serde_json::from_str::<Vec<String>>(&grant.capabilities_json).unwrap_or_default(),
|
||||
"source": grant.source,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn text_or_default(value: Option<&str>, fallback: &str) -> String {
|
||||
value
|
||||
.map(str::trim)
|
||||
@@ -378,6 +402,56 @@ fn file_path_from_root_uri(root_uri: &str) -> Option<PathBuf> {
|
||||
(!file_path.trim().is_empty()).then(|| PathBuf::from(file_path))
|
||||
}
|
||||
|
||||
fn default_openhub_workspace_root_uri(user_id: &str) -> String {
|
||||
format!("file:///mnt/Data1T/Mnote_data/users/{user_id}/workspaces/my-space")
|
||||
}
|
||||
|
||||
fn preferred_openhub_root_uri(root: &Value) -> Option<String> {
|
||||
root.get("rootUri")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| value.starts_with("file://"))
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
root.get("rootPath")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| format!("file://{value}"))
|
||||
})
|
||||
.or_else(|| {
|
||||
root.get("rootUri")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
})
|
||||
}
|
||||
|
||||
fn default_allowed_roots_for_scope(default_allowed_roots: &[Value], root_uri: &str) -> Value {
|
||||
let root_uri = root_uri.trim();
|
||||
let matched_root = default_allowed_roots
|
||||
.iter()
|
||||
.find(|root| {
|
||||
preferred_openhub_root_uri(root)
|
||||
.as_deref()
|
||||
.is_some_and(|candidate| candidate == root_uri)
|
||||
})
|
||||
.cloned();
|
||||
|
||||
if let Some(root) = matched_root {
|
||||
return json!([root]);
|
||||
}
|
||||
|
||||
json!([{
|
||||
"rootUri": root_uri,
|
||||
"permission": "write",
|
||||
"recursive": true,
|
||||
"capabilities": ["ai", "read", "write"],
|
||||
"source": "mnote_openhub_default_root",
|
||||
}])
|
||||
}
|
||||
|
||||
fn artifact_index_store_path(root_uri: Option<&str>) -> PathBuf {
|
||||
if let Ok(path) = std::env::var("MNOTE_OPENHUB_ARTIFACT_INDEX_PATH") {
|
||||
let path = path.trim();
|
||||
@@ -554,6 +628,12 @@ fn build_scope(
|
||||
request: &OpenHubBootstrapRequest,
|
||||
) -> Value {
|
||||
let mnote_user_id = mnote_user_id(state, context);
|
||||
let mnote_display_name = crate::routes::gateway::current_actor_display_name(state, context);
|
||||
let default_allowed_roots = build_allowed_roots(state, &mnote_user_id);
|
||||
let default_root_uri = default_allowed_roots
|
||||
.iter()
|
||||
.find_map(preferred_openhub_root_uri)
|
||||
.unwrap_or_else(|| default_openhub_workspace_root_uri(&mnote_user_id));
|
||||
let workspace_id = text_or_default(
|
||||
request
|
||||
.workspace_id
|
||||
@@ -561,7 +641,7 @@ fn build_scope(
|
||||
.or(context.workspace.workspace_id.as_deref()),
|
||||
"default-workspace",
|
||||
);
|
||||
let root_uri = text_or_default(request.root_uri.as_deref(), "");
|
||||
let root_uri = text_or_default(request.root_uri.as_deref(), &default_root_uri);
|
||||
let page_resource_id = text_or_default(
|
||||
request
|
||||
.page_id
|
||||
@@ -583,13 +663,14 @@ fn build_scope(
|
||||
"pageAbsolutePath": request.page_absolute_path.as_deref(),
|
||||
"pageTitle": request.page_title.as_deref(),
|
||||
});
|
||||
let allowed_roots = request
|
||||
.allowed_roots
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| default_allowed_roots_for_scope(&default_allowed_roots, &root_uri));
|
||||
let weknora_seed = format!(
|
||||
"{openhub_user_key}\n{openhub_workspace_key}\n{}",
|
||||
request
|
||||
.allowed_roots
|
||||
.as_ref()
|
||||
.map(Value::to_string)
|
||||
.unwrap_or_else(|| "[]".to_string())
|
||||
allowed_roots.to_string()
|
||||
);
|
||||
let weknora_tool_scope = stable_hash("weknora_tool", &weknora_seed);
|
||||
json!({
|
||||
@@ -597,6 +678,8 @@ fn build_scope(
|
||||
"authTruth": "mnote_session",
|
||||
"rejectsFrontendAuthTruth": ["openhub_jwt", "openhub_localStorage", "openhub_login_page"],
|
||||
"mnoteUserId": mnote_user_id,
|
||||
"mnoteActorId": context.auth.actor_id,
|
||||
"mnoteDisplayName": mnote_display_name,
|
||||
"openhubUserKey": openhub_user_key,
|
||||
"openhub_user_key": openhub_user_key,
|
||||
"openhubWorkspaceKey": openhub_workspace_key,
|
||||
@@ -612,7 +695,7 @@ fn build_scope(
|
||||
"weknoraToolScope": weknora_tool_scope,
|
||||
"weknora_tool_scope": weknora_tool_scope,
|
||||
"workspaceScope": root_scope,
|
||||
"allowedRoots": request.allowed_roots.as_ref().cloned().unwrap_or_else(|| json!([])),
|
||||
"allowedRoots": allowed_roots,
|
||||
"selectionPresent": request.selection.as_deref().map(str::trim).is_some_and(|value| !value.is_empty()),
|
||||
})
|
||||
}
|
||||
@@ -683,6 +766,12 @@ fn add_mnote_scope_headers(
|
||||
if let Some(value) = header_string(scope.get("openhub_user_key")) {
|
||||
request = request.header(HeaderName::from_static("x-mnote-user-key"), value);
|
||||
}
|
||||
if let Some(value) = header_string(scope.get("mnoteUserId")) {
|
||||
request = request.header(HeaderName::from_static("x-mnote-user-id"), value);
|
||||
}
|
||||
if let Some(value) = header_string(scope.get("mnoteDisplayName")) {
|
||||
request = request.header(HeaderName::from_static("x-mnote-display-name"), value);
|
||||
}
|
||||
if let Some(value) = header_string(scope.get("workspace_key")) {
|
||||
request = request.header(HeaderName::from_static("x-mnote-workspace-key"), value);
|
||||
}
|
||||
@@ -754,7 +843,7 @@ pub async fn status(
|
||||
let openhub_fastapi = probe_service(
|
||||
"openhub_fastapi",
|
||||
openhub_base.clone(),
|
||||
"/global/health",
|
||||
"/api/health",
|
||||
openhub_configured,
|
||||
)
|
||||
.await;
|
||||
@@ -1095,6 +1184,22 @@ fn response_with_status(status: StatusCode, body: String) -> Response {
|
||||
.unwrap_or_else(|_| Response::new(Body::from("openhub proxy response build failed")))
|
||||
}
|
||||
|
||||
fn is_streaming_response(headers: &HeaderMap) -> bool {
|
||||
let content_type = headers
|
||||
.get(header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("")
|
||||
.to_ascii_lowercase();
|
||||
content_type.contains("text/event-stream")
|
||||
|| content_type.contains("application/x-ndjson")
|
||||
|| headers
|
||||
.get(header::CACHE_CONTROL)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("")
|
||||
.to_ascii_lowercase()
|
||||
.contains("no-cache")
|
||||
}
|
||||
|
||||
pub async fn ai_proxy(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
@@ -1116,7 +1221,7 @@ pub async fn ai_proxy(
|
||||
let upstream_method =
|
||||
reqwest::Method::from_bytes(method.as_str().as_bytes()).unwrap_or(reqwest::Method::GET);
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.map_err(|error| {
|
||||
@@ -1162,6 +1267,28 @@ pub async fn ai_proxy(
|
||||
})?;
|
||||
let status = upstream.status();
|
||||
let headers = upstream.headers().clone();
|
||||
if is_streaming_response(&headers) {
|
||||
let mut builder = Response::builder().status(status);
|
||||
let response_headers = strip_hop_headers(&headers);
|
||||
for (name, value) in response_headers.iter() {
|
||||
builder = builder.header(name, value);
|
||||
}
|
||||
builder = builder
|
||||
.header(header::CACHE_CONTROL, "no-cache, no-transform")
|
||||
.header("x-accel-buffering", "no");
|
||||
let stream = upstream.bytes_stream().map_err(|error| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("OpenHub AI proxy 流式响应读取失败:{error}"),
|
||||
)
|
||||
});
|
||||
return Ok(builder.body(Body::from_stream(stream)).unwrap_or_else(|_| {
|
||||
response_with_status(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"OpenHub AI proxy 流式响应构造失败".to_string(),
|
||||
)
|
||||
}));
|
||||
}
|
||||
let content_type = headers
|
||||
.get(header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
|
||||
@@ -82,7 +82,7 @@ fn build_session_response(state: &AppState, context: RequestContext) -> SessionR
|
||||
email: resolved.user.email.unwrap_or_default(),
|
||||
name: resolved.user.display_name,
|
||||
actor_type: effective_actor_type_for_user(&resolved.user.id, &resolved.user.role),
|
||||
auth_mode: "sqliteSession",
|
||||
auth_mode: "controlPlaneSession",
|
||||
request_id: context.trace.request_id,
|
||||
trace_id: context.trace.trace_id,
|
||||
};
|
||||
@@ -321,7 +321,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_prefers_sqlite_cookie_identity_over_dev_fallback() {
|
||||
async fn session_prefers_control_plane_cookie_identity_over_dev_fallback() {
|
||||
let state = AppState::new(AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
@@ -386,11 +386,11 @@ mod tests {
|
||||
assert_eq!(payload["email"], "shujuan@163.com");
|
||||
assert_eq!(payload["name"], "shujuan");
|
||||
assert_eq!(payload["actorType"], "user");
|
||||
assert_eq!(payload["authMode"], "sqliteSession");
|
||||
assert_eq!(payload["authMode"], "controlPlaneSession");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_returns_admin_for_sqlite_admin_user() {
|
||||
async fn session_returns_admin_for_control_plane_admin_user() {
|
||||
let state = AppState::new(AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
@@ -452,7 +452,7 @@ mod tests {
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["userId"], "liaibo");
|
||||
assert_eq!(payload["actorType"], "admin");
|
||||
assert_eq!(payload["authMode"], "sqliteSession");
|
||||
assert_eq!(payload["authMode"], "controlPlaneSession");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -535,7 +535,7 @@ mod tests {
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["userId"], "liaibo");
|
||||
assert_eq!(payload["actorType"], "admin");
|
||||
assert_eq!(payload["authMode"], "sqliteSession");
|
||||
assert_eq!(payload["authMode"], "controlPlaneSession");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -79,7 +79,7 @@ pub(crate) async fn get_tree_view_state(
|
||||
Some(&scope.source_kind),
|
||||
)
|
||||
.map_err(|error| {
|
||||
WebError::internal(format!("SQLite tree view state 读取失败: {error}"))
|
||||
WebError::internal(format!("control-plane tree view state 读取失败: {error}"))
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let Some(record) = find_tree_view_state_record(&preferences, &scope) else {
|
||||
@@ -92,14 +92,14 @@ pub(crate) async fn get_tree_view_state(
|
||||
)));
|
||||
};
|
||||
let parsed = serde_json::from_str::<Value>(&record.value_json).map_err(|error| {
|
||||
WebError::internal(format!("SQLite tree view state JSON 无效: {error}"))
|
||||
WebError::internal(format!("control-plane tree view state JSON 无效: {error}"))
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let normalized = normalize_state(&context, &scope, parsed)?;
|
||||
Ok(Json(response_payload(
|
||||
&actor_id,
|
||||
&scope,
|
||||
"sqlite",
|
||||
"control-plane",
|
||||
Some(record.revision),
|
||||
normalized,
|
||||
)))
|
||||
@@ -143,7 +143,7 @@ pub(crate) async fn put_tree_view_state(
|
||||
Ok(Json(response_payload(
|
||||
&actor_id,
|
||||
&scope,
|
||||
"sqlite",
|
||||
"control-plane",
|
||||
Some(record.revision),
|
||||
normalized,
|
||||
)))
|
||||
|
||||
@@ -3287,7 +3287,7 @@ fn annotate_local_attachment_refs_authorization(
|
||||
.control_plane()
|
||||
.resolve_access(&context.auth.actor_id, resolved_uri)
|
||||
.map_err(|error| {
|
||||
WebError::internal(format!("SQLite 控制面附件授权解析失败: {error}"))
|
||||
WebError::internal(format!("control-plane 附件授权解析失败: {error}"))
|
||||
})?;
|
||||
object.insert(
|
||||
"authorized".to_string(),
|
||||
@@ -3816,7 +3816,7 @@ mod tests {
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert sqlite reader");
|
||||
.expect("upsert control-plane reader");
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
@@ -3834,7 +3834,7 @@ mod tests {
|
||||
source: "unit-test".into(),
|
||||
created_by: None,
|
||||
})
|
||||
.expect("grant sqlite read");
|
||||
.expect("grant control-plane read");
|
||||
}
|
||||
|
||||
fn app_with_unreachable_convex_without_fixture() -> axum::Router {
|
||||
@@ -4706,7 +4706,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn page_aggregate_endpoint_allows_sqlite_granted_local_folder_read_access() {
|
||||
async fn page_aggregate_endpoint_allows_control_plane_granted_local_folder_read_access() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-page-aggregate-grant-{}",
|
||||
std::process::id()
|
||||
@@ -4753,7 +4753,7 @@ mod tests {
|
||||
Some(&root_uri),
|
||||
)
|
||||
.await
|
||||
.expect("sqlite read grant can open local page aggregate");
|
||||
.expect("control-plane read grant can open local page aggregate");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
|
||||
@@ -4766,7 +4766,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_page_aggregate_marks_attachment_refs_authorization_from_sqlite_grants() {
|
||||
async fn local_page_aggregate_marks_attachment_refs_authorization_from_control_plane_grants() {
|
||||
let root = temp_root("mnote-local-page-aggregate-attachment-auth-root");
|
||||
let allowed_external_root = temp_root("mnote-local-page-aggregate-attachment-auth-allowed");
|
||||
let denied_external_root = temp_root("mnote-local-page-aggregate-attachment-auth-denied");
|
||||
@@ -4935,7 +4935,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_folder_page_aggregate_prefers_sqlite_ui_preference_over_default_title_header() {
|
||||
async fn local_folder_page_aggregate_prefers_control_plane_ui_preference_over_default_title_header() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-page-aggregate-ui-pref-{}",
|
||||
std::process::id()
|
||||
@@ -5118,7 +5118,7 @@ mod tests {
|
||||
.await
|
||||
.expect("body");
|
||||
let get_payload: Value = serde_json::from_slice(&get_body).expect("json");
|
||||
assert_eq!(get_payload["result"]["source"], "sqlite");
|
||||
assert_eq!(get_payload["result"]["source"], "control-plane");
|
||||
assert_eq!(
|
||||
get_payload["result"]["state"]["expandedRelativePaths"][0],
|
||||
"design/05-editor-mainline"
|
||||
|
||||
@@ -20,7 +20,7 @@ pub fn AuthPage() -> impl IntoView {
|
||||
<h1 id="mnote-auth-title">"账号登录"</h1>
|
||||
<p>"登录后进入你的工作区"</p>
|
||||
</div>
|
||||
<form class="mnote-auth-form" method="post" action="/api/auth" data-auth-mode="sqlite-session">
|
||||
<form class="mnote-auth-form" method="post" action="/api/auth" data-auth-mode="control-plane-session">
|
||||
<input type="hidden" name="action" value="auth:signIn" />
|
||||
<input type="hidden" name="provider" value="password" />
|
||||
<input type="hidden" name="flow" value="signIn" data-auth-flow />
|
||||
@@ -95,7 +95,7 @@ const AUTH_SCRIPT: &str = r#"
|
||||
(function () {
|
||||
var root = document.querySelector('[data-testid="mnote-auth-page"]');
|
||||
if (!root) return;
|
||||
var form = root.querySelector('form[data-auth-mode="sqlite-session"]');
|
||||
var form = root.querySelector('form[data-auth-mode="control-plane-session"]');
|
||||
var flowInput = root.querySelector('[data-auth-flow]');
|
||||
var accountInput = root.querySelector('#account');
|
||||
var emailInput = root.querySelector('#email');
|
||||
|
||||
@@ -192,7 +192,6 @@ pub fn PageLayout(
|
||||
<span class="wolai-public-pill" data-testid="wolai-public-state" hidden></span>
|
||||
<button type="button" class="wolai-icon-button" title="星标置顶" aria-label="星标置顶" data-state="closed" aria-haspopup="true" aria-expanded="false" data-mnote-action="toggle-sidebar-shortcut" data-mnote-shortcut-kind="page"><span class="material-symbols-outlined" data-icon="star" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="演示" aria-label="演示"><span class="material-symbols-outlined" data-icon="slideshow" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="资料库问答" aria-label="资料库问答" data-testid="mnote-knowledge-rag-settings-toggle" data-mnote-action="open-knowledge-rag-settings"><span class="material-symbols-outlined" data-icon="travel_explore" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="评论" aria-label="评论"><span class="material-symbols-outlined" data-icon="comment" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="分享" aria-label="分享"><span class="material-symbols-outlined" data-icon="share" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="成员" aria-label="成员"><span class="material-symbols-outlined" data-icon="person_add" aria-hidden="true"></span></button>
|
||||
@@ -204,6 +203,7 @@ pub fn PageLayout(
|
||||
{children()}
|
||||
</article>
|
||||
<div class="wolai-floating-actions" aria-label="浮动操作">
|
||||
<a href="/knowledge" data-testid="mnote-floating-knowledge" class="wolai-floating-button wolai-floating-button--knowledge" aria-label="知识库" title="打开知识库" data-mnote-action="open-knowledge-host"><span class="material-symbols-outlined material-symbols-filled" data-icon="travel_explore" aria-hidden="true"></span></a>
|
||||
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai" aria-label="AI 助手" data-state="closed" aria-haspopup="dialog" aria-expanded="false" title="点击 进入空间智能问答" data-mnote-action="open-page-ai"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -547,8 +547,15 @@ mod tests {
|
||||
|
||||
assert!(!html.contains(r#"data-testid="mnote-local-index-settings-toggle""#));
|
||||
assert!(!html.contains(r#"data-mnote-action="open-index-settings""#));
|
||||
assert!(html.contains(r#"data-testid="mnote-knowledge-rag-settings-toggle""#));
|
||||
assert!(html.contains(r#"data-mnote-action="open-knowledge-rag-settings""#));
|
||||
assert!(!html.contains(r#"data-testid="mnote-knowledge-rag-settings-toggle""#));
|
||||
assert!(html.contains(r#"data-testid="mnote-floating-knowledge""#));
|
||||
assert!(html.contains(r#"href="/knowledge""#));
|
||||
assert!(html.contains(r#"data-mnote-action="open-knowledge-host""#));
|
||||
assert!(
|
||||
html.find(r#"data-testid="mnote-floating-knowledge""#)
|
||||
< html.find(r#"data-testid="wolai-floating-ai""#)
|
||||
);
|
||||
assert!(!html.contains(r#"data-mnote-action="open-knowledge-rag-settings""#));
|
||||
assert!(html.contains(r#"data-icon="travel_explore""#));
|
||||
assert!(crate::ssr::styles::MNOTE_CSS.contains(r#"data-icon="travel_explore""#));
|
||||
assert!(!html.contains(r#"data-testid="mnote-local-ocr-task-toggle""#));
|
||||
@@ -595,9 +602,9 @@ mod tests {
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("data-knowledge-rag-input-status-label"));
|
||||
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("localOcrAutoEnabled"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("data-mnote-local-ocr-auto-retired"));
|
||||
assert!(
|
||||
SIDEBAR_TREE_RUNTIME_JS.contains("[data-mnote-action=\"open-knowledge-rag-settings\"]")
|
||||
);
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.location.href = '/knowledge'"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("open-knowledge-rag-debug-settings"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/debug/knowledge-rag"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("[data-knowledge-rag-action]"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("useKnowledgeRagFileTreeSelection"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("reindex-source"));
|
||||
|
||||
@@ -2340,9 +2340,11 @@
|
||||
}
|
||||
|
||||
.wolai-floating-actions {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
right: 24px;
|
||||
bottom: 44px;
|
||||
flex-direction: column-reverse;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
z-index: var(--wolai-z-dock);
|
||||
}
|
||||
@@ -2366,7 +2368,8 @@
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.wolai-floating-button--ai {
|
||||
.wolai-floating-button--ai,
|
||||
.wolai-floating-button--knowledge {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.1);
|
||||
@@ -2376,6 +2379,10 @@
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.wolai-floating-button--knowledge {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.document-shell[data-page-wide-layout="true"] {
|
||||
width: min(100%, 980px);
|
||||
}
|
||||
@@ -2614,7 +2621,7 @@
|
||||
.mnote-weknora-kb-page-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 320px) minmax(0, 1fr);
|
||||
min-height: 620px;
|
||||
min-height: min(620px, calc(100vh - 190px));
|
||||
max-height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -2869,6 +2876,39 @@
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-detail-tabs {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
gap: 3px;
|
||||
padding: 3px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-detail-tab {
|
||||
min-height: 28px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #4B5563;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-detail-tab.is-active {
|
||||
background: #EAF7EA;
|
||||
color: #166534;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-detail-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnote-weknora-doc-toolbar,
|
||||
.mnote-weknora-doc-actions {
|
||||
display: flex;
|
||||
@@ -3063,21 +3103,52 @@
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta div {
|
||||
display: grid;
|
||||
grid-template-columns: 72px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
color: #8B8782;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
color: #6B7280;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta .mnote-weknora-kb-summary-card {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta .mnote-weknora-kb-summary-card div {
|
||||
border: 0;
|
||||
border-right: 1px solid #E5E7EB;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta .mnote-weknora-kb-summary-card div:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta strong {
|
||||
color: #111827;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta code {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
@@ -3140,6 +3211,15 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-row {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #EEF0F2;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-source-input-row {
|
||||
grid-template-columns: minmax(0, 1fr) 56px 28px;
|
||||
}
|
||||
@@ -3249,6 +3329,7 @@
|
||||
|
||||
.mnote-knowledge-rag-source-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -3274,6 +3355,23 @@
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.mnote-weknora-kb-page-body,
|
||||
.mnote-weknora-kb-detail-hero,
|
||||
.mnote-weknora-doc-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mnote-weknora-kb-list-pane {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.mnote-knowledge-rag-meta .mnote-weknora-kb-summary-card {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.wolai-page-settings-tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
|
||||
Reference in New Issue
Block a user