chore: land tree view-state, vault, Pi module split, and repo hygiene
Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
This commit is contained in:
@@ -300,7 +300,14 @@
|
||||
return vm;
|
||||
}
|
||||
if (event.type === 'assistant_abort') {
|
||||
// S4: abort is idempotent — second apply (SSE after optimistic) must not mutate again.
|
||||
if (vm.runtimeStatus === STATE_ABORTED && vm.abortResponse) {
|
||||
return vm;
|
||||
}
|
||||
var abortMsg = vm.currentAssistant || findLastAssistant(vm) || ensurePiRunAssistant(vm);
|
||||
if (abortMsg.status === 'aborted' && !vm.currentAssistant) {
|
||||
return vm;
|
||||
}
|
||||
abortMsg.status = 'aborted';
|
||||
abortMsg.abortReason = event.reason || payload.reason || payload.stopReason || payload.error || payload.message || '已中止';
|
||||
abortMsg.abortResponse = payload || { reason: abortMsg.abortReason };
|
||||
@@ -3572,15 +3579,31 @@
|
||||
])
|
||||
.then(function (results) {
|
||||
var data = results[0] || {};
|
||||
var effectiveData = results[1] || null;
|
||||
piLabState.enabled = !!data.enabled;
|
||||
piLabState.sessionId = data.sessionId || piLabState.sessionId || null;
|
||||
piLabState.providerSessionId = data.providerSessionId || piLabState.providerSessionId || null;
|
||||
// Consume backend status/default fields when present.
|
||||
if (data.defaultModelProvider) piLabState.defaultModelProvider = data.defaultModelProvider;
|
||||
if (data.defaultModelId) piLabState.defaultModelId = data.defaultModelId;
|
||||
if (data.defaultThinkingLevel) piLabState.defaultThinkingLevel = normalizeThinkingLevel(data.defaultThinkingLevel);
|
||||
piLabState.modelProvider = (data.session && data.session.modelProvider) || piLabState.modelProvider || null;
|
||||
piLabState.modelId = (data.session && data.session.modelId) || piLabState.modelId || null;
|
||||
// S1: prefer effective AI settings as management-surface truth; status is
|
||||
// fallback only when effective load failed. Do not let env-stale status
|
||||
// overwrite a successfully loaded effective defaultModel.
|
||||
if (effectiveData) {
|
||||
applyEffectiveConfig(effectiveData);
|
||||
} else {
|
||||
if (data.defaultModelProvider) piLabState.defaultModelProvider = data.defaultModelProvider;
|
||||
if (data.defaultModelId) piLabState.defaultModelId = data.defaultModelId;
|
||||
}
|
||||
if (data.defaultThinkingLevel && !piLabState.defaultThinkingLevel) {
|
||||
piLabState.defaultThinkingLevel = normalizeThinkingLevel(data.defaultThinkingLevel);
|
||||
}
|
||||
// Active session model wins; otherwise keep effective defaults already applied.
|
||||
piLabState.modelProvider = (data.session && data.session.modelProvider)
|
||||
|| piLabState.modelProvider
|
||||
|| piLabState.defaultModelProvider
|
||||
|| null;
|
||||
piLabState.modelId = (data.session && data.session.modelId)
|
||||
|| piLabState.modelId
|
||||
|| piLabState.defaultModelId
|
||||
|| null;
|
||||
var sessionThinkingLevel = data.session && data.session.thinkingLevel;
|
||||
if (sessionThinkingLevel) {
|
||||
piLabState.thinkingLevel = normalizeThinkingLevel(sessionThinkingLevel);
|
||||
@@ -3851,6 +3874,8 @@
|
||||
es.close();
|
||||
piLabEventSource = null;
|
||||
}
|
||||
// S4: optimistic abort already applied UI; ignore duplicate SSE abort.
|
||||
if (piLabState.status === STATE_ABORTED) return;
|
||||
applyPiRunEvent({ type: 'assistant_abort', payload: payload });
|
||||
setState(STATE_ABORTED);
|
||||
updateMessages();
|
||||
@@ -4056,9 +4081,12 @@
|
||||
var deltaToolPayload = assistantToolCallPayload(msgData);
|
||||
if (deltaToolPayload) upsertToolCall(deltaToolPayload, 'running', { args: deltaToolPayload.args || {} });
|
||||
} else if (eventType === 'response' && payload.command === 'abort') {
|
||||
applyPiRunEvent({ type: 'assistant_abort', payload: payload });
|
||||
setState(STATE_ABORTED);
|
||||
updateMessages();
|
||||
// S4: ignore duplicate abort after optimistic UI apply.
|
||||
if (piLabState.status !== STATE_ABORTED) {
|
||||
applyPiRunEvent({ type: 'assistant_abort', payload: payload });
|
||||
setState(STATE_ABORTED);
|
||||
updateMessages();
|
||||
}
|
||||
} else if (eventType === 'citation' || msgDataType === 'citation') {
|
||||
applyPiRunEvent({ type: 'citation', payload: { source: payload.source || msgData.source || '', title: payload.title || msgData.title || '', url: payload.url || msgData.url || '#' } });
|
||||
updateMessages();
|
||||
@@ -4088,15 +4116,132 @@
|
||||
}
|
||||
}
|
||||
|
||||
function pageAiBlockingDirtyStateLocal(dirtyState) {
|
||||
var state = String(dirtyState || '').trim();
|
||||
var normalized = state.toLowerCase();
|
||||
if (normalized === 'dirty') return 'Dirty';
|
||||
if (normalized === 'stale') return 'Stale';
|
||||
if (normalized === 'deleted') return 'Deleted';
|
||||
if (normalized === 'externalmodified' || normalized === 'external-change-conflict' || normalized === 'hasexternalconflict') return 'ExternalModified';
|
||||
return '';
|
||||
}
|
||||
|
||||
function buildPiLabAgentTargetPackage(context) {
|
||||
var active = activeEditorSnapshot() || {};
|
||||
var activeWorkspacePath = active.workspacePath && typeof active.workspacePath === 'object' ? active.workspacePath : {};
|
||||
var relativePath = normalizeSlashes(context && context.pagePath || activeWorkspacePath.relativePath || active.path || '');
|
||||
var rootUri = String(context && context.rootUri || activeWorkspacePath.rootUri || active.rootUri || '').trim();
|
||||
var workspaceId = String(context && context.workspaceId || activeWorkspacePath.workspaceId || active.workspaceId || '').trim();
|
||||
var documentId = String(context && context.pageId || active.documentId || activeWorkspacePath.documentId || '').trim();
|
||||
var resourceKind = String(activeWorkspacePath.resourceKind || active.resourceKind || active.kind || 'markdown_page').trim() || 'markdown_page';
|
||||
var objectIdentity = String(active.objectIdentity || activeWorkspacePath.objectIdentity || documentId || relativePath || '').trim();
|
||||
var onlyofficeSessionId = String(active.onlyofficeSessionId || active.bridgeSessionId || '').trim();
|
||||
var dirtyState = String(active.dirtyState || active.dirtyGuard || '').trim();
|
||||
var primaryTargetId = objectIdentity || documentId || relativePath;
|
||||
var workspacePath = {
|
||||
schema: 'mnote.workspace_path.v1',
|
||||
workspaceId: workspaceId,
|
||||
sourceKind: 'local_folder',
|
||||
rootUri: rootUri,
|
||||
relativePath: relativePath,
|
||||
documentId: documentId,
|
||||
objectIdentity: objectIdentity,
|
||||
resourceKind: resourceKind,
|
||||
};
|
||||
var targetEntry = {
|
||||
targetId: primaryTargetId,
|
||||
objectIdentity: objectIdentity,
|
||||
documentId: documentId,
|
||||
workspaceId: workspaceId,
|
||||
sourceKind: 'local_folder',
|
||||
rootUri: rootUri,
|
||||
relativePath: relativePath,
|
||||
resourceKind: resourceKind,
|
||||
dirtyState: dirtyState,
|
||||
onlyofficeSessionId: onlyofficeSessionId,
|
||||
bridgeSessionId: onlyofficeSessionId,
|
||||
paneRole: active.paneRole || 'primary',
|
||||
title: context && context.pageTitle || active.title || '',
|
||||
policy: {
|
||||
permission: relativePath ? 'read_write' : 'read',
|
||||
writeRequiresCleanBuffer: true,
|
||||
conflictPolicy: 'fail_on_dirty_or_stale'
|
||||
}
|
||||
};
|
||||
return {
|
||||
schema: 'mnote.agent_target_package.v1',
|
||||
source: 'pi_lab_send_target_snapshot',
|
||||
frozenAt: Date.now(),
|
||||
primaryTargetId: primaryTargetId,
|
||||
onlyofficeSessionId: onlyofficeSessionId,
|
||||
bridgeSessionId: onlyofficeSessionId,
|
||||
workspaceId: workspaceId,
|
||||
sourceKind: 'local_folder',
|
||||
rootUri: rootUri,
|
||||
documentId: documentId,
|
||||
objectIdentity: objectIdentity,
|
||||
resourceKind: resourceKind,
|
||||
workspacePath: workspacePath,
|
||||
currentFile: relativePath ? {
|
||||
rootUri: rootUri,
|
||||
relativePath: relativePath,
|
||||
documentId: documentId,
|
||||
objectIdentity: objectIdentity,
|
||||
resourceKind: resourceKind
|
||||
} : null,
|
||||
allowedFiles: relativePath ? [relativePath] : [],
|
||||
targets: [targetEntry],
|
||||
policy: {
|
||||
writeRequiresExplicitTarget: true,
|
||||
allowedFilesSource: 'selected_page_ai_target',
|
||||
conflictPolicy: 'fail_on_dirty_or_stale'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function fetchPiLabTargetBufferState(context, targetPackage) {
|
||||
var workspacePath = targetPackage && targetPackage.workspacePath || {};
|
||||
var documentId = String(context && context.pageId || workspacePath.documentId || '').trim();
|
||||
var rootUri = String(context && context.rootUri || workspacePath.rootUri || '').trim();
|
||||
if (!documentId || !rootUri) return Promise.resolve(null);
|
||||
var relativePath = String(workspacePath.relativePath || context && context.pagePath || '').trim();
|
||||
var url = new URL('/api/documents/buffer-state', window.location.origin);
|
||||
url.searchParams.set('documentId', documentId);
|
||||
url.searchParams.set('sourceKind', 'local_folder');
|
||||
url.searchParams.set('rootUri', rootUri);
|
||||
var workspaceId = String(context && context.workspaceId || workspacePath.workspaceId || '').trim();
|
||||
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
|
||||
if (relativePath) url.searchParams.set('relativePath', relativePath);
|
||||
return fetch(url.toString(), { cache: 'no-store', credentials: 'same-origin', headers: { accept: 'application/json' } })
|
||||
.then(function (response) {
|
||||
return response.json().catch(function () { return null; }).then(function (payload) {
|
||||
if (!response.ok || !payload || payload.ok !== true) return null;
|
||||
return payload.result || null;
|
||||
});
|
||||
})
|
||||
.catch(function () { return null; });
|
||||
}
|
||||
|
||||
function assertPiLabTargetWritable(context, targetPackage) {
|
||||
var active = activeEditorSnapshot() || {};
|
||||
var snapshotDirty = pageAiBlockingDirtyStateLocal(active.dirtyState || active.dirtyGuard);
|
||||
return fetchPiLabTargetBufferState(context, targetPackage).then(function (bufferState) {
|
||||
var bufferDirty = pageAiBlockingDirtyStateLocal(bufferState && bufferState.dirtyState);
|
||||
var blockedState = bufferDirty || snapshotDirty;
|
||||
if (!blockedState) return bufferState;
|
||||
var error = new Error('目标文档存在未保存或外部变更状态(' + blockedState + '),请先保存、解决冲突或刷新后再让 AI 写入。');
|
||||
error.code = 'page_ai_target_buffer_not_writable';
|
||||
error.documentId = String(context && context.pageId || '').trim();
|
||||
error.dirtyState = blockedState;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function sendPrompt(text, options) {
|
||||
var prompt = String(text || '').trim();
|
||||
if (!prompt) return;
|
||||
if (!prompt) return Promise.resolve();
|
||||
var streamingBehavior = options && options.streamingBehavior;
|
||||
var midStream = piLabState.status === STATE_STREAMING && streamingAssistantMsg;
|
||||
applyPiRunEvent({ type: 'user_prompt', text: prompt, meta: midStream ? (streamingBehavior === 'followUp' ? 'follow-up queued' : 'steer') : '' });
|
||||
if (!midStream) applyPiRunEvent({ type: 'assistant_begin' });
|
||||
setState(STATE_STREAMING);
|
||||
updateMessages();
|
||||
var context = applyCurrentContextToState();
|
||||
var contextSelection = selectedContextPayload(context);
|
||||
var refs = contextSelection.refs || [];
|
||||
@@ -4104,27 +4249,39 @@
|
||||
var includeAnyContext = refs.length > 0;
|
||||
var includePageContext = refs.indexOf('current_page') >= 0 || refs.indexOf('selection') >= 0 || refs.indexOf('lightrag') >= 0;
|
||||
var includeFolderContext = refs.indexOf('folder') >= 0;
|
||||
var body = {
|
||||
sessionId: piLabState.sessionId,
|
||||
message: prompt,
|
||||
rootUri: includeAnyContext ? (context.rootUri || undefined) : undefined,
|
||||
workspaceId: includeAnyContext ? (context.workspaceId || undefined) : undefined,
|
||||
pagePath: includePageContext ? (context.pagePath || undefined) : undefined,
|
||||
pageTitle: includePageContext ? (context.pageTitle || undefined) : undefined,
|
||||
folderPath: includeFolderContext && selectedFolderPath !== null ? selectedFolderPath : undefined,
|
||||
contextRefs: contextSelection.refs,
|
||||
selectedContext: contextSelection,
|
||||
};
|
||||
if (streamingBehavior) body.streamingBehavior = streamingBehavior;
|
||||
if (piLabState.pendingImages && piLabState.pendingImages.length) {
|
||||
body.images = piLabState.pendingImages;
|
||||
piLabState.pendingImages = [];
|
||||
}
|
||||
return fetch(API.SEND, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
var targetPackage = buildPiLabAgentTargetPackage(context);
|
||||
// S7: block send when open target buffer is dirty/stale (fail_on_dirty_or_stale).
|
||||
var writableGate = (context && context.pagePath)
|
||||
? assertPiLabTargetWritable(context, targetPackage)
|
||||
: Promise.resolve(null);
|
||||
return writableGate.then(function () {
|
||||
applyPiRunEvent({ type: 'user_prompt', text: prompt, meta: midStream ? (streamingBehavior === 'followUp' ? 'follow-up queued' : 'steer') : '' });
|
||||
if (!midStream) applyPiRunEvent({ type: 'assistant_begin' });
|
||||
setState(STATE_STREAMING);
|
||||
updateMessages();
|
||||
var body = {
|
||||
sessionId: piLabState.sessionId,
|
||||
message: prompt,
|
||||
rootUri: includeAnyContext ? (context.rootUri || undefined) : undefined,
|
||||
workspaceId: includeAnyContext ? (context.workspaceId || undefined) : undefined,
|
||||
pagePath: includePageContext ? (context.pagePath || undefined) : undefined,
|
||||
pageTitle: includePageContext ? (context.pageTitle || undefined) : undefined,
|
||||
folderPath: includeFolderContext && selectedFolderPath !== null ? selectedFolderPath : undefined,
|
||||
contextRefs: contextSelection.refs,
|
||||
selectedContext: contextSelection,
|
||||
targetPackage: targetPackage,
|
||||
};
|
||||
if (streamingBehavior) body.streamingBehavior = streamingBehavior;
|
||||
if (piLabState.pendingImages && piLabState.pendingImages.length) {
|
||||
body.images = piLabState.pendingImages;
|
||||
piLabState.pendingImages = [];
|
||||
}
|
||||
return fetch(API.SEND, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
})
|
||||
.then(function (r) {
|
||||
return responseJsonOrError(r, 'Send failed');
|
||||
@@ -4134,7 +4291,15 @@
|
||||
if (!piLabEventSource || piLabEventSource.readyState === EventSource.CLOSED) connectEventSource(data.sessionId || piLabState.sessionId);
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (streamingAssistantMsg) applyPiRunEvent({ type: 'assistant_error', message: err.message });
|
||||
var message = err && err.message ? err.message : 'Send failed';
|
||||
if (err && err.code === 'page_ai_target_buffer_not_writable') {
|
||||
showPiToast(message, 'warning');
|
||||
updateDiagnostics(message);
|
||||
setState(piLabState.sessionId ? STATE_STARTED : STATE_IDLE);
|
||||
updateMessages();
|
||||
return;
|
||||
}
|
||||
if (streamingAssistantMsg) applyPiRunEvent({ type: 'assistant_error', message: message });
|
||||
setState(STATE_STARTED);
|
||||
updateMessages();
|
||||
});
|
||||
@@ -4330,23 +4495,19 @@
|
||||
piLabEventSource.close();
|
||||
piLabEventSource = null;
|
||||
}
|
||||
if (piLabState.sessionId) {
|
||||
fetch(API.ABORT, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId: piLabState.sessionId }),
|
||||
}).then(function (response) {
|
||||
return response.json().catch(function () { return {}; });
|
||||
}).then(function (payload) {
|
||||
applyPiRunEvent({ type: 'assistant_abort', payload: Object.assign({ stopReason: 'aborted' }, payload || {}) });
|
||||
updateMessages();
|
||||
}).catch(function () {});
|
||||
}
|
||||
applyPiRunEvent({ type: 'assistant_abort', payload: { reason: '已中止', stopReason: 'aborted' } });
|
||||
setState(STATE_ABORTED);
|
||||
updateMessages();
|
||||
}
|
||||
// S4: apply abort UI state exactly once (optimistic). Do not re-apply from ABORT response.
|
||||
applyPiRunEvent({ type: 'assistant_abort', payload: { reason: '已中止', stopReason: 'aborted' } });
|
||||
setState(STATE_ABORTED);
|
||||
updateMessages();
|
||||
if (piLabState.sessionId) {
|
||||
fetch(API.ABORT, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId: piLabState.sessionId }),
|
||||
}).catch(function () {});
|
||||
}
|
||||
}
|
||||
|
||||
function startNewConversation() {
|
||||
var previousSessionId = piLabState.sessionId;
|
||||
|
||||
Reference in New Issue
Block a user