diff --git a/.github/workflows/chat_kernel_ui.yml b/.github/workflows/chat_kernel_ui.yml new file mode 100644 index 000000000..a0efe3310 --- /dev/null +++ b/.github/workflows/chat_kernel_ui.yml @@ -0,0 +1,92 @@ +name: Chat Kernel and UI gates + +on: + workflow_dispatch: + pull_request: + paths: + - 'renderer.js' + - 'main.html' + - 'modules/**' + - 'preloads/**' + - 'styles/**' + - 'tests/**' + - 'scripts/**' + - 'docs/chat-kernel-*.md' + - 'docs/contracts/**' + - 'docs/next-ui-current-state.md' + - 'package.json' + - 'package-lock.json' + - '.github/workflows/chat_kernel_ui.yml' + +permissions: + contents: read + +jobs: + contracts: + name: Chat Kernel and UI contracts + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + fetch-depth: 0 + + - name: Setup Node 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + cache: npm + + - name: Setup Rust for built-artifact evidence + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable 2026-08-11 + + - name: Install dependencies (including Electron runtime) + env: + PUPPETEER_SKIP_DOWNLOAD: 'true' + # Electron's postinstall script downloads the runtime used by the + # embedded-app contract tests. Do not suppress lifecycle scripts here; + # this job does not perform native addon rebuilds. + run: npm ci --omit=optional + + - name: Verify Electron runtime + run: node -e "const electron = require('electron'); if (typeof electron !== 'string') { throw new Error('Electron runtime path was not installed'); } console.log(electron)" + + - name: Run Chat Kernel tests + run: npm run test:chat-kernel + + - name: Run Chat contract evidence gate + env: + VCPCHAT_PACKAGED_ARTIFACT_SMOKE_OUTPUT: ${{ runner.temp }}/packaged-artifact-smoke.json + run: npm run check:chat-evidence + + - name: Write Chat evidence manifest + env: + VCPCHAT_PACKAGED_ARTIFACT_EVIDENCE_INPUT: ${{ runner.temp }}/packaged-artifact-smoke.json + run: npm run write:chat-evidence-manifest -- "$RUNNER_TEMP/chat-evidence-manifest.json" + + - name: Publish Chat evidence summary + env: + CHAT_EVIDENCE_MANIFEST: ${{ runner.temp }}/chat-evidence-manifest.json + run: node -e "const fs=require('fs'); const m=JSON.parse(fs.readFileSync(process.env.CHAT_EVIDENCE_MANIFEST,'utf8')); fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, ['## Chat evidence','', '- status: `'+m.status+'`','- contracts/events: `'+m.checks.contracts+'/'+m.checks.generatedEvents+'`','- dynamic sites: registered `'+m.checks.dynamicSites.registered+'`, undiscovered `'+m.checks.dynamicSites.undiscovered+'`','- artifact plane: `'+m.checks.artifactPlane+'`','- packaged artifact: `'+m.checks.packagedArtifact+'`','- Windows matrix: `'+m.checks.windowsMatrix+'`','- manual soak: `'+m.checks.manualSoak+'`','- real Electron: `'+m.checks.realElectronSequence+'` (manual/platform evidence remains separate)',''].join('\\n'));" + + - name: Upload Chat evidence manifest + uses: actions/upload-artifact@v4 + with: + name: chat-evidence-manifest + path: | + ${{ runner.temp }}/chat-evidence-manifest.json + ${{ runner.temp }}/packaged-artifact-smoke.json + + - name: Upload Chat evidence bundle + uses: actions/upload-artifact@v4 + with: + name: chat-evidence-bundle + if-no-files-found: error + path: | + docs/contracts/chat-contract.schema.json + docs/contracts/chat-contracts.json + docs/contracts/generated/chat-event-graph.json + docs/contracts/snapshots/*.json + + - name: Run complete UI system gate + run: npm run check:ui-system diff --git a/Flowlockmodules/flowlock-integration.js b/Flowlockmodules/flowlock-integration.js index b77e1284f..f35e250a7 100644 --- a/Flowlockmodules/flowlock-integration.js +++ b/Flowlockmodules/flowlock-integration.js @@ -3,6 +3,10 @@ // 多 Agent 并发架构:续写不再依赖当前 UI 状态,而是按指定 Agent/Topic 后台执行。 console.log('[Flowlock Integration] Loading integration script (multi-session)...'); +let chatManagerProvider = null; +let historyMutationAuthorityProvider = null; +let settingsProvider = null; +const mainChatSnapshot = () => window.VCPMainChatState?.snapshot?.() || { selectedItem: null, topicId: null }; /** * 初始化Flowlock模块 @@ -23,7 +27,7 @@ function initializeFlowlock() { electronAPI: electronAPI, uiHelper: window.uiHelperFunctions, globalSettingsRef: { - get: () => window.globalSettings || {} + get: () => settingsProvider?.get?.() || {} }, continueWritingForContext: continueWritingForContext }); @@ -58,7 +62,7 @@ function normalizeFlowlockHeartbeatPrompt(prompt) { async function continueWritingForContext(params) { const { agentId, topicId, prompt, messageId } = params; const chatAPI = window.chatAPI || window.electronAPI; - const globalSettings = window.globalSettings || {}; + const globalSettings = settingsProvider?.get?.() || {}; if (!agentId || !topicId) { console.error('[Flowlock] continueWritingForContext: missing agentId or topicId'); @@ -211,8 +215,7 @@ async function continueWritingForContext(params) { avatarColor: agentConfig?.avatarCalculatedColor }; - const currentSelectedItem = window.currentSelectedItem; - const currentTopicId = window.currentTopicId; + const { selectedItem: currentSelectedItem, topicId: currentTopicId } = mainChatSnapshot(); const isForCurrentView = currentSelectedItem?.id === agentId && currentTopicId === topicId; // 构建上下文。后台流也必须带完整身份,streamManager 才能写入正确历史。 @@ -225,29 +228,21 @@ async function continueWritingForContext(params) { avatarColor: agentConfig?.avatarCalculatedColor }; - // 流式消息必须在发送请求前初始化,无论它当前是否可见。 - // streamManager 自己负责“当前视图渲染 / 后台只写历史”的分流。 - if (useStreaming) { - const streamStarter = window.streamManager?.startStreamingMessage - || window.messageRenderer?.startStreamingMessage; - if (typeof streamStarter !== 'function') { - throw new Error('流式消息管理器未就绪'); - } - - await streamStarter({ - ...thinkingMessage, - content: '', - agentId, - topicId, - context - }); - } else { + // Streaming lifecycle is now started by the VCP stream bridge when the + // producer publishes its first owned event. This prevents Flowlock from + // creating a second terminal state machine for the same message. + if (!useStreaming) { let historyWithThinking = await chatAPI.getChatHistory(agentId, topicId); if (!historyWithThinking || historyWithThinking.error) { historyWithThinking = []; } historyWithThinking.push(thinkingMessage); - await chatAPI.saveChatHistory(agentId, topicId, historyWithThinking); + await historyMutationAuthorityProvider.replace({ + itemId: agentId, + itemType: 'agent', + topicId, + category: 'flowlock-start' + }, historyWithThinking); } // 发送到 VCP @@ -284,10 +279,15 @@ async function continueWritingForContext(params) { if (historyForSave && !historyForSave.error) { const finalHistory = historyForSave.filter(msg => msg.id !== thinkingMessageId && !msg.isThinking); finalHistory.push(assistantMessage); - await chatAPI.saveChatHistory(agentId, topicId, finalHistory); - - if (isForCurrentView && window.chatManager?.loadChatHistory) { - await window.chatManager.loadChatHistory(agentId, 'agent', topicId); + await historyMutationAuthorityProvider.replace({ + itemId: agentId, + itemType: 'agent', + topicId, + category: 'flowlock-terminal' + }, finalHistory); + + if (isForCurrentView && chatManagerProvider?.loadChatHistory) { + await chatManagerProvider.loadChatHistory(agentId, 'agent', topicId); } } @@ -323,8 +323,7 @@ function setupFlowlockInteractions() { e.preventDefault(); - const currentItem = window.currentSelectedItem; - const currentTopic = window.currentTopicId; + const { selectedItem: currentItem, topicId: currentTopic } = mainChatSnapshot(); if (!currentItem || !currentItem.id || !currentTopic) { if (window.uiHelperFunctions?.showToastNotification) { @@ -351,8 +350,7 @@ function setupFlowlockInteractions() { if (!window.flowlockManager) return; - const currentItem = window.currentSelectedItem; - const currentTopic = window.currentTopicId; + const { selectedItem: currentItem, topicId: currentTopic } = mainChatSnapshot(); if (window.flowlockManager.isAgentLocked(currentItem.id)) { // 如果已激活,则停止 @@ -384,8 +382,7 @@ function setupFlowlockShortcuts() { if (!window.flowlockManager) return; - const currentItem = window.currentSelectedItem; - const currentTopic = window.currentTopicId; + const { selectedItem: currentItem, topicId: currentTopic } = mainChatSnapshot(); if (!currentItem || !currentItem.id || !currentTopic) { if (window.uiHelperFunctions?.showToastNotification) { @@ -410,18 +407,40 @@ function setupFlowlockShortcuts() { /** * 主初始化函数 */ -function initializeFlowlockIntegration() { +function initializeFlowlockIntegration(dependencies = {}) { try { + const provider = dependencies.chatManager; + if (!provider || typeof provider.loadChatHistory !== 'function' || provider.isReady?.() === false) { + throw new TypeError('Flowlock integration requires a ready ChatManager provider.'); + } + const mutationAuthority = dependencies.historyMutationAuthority; + if (!mutationAuthority || typeof mutationAuthority.replace !== 'function') { + throw new TypeError('Flowlock integration requires a history mutation authority.'); + } + if (chatManagerProvider && chatManagerProvider !== provider) { + throw new Error('Flowlock ChatManager provider is already registered.'); + } + if (historyMutationAuthorityProvider && historyMutationAuthorityProvider !== mutationAuthority) { + throw new Error('Flowlock history mutation authority is already registered.'); + } + if (!dependencies.settings || typeof dependencies.settings.get !== 'function') { + throw new TypeError('Flowlock integration requires a settings provider.'); + } + chatManagerProvider = provider; + historyMutationAuthorityProvider = mutationAuthority; + settingsProvider = dependencies.settings; initializeFlowlock(); setupFlowlockInteractions(); setupFlowlockShortcuts(); // 页面卸载时清理 - window.addEventListener('beforeunload', () => { + const cleanup = () => { if (window.flowlockManager) { window.flowlockManager.cleanup(); } - }); + }; + if (dependencies.listenerOwner?.add) dependencies.listenerOwner.add(window, 'beforeunload', cleanup, { once: true }); + else window.addEventListener('beforeunload', cleanup, { once: true }); console.log('[Flowlock Integration] Full integration complete (multi-session).'); } catch (error) { @@ -432,4 +451,4 @@ function initializeFlowlockIntegration() { // 导出到全局作用域 window.initializeFlowlockIntegration = initializeFlowlockIntegration; -console.log('[Flowlock Integration] Integration script loaded (multi-session).'); \ No newline at end of file +console.log('[Flowlock Integration] Integration script loaded (multi-session).'); diff --git a/Flowlockmodules/flowlock.js b/Flowlockmodules/flowlock.js index 8260715ce..242825118 100644 --- a/Flowlockmodules/flowlock.js +++ b/Flowlockmodules/flowlock.js @@ -115,7 +115,7 @@ class FlowlockManager { */ async stop(agentId) { // 兼容旧插件 stop():优先停止当前可见 Agent,不再错误地停止其他并发 Session。 - const resolvedAgentId = agentId || window.currentSelectedItem?.id; + const resolvedAgentId = agentId || window.VCPMainChatState?.snapshot?.()?.selectedItem?.id; const session = this.sessions.get(resolvedAgentId); agentId = resolvedAgentId; if (!session) { @@ -635,7 +635,7 @@ class FlowlockManager { * 切换 Agent 后也可无参数调用,以当前可见 Agent 的真实 Session 为准。 */ syncCurrentHeaderIndicator() { - const currentItem = window.currentSelectedItem; + const currentItem = window.VCPMainChatState?.snapshot?.()?.selectedItem; const header = document.getElementById('currentChatAgentName'); if (!header) return; @@ -651,7 +651,7 @@ class FlowlockManager { } updateCurrentHeaderIndicator(agentId, active) { - const currentItem = window.currentSelectedItem; + const currentItem = window.VCPMainChatState?.snapshot?.()?.selectedItem; if (currentItem?.type !== 'agent' || currentItem.id !== agentId) return; const header = document.getElementById('currentChatAgentName'); @@ -667,7 +667,7 @@ class FlowlockManager { } triggerCurrentHeaderHeartbeat(agentId) { - const currentItem = window.currentSelectedItem; + const currentItem = window.VCPMainChatState?.snapshot?.()?.selectedItem; if (currentItem?.type !== 'agent' || currentItem.id !== agentId) return; const header = document.getElementById('currentChatAgentName'); @@ -747,4 +747,4 @@ const flowlockManager = new FlowlockManager(); // 导出到window对象供其他模块使用 window.flowlockManager = flowlockManager; -console.log('[Flowlock] Manager instance created and exposed globally (multi-session).'); \ No newline at end of file +console.log('[Flowlock] Manager instance created and exposed globally (multi-session).'); diff --git a/VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js b/VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js index e99353423..95ad0444e 100644 --- a/VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js +++ b/VCPDistributedServer/Plugin/VChatAutoTTS/plugin.js @@ -28,7 +28,7 @@ } function currentAgent() { - const item = window.currentSelectedItem; + const item = window.VCPMainChatState?.snapshot?.()?.selectedItem; return item?.type === 'agent' && item.id ? item : null; } diff --git a/Voicechatmodules/voicechat.html b/Voicechatmodules/voicechat.html index 80a5c315e..6f1de3daf 100644 --- a/Voicechatmodules/voicechat.html +++ b/Voicechatmodules/voicechat.html @@ -48,7 +48,6 @@