From 92289b17ed2ebd9f2f8aafdd0bf776c9a3d4b160 Mon Sep 17 00:00:00 2001 From: wangyunlong <3080433063@qq.com> Date: Fri, 4 Sep 2026 15:22:37 +0800 Subject: [PATCH] fix: wait for slot declaration via slots.inject and id list-slot entries --- workbench/plugin/client/src/index.jsx | 18 ++++++++++++++++-- workbench/plugin/lib/client.js | 16 ++++++++++++++-- workbench/plugin/lib/client.js.map | 4 ++-- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/workbench/plugin/client/src/index.jsx b/workbench/plugin/client/src/index.jsx index e6b14a7..42a9e52 100644 --- a/workbench/plugin/client/src/index.jsx +++ b/workbench/plugin/client/src/index.jsx @@ -60,12 +60,26 @@ function WorkbenchOverlay() { export function apply(ctx) { injectStyles() + // 插槽须等宿主声明后才能注册:slots.inject 会等待座位声明(顺序安全), + // 直接 register 在我们先于 ui-sidebar/ui-layout 加载时会报“slot is not declared”。 ctx.effect( - () => ctx.slots.register({ name: 'sidebar.footer.action' }, WorkbenchFooterAction), + () => + ctx.slots.inject('sidebar.footer.action', () => + ctx.slots.register( + { name: 'sidebar.footer.action', id: 'gyxx-workbench.footer' }, + WorkbenchFooterAction, + ), + ), 'gyxx-workbench: footer action', ) ctx.effect( - () => ctx.slots.register({ name: 'shell.overlay' }, WorkbenchOverlay), + () => + ctx.slots.inject('shell.overlay', () => + ctx.slots.register( + { name: 'shell.overlay', id: 'gyxx-workbench.overlay' }, + WorkbenchOverlay, + ), + ), 'gyxx-workbench: overlay', ) } diff --git a/workbench/plugin/lib/client.js b/workbench/plugin/lib/client.js index 6000fa6..ab80fa2 100644 --- a/workbench/plugin/lib/client.js +++ b/workbench/plugin/lib/client.js @@ -556,11 +556,23 @@ function WorkbenchOverlay() { function apply(ctx) { injectStyles(); ctx.effect( - () => ctx.slots.register({ name: "sidebar.footer.action" }, WorkbenchFooterAction), + () => ctx.slots.inject( + "sidebar.footer.action", + () => ctx.slots.register( + { name: "sidebar.footer.action", id: "gyxx-workbench.footer" }, + WorkbenchFooterAction + ) + ), "gyxx-workbench: footer action" ); ctx.effect( - () => ctx.slots.register({ name: "shell.overlay" }, WorkbenchOverlay), + () => ctx.slots.inject( + "shell.overlay", + () => ctx.slots.register( + { name: "shell.overlay", id: "gyxx-workbench.overlay" }, + WorkbenchOverlay + ) + ), "gyxx-workbench: overlay" ); } diff --git a/workbench/plugin/lib/client.js.map b/workbench/plugin/lib/client.js.map index 254c15e..2dbb036 100644 --- a/workbench/plugin/lib/client.js.map +++ b/workbench/plugin/lib/client.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../client/src/index.jsx", "../client/src/Panel.jsx", "../client/src/store.js", "../client/src/api.js", "../client/src/Toasts.jsx", "../client/src/styles.css"], - "sourcesContent": ["/**\n * GYXX \u5DE5\u4F5C\u53F0\u5BA2\u6237\u7AEF\u63D2\u4EF6\u5165\u53E3\u3002\n * \u7531 esbuild \u6253\u5305\u4E3A CJS \u5E76\u901A\u8FC7 window.__ModuleLoader__ \u6CE8\u518C\uFF08\u89C1 scripts/build-client.mjs\uFF09\u3002\n * \u8FD0\u884C\u65F6\u53EA\u4F9D\u8D56 dsh \u5E73\u53F0\u5171\u4EAB\u6A21\u5757\u8868\u4E2D\u7684 react\u3002\n */\n\nimport { createElement, useSyncExternalStore } from 'react'\nimport { WorkflowPanel, refreshWorkflows } from './Panel.jsx'\nimport { Toasts } from './Toasts.jsx'\nimport { getState, subscribe, update } from './store.js'\nimport stylesText from './styles.css'\n\nexport const inject = ['slots']\n\nfunction useWorkbench() {\n return useSyncExternalStore(subscribe, getState)\n}\n\nfunction injectStyles() {\n if (typeof document === 'undefined') return\n if (document.getElementById('gyxxwb-styles')) return\n const style = document.createElement('style')\n style.id = 'gyxxwb-styles'\n style.textContent = stylesText\n document.head.appendChild(style)\n}\n\n/** \u4FA7\u8FB9\u680F\u5E95\u90E8\u52A8\u4F5C\uFF1A\u5DE5\u4F5C\u6D41\u9762\u677F\u5F00\u5173 + \u805A\u5408\u72B6\u6001\u70B9\u3002 */\nfunction WorkbenchFooterAction() {\n const s = useWorkbench()\n const failed = s.workflows.filter((w) => !w.active_run && w.last_run?.status === 'failed').length\n const running = s.workflows.filter((w) => w.active_run).length\n const tone = failed > 0 ? 'failed' : running > 0 ? 'running' : 'success'\n return createElement(\n 'button',\n {\n type: 'button',\n className: `gyxxwb-footer-btn${s.open ? ' gyxxwb-footer-btn-active' : ''}`,\n title: 'GYXX \u5DE5\u4F5C\u6D41\u5DE5\u4F5C\u53F0',\n onClick: () => {\n const next = !getState().open\n update({ open: next })\n if (next) refreshWorkflows()\n },\n },\n createElement('span', { className: `gyxxwb-dot gyxxwb-dot-${tone}` }),\n createElement('span', null, '\u5DE5\u4F5C\u6D41'),\n )\n}\n\n/** \u8986\u76D6\u5C42\u6839\uFF1A\u5DE6\u4FA7\u62BD\u5C49\u9762\u677F + \u5931\u8D25 toast\u3002 */\nfunction WorkbenchOverlay() {\n return createElement(\n 'div',\n { className: 'gyxxwb-overlay' },\n createElement(WorkflowPanel, null),\n createElement(Toasts, null),\n )\n}\n\nexport function apply(ctx) {\n injectStyles()\n ctx.effect(\n () => ctx.slots.register({ name: 'sidebar.footer.action' }, WorkbenchFooterAction),\n 'gyxx-workbench: footer action',\n )\n ctx.effect(\n () => ctx.slots.register({ name: 'shell.overlay' }, WorkbenchOverlay),\n 'gyxx-workbench: overlay',\n )\n}\n", "/** \u5DE5\u4F5C\u6D41\u9762\u677F\uFF1A\u4FA7\u8FB9\u62BD\u5C49\uFF0C\u5217\u51FA\u5168\u90E8\u5DE5\u4F5C\u6D41\uFF0C\u9009\u4E2D\u540E\u53EF\u63D0\u95EE / \u8BCA\u65AD / \u4FEE\u590D / \u542F\u505C\u3002 */\n\nimport { useSyncExternalStore, useEffect, useCallback } from 'react'\nimport { getState, subscribe, update, pushToast } from './store.js'\nimport { api } from './api.js'\n\nfunction useWorkbench() {\n return useSyncExternalStore(subscribe, getState)\n}\n\nconst STATUS_LABELS = {\n success: '\u6210\u529F',\n failed: '\u5931\u8D25',\n running: '\u8FD0\u884C\u4E2D',\n cancelled: '\u5DF2\u505C\u6B62',\n none: '\u672A\u8FD0\u884C',\n}\n\nfunction statusOf(workflow) {\n if (workflow.active_run) return 'running'\n return workflow.last_run?.status ?? 'none'\n}\n\nfunction formatTime(value) {\n if (!value) return '\u2014'\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return String(value)\n const pad = (n) => String(n).padStart(2, '0')\n return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`\n}\n\nfunction formatDuration(seconds) {\n if (seconds == null) return ''\n if (seconds < 60) return `${seconds}s`\n return `${Math.floor(seconds / 60)}m${seconds % 60 ? `${seconds % 60}s` : ''}`\n}\n\nexport async function refreshWorkflows() {\n update({ loading: true, error: null })\n try {\n const payload = await api.listWorkflows()\n update({\n loading: false,\n workflows: payload.workflows ?? [],\n summary: payload.summary ?? null,\n generatedAt: payload.generated_at ?? null,\n })\n } catch (error) {\n update({ loading: false, error: error.message })\n }\n}\n\nasync function selectWorkflow(workflowId) {\n update({ selectedId: workflowId, detailLoading: true, diagnosis: null, runs: [] })\n try {\n const [detail, runs] = await Promise.all([\n api.workflowDetail(workflowId).catch(() => null),\n api.workflowRuns(workflowId, 6).catch(() => ({ runs: [] })),\n ])\n update({ detailLoading: false, detail, runs: runs.runs ?? [] })\n } catch (error) {\n update({ detailLoading: false, error: error.message })\n }\n}\n\nasync function loadDiagnosis(workflowId, runId) {\n update({ diagnosisLoading: true })\n try {\n const payload = await api.diagnosis(workflowId, runId)\n update({ diagnosisLoading: false, diagnosis: payload })\n } catch (error) {\n update({ diagnosisLoading: false })\n pushToast({ kind: 'error', text: `\u8BCA\u65AD\u6570\u636E\u52A0\u8F7D\u5931\u8D25\uFF1A${error.message}` })\n }\n}\n\nasync function doAsk(action) {\n const s = getState()\n if (!s.selectedId) return\n const question = s.question.trim()\n try {\n const payload = await api.ask({\n workflow_id: s.selectedId,\n action,\n question,\n })\n const label = { ask: '\u63D0\u95EE', diagnose: '\u8BCA\u65AD', repair: '\u4FEE\u590D' }[action] ?? '\u4F1A\u8BDD'\n pushToast({\n kind: 'ok',\n text: `\u5DF2\u521B\u5EFA${label}\u4F1A\u8BDD\uFF08${String(payload.session_id).slice(0, 18)}\u2026\uFF09\uFF0C\u8BF7\u5728\u5DE6\u4FA7\u4F1A\u8BDD\u5217\u8868\u4E2D\u67E5\u770B`,\n })\n update({ question: '' })\n } catch (error) {\n pushToast({ kind: 'error', text: `\u521B\u5EFA\u4F1A\u8BDD\u5931\u8D25\uFF1A${error.message}` })\n }\n}\n\nasync function doTrigger(execute) {\n const s = getState()\n if (!s.selectedId) return\n if (execute) {\n const ok = window.confirm(\n `\u6B63\u5F0F\u6267\u884C ${s.selectedId}\uFF08\u4E1A\u52A1\u65E5\u671F ${s.businessDate}\uFF09\u4F1A\u5BF9\u5916\u90E8\u7CFB\u7EDF\u4EA7\u751F\u771F\u5B9E\u5199\u5165\u3002\u786E\u8BA4\u7EE7\u7EED\uFF1F`,\n )\n if (!ok) return\n }\n try {\n await api.trigger({\n workflow_id: s.selectedId,\n business_date: s.businessDate,\n execute,\n confirmed: execute,\n })\n pushToast({ kind: 'ok', text: execute ? '\u5DF2\u53D1\u8D77\u6B63\u5F0F\u6267\u884C' : '\u5DF2\u53D1\u8D77\u8BD5\u8FD0\u884C\uFF08\u65E0\u5916\u90E8\u526F\u4F5C\u7528\uFF09' })\n setTimeout(refreshWorkflows, 1500)\n } catch (error) {\n pushToast({ kind: 'error', text: `\u542F\u52A8\u5931\u8D25\uFF1A${error.message}` })\n }\n}\n\nasync function doCancel() {\n const s = getState()\n const active = s.detail?.workflow?.active_run\n if (!s.selectedId) return\n try {\n if (active?.operation_id) {\n await api.cancel({ workflow_id: s.selectedId, operation_id: active.operation_id })\n } else {\n await api.cancel({ workflow_id: s.selectedId, scheduled: true })\n }\n pushToast({ kind: 'ok', text: '\u5DF2\u53D1\u9001\u505C\u6B62\u6307\u4EE4' })\n setTimeout(refreshWorkflows, 1500)\n } catch (error) {\n pushToast({ kind: 'error', text: `\u505C\u6B62\u5931\u8D25\uFF1A${error.message}` })\n }\n}\n\nfunction SummaryPills({ summary, workflows }) {\n const failed = workflows.filter((w) => statusOf(w) === 'failed').length\n const running = workflows.filter((w) => w.active_run).length\n return (\n
{step.error}}\n {log.tail}\n