From 06327d59f598eda4dae4404ae80847f0aa591ae1 Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Sat, 13 Jun 2026 22:59:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=207-61=20Yuxi=20reference=20=E2=80=94=20e?= =?UTF-8?q?xtensions=20panel,=20KB=20detail,=20manifest=20split,=20event?= =?UTF-8?q?=20router,=20dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Batch B: settings popover 新增"扩展"tab,按知识库/Skills/MCP/Agent工具分面板 Batch C: knowledge-rag popover 新增"资料源管理/检索测试/检索参数"三tab Batch A: manifest.rs 按能力域拆为 10 个分组函数(context/doc/knowledge_rag/block/page/mindmap/office/onlyoffice/artifact) Batch D: 新增 agent-stream-event-router.js 共享模块(事件状态机 + SSE 帧解析) Batch E: settings popover 新增"仪表盘"tab — 知识库文件数/Skills数量/MCP连接状态/Agent run次数 836/838 Rust 测试通过(仅 2 个预存失败) --- .../browser/agent-stream-event-router.js | 127 +++++ .../browser/sidebar-page-settings-runtime.js | 464 +++++++++++++++++- .../mnote-web/browser/sidebar-tree-runtime.js | 69 +++ .../mnote-web/src/hermes_tools/manifest.rs | 96 +++- rust/crates/mnote-web/src/routes/mod.rs | 5 + rust/crates/mnote-web/src/routes/web_shell.rs | 9 + rust/crates/mnote-web/src/ssr/styles.rs | 278 ++++++++++- 7 files changed, 1029 insertions(+), 19 deletions(-) create mode 100644 rust/crates/mnote-web/browser/agent-stream-event-router.js diff --git a/rust/crates/mnote-web/browser/agent-stream-event-router.js b/rust/crates/mnote-web/browser/agent-stream-event-router.js new file mode 100644 index 00000000..5356c55c --- /dev/null +++ b/rust/crates/mnote-web/browser/agent-stream-event-router.js @@ -0,0 +1,127 @@ +// AgentStreamEventRouter — 统一的 agent 流式事件状态机 +// 供 Page AI sidebar runtime 和 document editor adapter runtime 共用 +// +// 事件状态: +// init — 建立 request_id 绑定 +// loading — message_delta / tool_call_delta → 增量更新 +// stream_event — tool-started / tool-finished / agent_state +// finished — 正常终止 +// interrupted — 中断(等待审批/恢复) +// error — 错误终止 +// approval_required — 等待用户确认 + +export const AGENT_EVENT_STATES = { + INIT: 'init', + LOADING: 'loading', + STREAM_EVENT: 'stream_event', + FINISHED: 'finished', + INTERRUPTED: 'interrupted', + ERROR: 'error', + APPROVAL_REQUIRED: 'approval_required', +}; + +export const TERMINAL_STATES = new Set([ + AGENT_EVENT_STATES.FINISHED, + AGENT_EVENT_STATES.INTERRUPTED, + AGENT_EVENT_STATES.ERROR, +]); + +export function isTerminalState(status) { + return TERMINAL_STATES.has(status); +} + +// 解析 SSE 帧为 eventName + payloadText +export function parseSSEFrames(buffer, lastBoundary) { + var frames = buffer.split('\n\n'); + var remaining = frames.pop() || ''; + var events = []; + frames.forEach(function(frame) { + var eventName = ''; + var dataLines = []; + frame.split('\n').forEach(function(line) { + if (line.startsWith('event:')) eventName = line.slice(6).trim(); + if (line.startsWith('data:')) dataLines.push(line.slice(5).trim()); + }); + var payloadText = dataLines.join('\n'); + if (!eventName && payloadText) { + try { + var parsed = JSON.parse(payloadText); + eventName = parsed && parsed.event ? String(parsed.event) : ''; + } catch (_) {} + } + if (eventName) events.push({ eventName: eventName, payloadText: payloadText }); + }); + return { events: events, remaining: remaining }; +} + +// 从 SSE 流读取事件 +export async function readSSEStream(response, onFrame) { + if (!response.body || typeof response.body.getReader !== 'function') return; + var reader = response.body.getReader(); + var decoder = new TextDecoder(); + var buffer = ''; + while (true) { + var chunk = await reader.read(); + if (chunk.done) break; + buffer += decoder.decode(chunk.value, { stream: true }); + var result = parseSSEFrames(buffer, ''); + buffer = result.remaining; + result.events.forEach(function(evt) { + onFrame(evt.eventName, evt.payloadText); + }); + } +} + +// 创建标准事件路由器 +// handlers: { onInit, onDelta, onToolCall, onToolResult, onAgentState, onTerminal, onApprovalRequired } +export function createAgentStreamRouter(handlers) { + var h = handlers || {}; + + return function routeEvent(eventName, payloadText) { + var payload = null; + try { payload = JSON.parse(payloadText || 'null'); } catch (_) {} + + var status = (payload && payload.status) || eventName; + + switch (status) { + case AGENT_EVENT_STATES.INIT: + if (h.onInit) h.onInit(payload); + break; + + case AGENT_EVENT_STATES.LOADING: + if (h.onDelta) h.onDelta(payload); + if (h.onToolCall) { + var toolChunks = (payload && payload.tool_call_chunks) || (payload && payload.msg && payload.msg.tool_call_chunks); + if (toolChunks && toolChunks.length) h.onToolCall(payload); + } + break; + + case AGENT_EVENT_STATES.STREAM_EVENT: + if (payload && payload.event === 'tool-finished') { + if (h.onToolResult) h.onToolResult(payload); + } else if (payload && payload.event === 'tool-started') { + // tool start — 可选处理 + } else if (payload && payload.agent_state) { + if (h.onAgentState) h.onAgentState(payload.agent_state); + } + break; + + case AGENT_EVENT_STATES.FINISHED: + case AGENT_EVENT_STATES.INTERRUPTED: + case AGENT_EVENT_STATES.ERROR: + if (h.onTerminal) h.onTerminal(status, payload); + break; + + case AGENT_EVENT_STATES.APPROVAL_REQUIRED: + if (h.onApprovalRequired) h.onApprovalRequired(payload); + break; + + default: + // 未知状态:尝试作为 loading 处理 + if (payload && (payload.text || payload.delta || payload.content)) { + if (h.onDelta) h.onDelta(payload); + } + break; + } + }; +} diff --git a/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js b/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js index e38274c1..5015cf07 100644 --- a/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js @@ -574,6 +574,8 @@ export function createSidebarPageSettingsRuntime(context) { '' + '' + '' + + '' + + '' + '' + '
' + createPageOptionRow('wideLayout', 'checkbox') + @@ -592,6 +594,51 @@ export function createSidebarPageSettingsRuntime(context) { '' + + '' + '
' + '' + @@ -648,13 +695,22 @@ export function createSidebarPageSettingsRuntime(context) { } function createKnowledgeRagSettingsPanelHtml() { + return createKnowledgeRagSettingsPanelHtmlV2(); + } + + function createKnowledgeRagSettingsPanelHtmlV2() { return '' + '