From b1861f69cdc1be69b0d76806efb3a7b135825b9d Mon Sep 17 00:00:00 2001 From: wangyunlong <3080433063@qq.com> Date: Mon, 19 Oct 2026 11:19:31 +0800 Subject: [PATCH] =?UTF-8?q?chore(workbench):=20=E7=A7=BB=E9=99=A4=20dsh=20?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8F=B0=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 工作台以 deepseek-harness 为 agent 底座,把现有工作流以 dsh 插件接入 Web UI。 该路线已停用,移除整个 workbench/ 与配套文档,避免留下点了启动不起来的死入口。 - 删除 workbench/:宿主插件 gyxx-workbench.mjs、侧边栏面板前端、cordis 配置 模板、启动脚本、独立面板与 node 冒烟测试 - 删除 docs/workbench.md 与 README 的「智能工作台」章节 - .gitignore 去掉 workbench 本地产物两条规则 - 基线结构测试的 docs 白名单同步移除 workbench.md 工作流能力不受影响:gyxx console、调度器、运行记录与诊断接口均未改动。 --- .gitignore | 4 - README.md | 11 - docs/workbench.md | 89 -- tests/test_engineering_baseline.py | 1 - workbench/README.md | 139 ---- workbench/bin/start-workbench.ps1 | 77 -- workbench/bin/start-workbench.sh | 43 - workbench/cordis.template.yml | 22 - workbench/plugin/client/src/Panel.jsx | 355 -------- workbench/plugin/client/src/Toasts.jsx | 74 -- workbench/plugin/client/src/api.js | 55 -- workbench/plugin/client/src/index.jsx | 85 -- workbench/plugin/client/src/store.js | 57 -- workbench/plugin/client/src/styles.css | 471 ----------- workbench/plugin/client/standalone.html | 258 ------ workbench/plugin/gyxx-workbench.mjs | 937 ---------------------- workbench/plugin/lib/client.js | 580 ------------- workbench/plugin/lib/client.js.map | 7 - workbench/plugin/package-lock.json | 481 ----------- workbench/plugin/package.json | 30 - workbench/plugin/scripts/build-client.mjs | 51 -- workbench/plugin/tests/workbench.test.mjs | 164 ---- 22 files changed, 3991 deletions(-) delete mode 100644 docs/workbench.md delete mode 100644 workbench/README.md delete mode 100644 workbench/bin/start-workbench.ps1 delete mode 100644 workbench/bin/start-workbench.sh delete mode 100644 workbench/cordis.template.yml delete mode 100644 workbench/plugin/client/src/Panel.jsx delete mode 100644 workbench/plugin/client/src/Toasts.jsx delete mode 100644 workbench/plugin/client/src/api.js delete mode 100644 workbench/plugin/client/src/index.jsx delete mode 100644 workbench/plugin/client/src/store.js delete mode 100644 workbench/plugin/client/src/styles.css delete mode 100644 workbench/plugin/client/standalone.html delete mode 100644 workbench/plugin/gyxx-workbench.mjs delete mode 100644 workbench/plugin/lib/client.js delete mode 100644 workbench/plugin/lib/client.js.map delete mode 100644 workbench/plugin/package-lock.json delete mode 100644 workbench/plugin/package.json delete mode 100644 workbench/plugin/scripts/build-client.mjs delete mode 100644 workbench/plugin/tests/workbench.test.mjs diff --git a/.gitignore b/.gitignore index e8aa7c1..634e972 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,3 @@ Thumbs.db /output/ /nul /new_all_bag.xlsx - -# Workbench (dsh plugin) local artifacts -/workbench/plugin/node_modules/ -/workbench/cordis.local.yml diff --git a/README.md b/README.md index 72445a6..917a77c 100644 --- a/README.md +++ b/README.md @@ -40,17 +40,6 @@ var/ 默认运行数据;不属于源码 - `shop_intelligence`:店铺、竞店和京东自营业绩。 - `supply_chain`:采购确认、补货、库存预警和采购单更新。 -## 智能工作台(dsh-refact 分支) - -`workbench/` 以 [deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) 为 -agent 底座,把现有工作流以 dsh 插件形式接入 Web UI:侧边栏选中任意工作流即可提问、 -诊断、修复,支持监控告警与受控启动。使用与设计见 [workbench/README.md](workbench/README.md) -与 [docs/workbench.md](docs/workbench.md)。 - -```powershell -powershell -File workbench\bin\start-workbench.ps1 -``` - ## 开发环境 ```powershell diff --git a/docs/workbench.md b/docs/workbench.md deleted file mode 100644 index fa2a5e7..0000000 --- a/docs/workbench.md +++ /dev/null @@ -1,89 +0,0 @@ -# 智能工作台设计(dsh-refact 分支) - -> 状态:v1 已实现。本文档记录「以 deepseek-harness 为 agent 底座、gyxx-flow 工作流插件化」 -> 的架构决策与边界。使用说明见 [../workbench/README.md](../workbench/README.md)。 - -## 目标与约束 - -- **底座**:deepseek-harness(`dsh`)Web UI + DeepSeek 模型 = 智能体运行时; - gyxx-flow 保持唯一的工作流执行/调度事实来源(LangGraph 引擎、`RunJournal`、 - `LockManager`、`EffectLedger` 契约不变)。 -- **插件化**:不重写工作流;通过 dsh「everything-is-a-plugin」扩展点把现有 - 控制台能力投影为智能体工具与 UI 面板。 -- **UI**:在原 dsh Web UI 侧边栏追加工作流入口(`sidebar.footer.action` + - `shell.overlay` 两个**追加型**插槽,不替换任何内置区域),选中工作流即可 - 提问 / 诊断 / 修复。 -- **生产约束**(AGENTS.md):`gyxx schedule run` 仍是唯一生产调度路径; - 正式执行保持 `execute + confirmed` 双确认;控制台仍以 `--env-file` 注入云端凭据 - (启动脚本默认 `D:\product-collector-analyze-flow\.env`)。 - -## 为什么不把 gyxx-flow 改写进 dsh monorepo - -dsh 是 pnpm monorepo(host/client 双聚合、Typert 远程契约、自有构建链)。 -把 Python 工作流引擎迁进去既不可能也无必要。dsh 的外部插件机制 -(`--patch` cordis.yml + 绝对路径插件 + `dsh.client` 包声明)就是为这种 -「外部系统接入」设计的。我们选择**进程外集成**: - -- gyxx-flow 控制台 HTTP API 是唯一集成面(已含脱敏、并发守卫、写操作令牌); -- 工作台插件只是控制台的客户端 + 智能体能力注册器; -- 控制台挂了,dsh 照常可用;dsh 挂了,调度器照常跑。 - -## 组件 - -### 1. 控制台诊断端点(Python,`src/gyxx_flow/console.py`) - -| 端点 | 说明 | -| --- | --- | -| `GET /api/workflows/{id}` | 单工作流详情(overview 投影 + `schedule_revision`) | -| `GET /api/workflows/{id}/runs/{run_id}` | 单次运行 + 步骤明细 | -| `GET /api/workflows/{id}/runs/{run_id}/diagnosis` | 诊断包:运行 + journal trace + 脱敏日志尾部 | - -诊断包日志来源:`run.json` 的 `trace.paths.log` 目录 + 运行时间窗内的控制台日志 -(`logs/console/{workflow_id}-*.log`),每个文件限读尾部 256KB,经 `_sanitize_error` -脱敏并截断,单响应最多 5 个文件。`run_id` 与 `workflow_id` 强制配对(404 不泄露 -跨工作流记录)。 - -### 2. 宿主插件(`workbench/plugin/gyxx-workbench.mjs`,零运行时依赖) - -- **7 个工具**(原始 JSON-Schema `ToolDefinition`,不 import 任何 dsh 包, - 保证以绝对路径加载时无解析风险); -- **系统提示词段**(`gyxx-workbench`,order 700):中文运维契约与安全规则; -- **失败监控器**:`ctx.effect` 轮询,`workflow_id+run_id` 去重,自定义事件 - `gyxx-workbench/alert` + 可选自动诊断会话; -- **桥接服务**(127.0.0.1:8790):为浏览器面板代理控制台 API(控制台禁 CORS, - 面板无法直连),并承载 `/bridge/ask` 会话创建与降级页面。 - 写操作校验 `x-gyxx-workbench: 1` + JSON content-type;CORS 仅回环来源。 - -### 3. 侧边栏面板(`workbench/plugin/client/`) - -- React 源码经 esbuild 打包为 CJS,平台模块(react 等)按 dsh 模块表协议外置, - 包装为 `window.__ModuleLoader__.load({id, factory})`——与官方 tsdown 产物同协议; -- `package.json` 的 `dsh.client` 声明让 `client-modules` 扫描器(支持路径型 - Loader 条目,向上找最近 package.json)发现并提供该浏览器包; -- 插槽:`sidebar.footer.action`(「工作流」开关 + 聚合状态点)、`shell.overlay` - (左抽屉面板 + 失败 toast)。均为 list 型追加插槽,不替换内置区域; -- 面板动作:提问/诊断/修复(创建带工作流上下文的智能体会话)、试运行/正式运行 - (浏览器端 confirm)、停止、运行历史查看、单运行诊断视图。 - -### 4. 会话创建(`/bridge/ask`) - -`ctx.agents.create({ sessionId, meta: { cwd, origin } })` → `agent.followup()` 注入 -带工作流上下文的首条用户消息。新会话自动出现在 dsh 会话列表(host 侧创建即入册)。 -文案模板区分 ask/diagnose/repair 三种动作。 - -## 已知限制(v1) - -- 会话创建后不能自动聚焦(dsh 客户端无公开的「选中会话」运行时 API);面板以 - toast 提示用户在会话列表中点开。 -- `sidebar.workspaces` 为 single 型插槽(替换即失去会话树),故面板走 overlay - 抽屉而非内嵌会话树;若未来 dsh 提供追加型侧栏区块插槽,可平移。 -- 客户端包协议依赖 dsh 未冻结的 developer-preview 契约;升级 dsh 需回归验证 - (README「依赖的 dsh 扩展点」一节列出了核对清单)。 -- `autoDiagnose` 默认关闭:自动开会话会产生模型调用成本。 - -## 测试 - -- Python:`tests/test_console.py` 新增 5 用例(详情/运行详情/诊断包/越权配对/HTTP 路由)。 -- 插件:`workbench/plugin/tests/`(node:test)——工具注册、离线错误结构化、 - confirmed 守卫、桥接代理。 -- 端到端手工验证:`start-workbench.ps1` → 面板选中工作流 → 提问/诊断/修复/试运行。 diff --git a/tests/test_engineering_baseline.py b/tests/test_engineering_baseline.py index 08c4151..648b0a2 100644 --- a/tests/test_engineering_baseline.py +++ b/tests/test_engineering_baseline.py @@ -36,7 +36,6 @@ def test_repository_keeps_the_simplified_public_structure() -> None: "note-inventory.md", "plan.md", "runbook.md", - "workbench.md", "workflow-acceptance-test-report.md", "workflow-acceptance-test-requirements.md", "workflow-console.md", diff --git a/workbench/README.md b/workbench/README.md deleted file mode 100644 index 7994177..0000000 --- a/workbench/README.md +++ /dev/null @@ -1,139 +0,0 @@ -# GYXX 智能工作台(deepseek-harness 集成) - -以 [deepseek-harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`,DeepSeek 智能体底座) -为 agent 运行时,把 gyxx-flow 的现有工作流**以插件形式**扩展进 dsh Web UI,形成一个 -可监控、可启动、可智能诊断/修复工作流的智能工作台。 - -``` -┌──────────────────────── dsh Web UI(DeepSeek 智能体)────────────────────────┐ -│ 侧边栏「工作流」面板 ───────────────┐ │ -│ (插件客户端包 shell.overlay 抽屉) │ 对话:选中工作流后提问 / 诊断 / 修复 │ -└─────────┬────────────────────────────┴───────────────────▲─────────────────┘ - │ 本机回环桥接 127.0.0.1:8790 │ 7 个工作流工具 -┌─────────▼─────────────────────────────────────────────────┴─────────────────┐ -│ gyxx-workbench 宿主插件(workbench/plugin/gyxx-workbench.mjs) │ -│ 工具注册 · 系统提示词 · 失败监控器 · 桥接服务 · 会话创建 │ -└─────────┬────────────────────────────────────────────────────────────────────┘ - │ HTTP(只读 + 受控写) -┌─────────▼─────────────────────────────────────────┐ -│ gyxx console(127.0.0.1:8765,gyxx-flow 现有控制台) │ -│ /api/overview · /api/workflows/* · /api/dynamic-configs │ -└─────────┬─────────────────────────────────────────┘ -┌─────────▼─────────────────────────────────────────┐ -│ gyxx-flow 工作流引擎(LangGraph)· 调度器 · RunJournal │ -└───────────────────────────────────────────────────┘ -``` - -## 快速开始 - -前置条件:Python 3.12 + `uv sync` 已完成;Node.js 22.19+;一个 DeepSeek API Key -(`DEEPSEEK_API_KEY`,dsh 自身要求)。 - -```powershell -# Windows:渲染补丁、拉起控制台(注入云端凭据)、启动 dsh Web UI -powershell -File workbench\bin\start-workbench.ps1 - -# 控制台凭据文件不是默认路径时 -powershell -File workbench\bin\start-workbench.ps1 -ConsoleEnvFile <你的.env 路径> -``` - -```bash -# Linux/macOS -bash workbench/bin/start-workbench.sh [console_env_file] -``` - -启动后打开 dsh Web UI(默认 ): - -- 侧边栏底部出现「工作流」按钮 → 打开工作流面板; -- 面板按模块分组列出全部调度工作流(状态点:绿=成功 / 红=失败 / 蓝=运行中 / 灰=未运行); -- **选中任意工作流**后可直接: - - 「提问」:带着该工作流上下文创建智能体会话,自由提问; - - 「诊断」:自动获取最近一次失败运行的诊断包(步骤、脱敏日志)并输出根因报告; - - 「修复」:智能体先诊断再给修复方案,**任何正式执行/调度修改必须先经你确认**; - - 「试运行 / 正式运行 / 停止」:对应控制台的受控执行语义(试运行无外部副作用)。 - -降级方案:若 dsh 客户端包因版本差异未能加载,直接打开桥接服务自带的独立面板 -,功能与侧边栏面板一致(零依赖页面)。 - -## 目录结构 - -``` -workbench/ - cordis.template.yml # dsh 组合补丁模板(启动脚本渲染出 cordis.local.yml) - bin/ - start-workbench.ps1 # Windows 一键启动 - start-workbench.sh # Linux/macOS 一键启动 - plugin/ # dsh 插件包(@gyxx/dsh-plugin-gyxx-workbench) - package.json # 含 dsh.client 声明(浏览器包发现契约) - gyxx-workbench.mjs # 宿主插件:工具 / 提示词 / 监控 / 桥接(零运行时依赖) - client/ - src/ # 侧边栏面板源码(React,打包时 react 外置) - standalone.html # 降级独立面板(桥接服务直接托管) - scripts/build-client.mjs - lib/client.js # 已构建的浏览器包(随仓库提交,改源码后需重建) - tests/ # node --test 冒烟测试 -``` - -## 智能体工具清单 - -| 工具 | 说明 | 副作用 | -| --- | --- | --- | -| `gyxx_workflow_list` | 全部工作流及调度、最近运行状态 | 无 | -| `gyxx_workflow_detail` | 单工作流定义(步骤/依赖/重放策略/调度) | 无 | -| `gyxx_workflow_runs` | 最近运行历史(步骤级状态、退出码、脱敏错误) | 无 | -| `gyxx_workflow_diagnose` | 一次运行的完整诊断包(journal 路径 + 脱敏日志尾部) | 无 | -| `gyxx_workflow_trigger` | 触发运行;默认试运行 | 试运行无副作用;正式执行需 `execute=true` + `confirmed=true` | -| `gyxx_workflow_cancel` | 停止手动/定时运行 | 有(停止进程) | -| `gyxx_schedule_update` | 启用/停用/改调度(读-改-写,带版本校验) | 有(改 `config/schedules.json`) | - -## 安全模型 - -- 宿主插件与控制台都只绑定 `127.0.0.1`;桥接服务的写操作要求 `x-gyxx-workbench: 1` - 自定义头 + `application/json`,拒绝跨站表单提交;CORS 仅回环来源。 -- 控制台自身的守卫不变:正式执行仍需 `confirmed=true`,写操作仍需 - `X-GYXX-Console: 1` 与同源检查;令牌模式(`GYXX_CONSOLE_TOKEN`)对插件同样生效。 -- 系统提示词固化「诊断 → 方案 → 用户确认 → 执行」的修复顺序,禁止智能体跳过确认。 -- 日志经控制台脱敏管道(口令/Token/URL 凭据打码)后才进入对话上下文。 - -## 失败监控 - -宿主插件每 30s(`pollIntervalMs`)轮询 `/api/overview`:某工作流出现**新的**失败运行时 -→ 面板右下角弹出告警 toast(可一键「立即诊断」);`autoDiagnose: true` 时还会自动创建 -诊断会话。告警去重以 `workflow_id + run_id` 为准,恢复成功后重置。 - -## 配置项(cordis.local.yml → config) - -| 键 | 默认 | 说明 | -| --- | --- | --- | -| `consoleBaseUrl` | `http://127.0.0.1:8765`(或环境变量 `GYXX_CONSOLE_URL`) | gyxx 控制台地址 | -| `consoleToken` | 环境变量 `GYXX_CONSOLE_TOKEN` | 控制台访问令牌(控制台以令牌模式运行时必填) | -| `bridgeHost` / `bridgePort` | `127.0.0.1` / `8790` | 桥接服务监听地址 | -| `pollIntervalMs` | `30000` | 失败监控轮询间隔 | -| `autoDiagnose` | `false` | 发现失败时自动创建诊断会话 | -| `projectRoot` | dsh 进程 cwd | 新建智能体会话的工作目录 | - -## 开发 - -```bash -# 重建客户端包(修改 client/src 后必须执行并提交 lib/client.js) -cd workbench/plugin && npm install && npm run build - -# 宿主插件冒烟测试(不需要 dsh / gyxx console) -cd workbench/plugin && npm test - -# Python 侧端点测试 -uv run pytest tests/test_console.py -k "workflow_detail or run_detail or diagnosis" -``` - -## 依赖的 dsh 扩展点(上游契约) - -- `ctx.tools.register()` 原始 JSON-Schema 工具定义(cookbook: extension-cookbook) -- `ctx.systemPrompt.section()` 系统提示词段 -- cordis.yml `--patch` 组合覆盖(`apps/cli/src/args.ts`,npm 版同样支持) -- `dsh.client` package.json 声明 → 客户端包发现(`packages/client/modules`) -- 插槽:`sidebar.footer.action`(list,追加)、`shell.overlay`(list,追加) -- `ctx.agents.create()` + `agent.followup()` 编程式会话 - -dsh 处于 developer preview,扩展点可能变化;升级 dsh 后若面板消失,先检查 -浏览器控制台模块加载错误,再核对上述插槽名是否仍存在于 -`packages/client/ui-layout` / `ui-sidebar` 的 SlotMap。 diff --git a/workbench/bin/start-workbench.ps1 b/workbench/bin/start-workbench.ps1 deleted file mode 100644 index 91c7465..0000000 --- a/workbench/bin/start-workbench.ps1 +++ /dev/null @@ -1,77 +0,0 @@ -# GYXX 智能工作台一键启动(Windows PowerShell) -# 用法: -# powershell -File workbench\bin\start-workbench.ps1 -# powershell -File workbench\bin\start-workbench.ps1 -ConsoleEnvFile D:\product-collector-analyze-flow\.env -# -# 行为: -# 1. 渲染 workbench/cordis.local.yml(插件绝对路径) -# 2. 若 8765 控制台未运行,则以 --env-file 注入云端凭据启动 gyxx console -# 3. 启动 dsh Web UI 并应用工作台补丁(优先 $env:DSH_HOME 源码目录,其次 npx @deepseek-ai/dsh) - -[CmdletBinding()] -param( - [string]$ConsoleEnvFile = 'D:\product-collector-analyze-flow\.env', - [int]$ConsolePort = 8765, - [int]$BridgePort = 8790, - [switch]$SkipConsole -) - -$ErrorActionPreference = 'Stop' -$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path -$WorkbenchDir = Join-Path $RepoRoot 'workbench' -$LocalYml = Join-Path $WorkbenchDir 'cordis.local.yml' - -# 1) 渲染 cordis.local.yml -$template = Get-Content (Join-Path $WorkbenchDir 'cordis.template.yml') -Raw -Encoding UTF8 -$workbenchDirPosix = ($WorkbenchDir -replace '\\', '/') -$projectRootPosix = ($RepoRoot -replace '\\', '/') -# Windows 上 dsh 的 ESM 加载器要求 file:// URL -$workbenchModuleUrl = 'file:///' + $workbenchDirPosix -$rendered = $template.Replace('__WORKBENCH_DIR__', $workbenchModuleUrl).Replace('__PROJECT_ROOT__', $projectRootPosix) -$rendered = $rendered -replace 'bridgePort: 8790', "bridgePort: $BridgePort" -$rendered = $rendered -replace "consoleBaseUrl: 'http://127.0.0.1:8765'", "consoleBaseUrl: 'http://127.0.0.1:$ConsolePort'" -[System.IO.File]::WriteAllText($LocalYml, $rendered, (New-Object System.Text.UTF8Encoding($false))) -Write-Host "[workbench] 已生成 $LocalYml" - -# 2) 确保 gyxx console 在运行 -if (-not $SkipConsole) { - $consoleUp = $false - try { - $null = Invoke-WebRequest -UseBasicParsing -Uri "http://127.0.0.1:$ConsolePort/api/overview" -TimeoutSec 3 - $consoleUp = $true - } catch { $consoleUp = $false } - - if ($consoleUp) { - Write-Host "[workbench] gyxx console 已在 http://127.0.0.1:$ConsolePort 运行" - } else { - if (-not (Test-Path $ConsoleEnvFile)) { - Write-Warning "缺少 $ConsoleEnvFile —— 正式执行将无法注入云端凭据(AGENTS.md 约定)。" - } - $consoleArgs = @('run', 'gyxx', 'console', '--port', "$ConsolePort", '--env-file', $ConsoleEnvFile) - Write-Host "[workbench] 启动 gyxx console: uv $($consoleArgs -join ' ')" - Start-Process -FilePath 'uv' -ArgumentList $consoleArgs -WorkingDirectory $RepoRoot -WindowStyle Minimized - $deadline = (Get-Date).AddSeconds(30) - do { - Start-Sleep -Milliseconds 800 - try { - $null = Invoke-WebRequest -UseBasicParsing -Uri "http://127.0.0.1:$ConsolePort/api/overview" -TimeoutSec 2 - $consoleUp = $true - } catch { $consoleUp = $false } - } until ($consoleUp -or (Get-Date) -gt $deadline) - if (-not $consoleUp) { Write-Warning 'gyxx console 启动超时,工作台仍可启动但工具会提示控制台离线。' } - } -} - -# 3) 启动 dsh Web UI -if ($env:DSH_HOME -and (Test-Path (Join-Path $env:DSH_HOME 'package.json'))) { - Write-Host "[workbench] 使用源码版 dsh: $($env:DSH_HOME)" - Push-Location $env:DSH_HOME - try { - & pnpm dsh web --patch $LocalYml - } finally { - Pop-Location - } -} else { - Write-Host '[workbench] 使用 npm 版 dsh(npx @deepseek-ai/dsh)' - & npx -y '@deepseek-ai/dsh' web --patch $LocalYml -} diff --git a/workbench/bin/start-workbench.sh b/workbench/bin/start-workbench.sh deleted file mode 100644 index d56b13a..0000000 --- a/workbench/bin/start-workbench.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -# GYXX 智能工作台一键启动(Linux/macOS) -# 用法:bash workbench/bin/start-workbench.sh [console_env_file] -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -WORKBENCH_DIR="$REPO_ROOT/workbench" -LOCAL_YML="$WORKBENCH_DIR/cordis.local.yml" -CONSOLE_ENV_FILE="${1:-${GYXX_CONSOLE_ENV_FILE:-/etc/gyxx/flow.env}}" -CONSOLE_PORT="${GYXX_CONSOLE_PORT:-8765}" -BRIDGE_PORT="${GYXX_WB_BRIDGE_PORT:-8790}" - -# 1) 渲染 cordis.local.yml(插件路径必须为绝对路径) -# dsh 的 ESM 加载器要求 file:// URL(Windows 上必须,POSIX 上同样可用) -sed \ - -e "s|__WORKBENCH_DIR__|file://$WORKBENCH_DIR|g" \ - -e "s|__PROJECT_ROOT__|$REPO_ROOT|g" \ - -e "s|bridgePort: 8790|bridgePort: $BRIDGE_PORT|g" \ - -e "s|http://127.0.0.1:8765|http://127.0.0.1:$CONSOLE_PORT|g" \ - "$WORKBENCH_DIR/cordis.template.yml" > "$LOCAL_YML" -echo "[workbench] 已生成 $LOCAL_YML" - -# 2) 确保 gyxx console 在运行 -if ! curl -fsS -m 3 "http://127.0.0.1:$CONSOLE_PORT/api/overview" >/dev/null 2>&1; then - if [ ! -f "$CONSOLE_ENV_FILE" ]; then - echo "[workbench] 警告:缺少 $CONSOLE_ENV_FILE —— 正式执行将无法注入云端凭据(AGENTS.md 约定)。" >&2 - fi - echo "[workbench] 启动 gyxx console(端口 $CONSOLE_PORT)" - (cd "$REPO_ROOT" && nohup uv run gyxx console --port "$CONSOLE_PORT" --env-file "$CONSOLE_ENV_FILE" >/dev/null 2>&1 &) - for _ in $(seq 1 30); do - sleep 1 - if curl -fsS -m 2 "http://127.0.0.1:$CONSOLE_PORT/api/overview" >/dev/null 2>&1; then break; fi - done -fi - -# 3) 启动 dsh Web UI -if [ -n "${DSH_HOME:-}" ] && [ -f "$DSH_HOME/package.json" ]; then - echo "[workbench] 使用源码版 dsh: $DSH_HOME" - (cd "$DSH_HOME" && pnpm dsh web --patch "$LOCAL_YML") -else - echo "[workbench] 使用 npm 版 dsh(npx @deepseek-ai/dsh)" - npx -y @deepseek-ai/dsh web --patch "$LOCAL_YML" -fi diff --git a/workbench/cordis.template.yml b/workbench/cordis.template.yml deleted file mode 100644 index 8d922b8..0000000 --- a/workbench/cordis.template.yml +++ /dev/null @@ -1,22 +0,0 @@ -# GYXX 智能工作台 —— dsh Web 组合补丁(模板)。 -# 启动脚本会把 __WORKBENCH_DIR__ / __PROJECT_ROOT__ 渲染为绝对路径后生成 -# cordis.local.yml,再通过 `dsh web --patch workbench/cordis.local.yml` 加载。 -# 不要直接手工使用本文件:dsh 要求插件路径为绝对路径(Windows 上必须是 file:// URL)。 - -- insert: - - id: gyxx-workbench - name: '__WORKBENCH_DIR__/plugin/gyxx-workbench.mjs' - config: - # gyxx 控制台 HTTP API(gyxx console,默认 8765 端口) - consoleBaseUrl: 'http://127.0.0.1:8765' - # 访问令牌:缺省读取进程环境变量 GYXX_CONSOLE_TOKEN - # consoleToken: '' - # 侧边栏面板桥接服务(本机回环) - bridgeHost: '127.0.0.1' - bridgePort: 8790 - # 失败监控轮询间隔(毫秒) - pollIntervalMs: 30000 - # 发现失败时是否自动创建诊断会话(默认 false,只在面板弹告警) - autoDiagnose: false - # 智能体会话的工作目录(数据根目录下的日志可用文件工具直接查看) - projectRoot: '__PROJECT_ROOT__' diff --git a/workbench/plugin/client/src/Panel.jsx b/workbench/plugin/client/src/Panel.jsx deleted file mode 100644 index 9f8953e..0000000 --- a/workbench/plugin/client/src/Panel.jsx +++ /dev/null @@ -1,355 +0,0 @@ -/** 工作流面板:侧边抽屉,列出全部工作流,选中后可提问 / 诊断 / 修复 / 启停。 */ - -import { useSyncExternalStore, useEffect, useCallback } from 'react' -import { getState, subscribe, update, pushToast } from './store.js' -import { api } from './api.js' - -function useWorkbench() { - return useSyncExternalStore(subscribe, getState) -} - -const STATUS_LABELS = { - success: '成功', - failed: '失败', - running: '运行中', - cancelled: '已停止', - none: '未运行', -} - -function statusOf(workflow) { - if (workflow.active_run) return 'running' - return workflow.last_run?.status ?? 'none' -} - -function formatTime(value) { - if (!value) return '—' - const date = new Date(value) - if (Number.isNaN(date.getTime())) return String(value) - const pad = (n) => String(n).padStart(2, '0') - return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}` -} - -function formatDuration(seconds) { - if (seconds == null) return '' - if (seconds < 60) return `${seconds}s` - return `${Math.floor(seconds / 60)}m${seconds % 60 ? `${seconds % 60}s` : ''}` -} - -export async function refreshWorkflows() { - update({ loading: true, error: null }) - try { - const payload = await api.listWorkflows() - update({ - loading: false, - workflows: payload.workflows ?? [], - summary: payload.summary ?? null, - generatedAt: payload.generated_at ?? null, - }) - } catch (error) { - update({ loading: false, error: error.message }) - } -} - -async function selectWorkflow(workflowId) { - update({ selectedId: workflowId, detailLoading: true, diagnosis: null, runs: [] }) - try { - const [detail, runs] = await Promise.all([ - api.workflowDetail(workflowId).catch(() => null), - api.workflowRuns(workflowId, 6).catch(() => ({ runs: [] })), - ]) - update({ detailLoading: false, detail, runs: runs.runs ?? [] }) - } catch (error) { - update({ detailLoading: false, error: error.message }) - } -} - -async function loadDiagnosis(workflowId, runId) { - update({ diagnosisLoading: true }) - try { - const payload = await api.diagnosis(workflowId, runId) - update({ diagnosisLoading: false, diagnosis: payload }) - } catch (error) { - update({ diagnosisLoading: false }) - pushToast({ kind: 'error', text: `诊断数据加载失败:${error.message}` }) - } -} - -async function doAsk(action) { - const s = getState() - if (!s.selectedId) return - const question = s.question.trim() - try { - const payload = await api.ask({ - workflow_id: s.selectedId, - action, - question, - }) - const label = { ask: '提问', diagnose: '诊断', repair: '修复' }[action] ?? '会话' - pushToast({ - kind: 'ok', - text: `已创建${label}会话(${String(payload.session_id).slice(0, 18)}…),请在左侧会话列表中查看`, - }) - update({ question: '' }) - } catch (error) { - pushToast({ kind: 'error', text: `创建会话失败:${error.message}` }) - } -} - -async function doTrigger(execute) { - const s = getState() - if (!s.selectedId) return - if (execute) { - const ok = window.confirm( - `正式执行 ${s.selectedId}(业务日期 ${s.businessDate})会对外部系统产生真实写入。确认继续?`, - ) - if (!ok) return - } - try { - await api.trigger({ - workflow_id: s.selectedId, - business_date: s.businessDate, - execute, - confirmed: execute, - }) - pushToast({ kind: 'ok', text: execute ? '已发起正式执行' : '已发起试运行(无外部副作用)' }) - setTimeout(refreshWorkflows, 1500) - } catch (error) { - pushToast({ kind: 'error', text: `启动失败:${error.message}` }) - } -} - -async function doCancel() { - const s = getState() - const active = s.detail?.workflow?.active_run - if (!s.selectedId) return - try { - if (active?.operation_id) { - await api.cancel({ workflow_id: s.selectedId, operation_id: active.operation_id }) - } else { - await api.cancel({ workflow_id: s.selectedId, scheduled: true }) - } - pushToast({ kind: 'ok', text: '已发送停止指令' }) - setTimeout(refreshWorkflows, 1500) - } catch (error) { - pushToast({ kind: 'error', text: `停止失败:${error.message}` }) - } -} - -function SummaryPills({ summary, workflows }) { - const failed = workflows.filter((w) => statusOf(w) === 'failed').length - const running = workflows.filter((w) => w.active_run).length - return ( -
- 共 {summary?.total ?? workflows.length} - {running > 0 && 运行中 {running}} - {failed > 0 && 失败 {failed}} -
- ) -} - -function WorkflowRow({ workflow, selected, onSelect }) { - const status = statusOf(workflow) - return ( - - ) -} - -function RunList({ runs, onDiagnose }) { - if (!runs.length) return
暂无运行记录
- return ( -
- {runs.map((run) => ( - - ))} -
- ) -} - -function DiagnosisView({ diagnosis, loading }) { - if (loading) return
正在加载诊断数据…
- if (!diagnosis) return null - if (diagnosis.message) return
{diagnosis.message}
- const run = diagnosis.run ?? {} - const steps = Array.isArray(run.steps) ? run.steps : [] - const logs = Array.isArray(diagnosis.logs) ? diagnosis.logs : [] - return ( -
-
- 诊断:{run.run_id}({run.status}) -
- {steps - .filter((step) => step.status === 'failed') - .map((step) => ( -
-
- ✗ {step.id}(退出码 {step.exit_code ?? '—'}) -
- {step.error &&
{step.error}
} -
- ))} - {logs.map((log) => ( -
-
{log.path}
-
{log.tail}
-
- ))} - {!steps.some((s) => s.status === 'failed') && !logs.length && ( -
该运行没有失败步骤或日志
- )} -
- ) -} - -export function WorkflowPanel() { - const s = useWorkbench() - - useEffect(() => { - if (!s.open) return undefined - refreshWorkflows() - const timer = setInterval(refreshWorkflows, 10_000) - return () => clearInterval(timer) - }, [s.open]) - - const onSelect = useCallback((workflowId) => { - selectWorkflow(workflowId) - }, []) - - const groups = [] - const byModule = new Map() - for (const workflow of s.workflows) { - const key = workflow.module_label ?? workflow.module - if (!byModule.has(key)) byModule.set(key, []) - byModule.get(key).push(workflow) - } - for (const [label, items] of byModule) groups.push({ label, items }) - - const selected = s.workflows.find((w) => w.id === s.selectedId) ?? null - - return ( -
-
- GYXX 工作流 - - - -
- {s.error && ( -
- 无法连接工作台桥接服务:{s.error} -
- 请确认 dsh 已通过 workbench/cordis.yml 启动。 -
- )} -
-
- {groups.map((group) => ( -
-
{group.label}
- {group.items.map((workflow) => ( - - ))} -
- ))} - {!s.loading && !s.workflows.length && !s.error && ( -
未发现工作流
- )} -
- {selected && ( -
-
- {selected.name} - - {STATUS_LABELS[statusOf(selected)]} - -
-
- {selected.id} · 调度{' '} - {selected.schedule - ? `${selected.schedule.enabled ? '启用' : '停用'} ${ - Array.isArray(selected.schedule.at) - ? selected.schedule.at.join('/') - : (selected.schedule.at ?? '') - }` - : '无'} - {selected.next_run_at ? ` · 下次 ${formatTime(selected.next_run_at)}` : ''} -
-
- - - - - {selected.active_run && ( - - )} -
-
- update({ question: event.target.value })} - onKeyDown={(event) => { - if (event.key === 'Enter') doAsk('ask') - }} - /> - -
-
- 业务日期{' '} - update({ businessDate: event.target.value })} - pattern="\d{4}-\d{2}-\d{2}" - /> -
-
最近运行
- loadDiagnosis(selected.id, runId)} /> - -
- )} -
-
- ) -} diff --git a/workbench/plugin/client/src/Toasts.jsx b/workbench/plugin/client/src/Toasts.jsx deleted file mode 100644 index dd06b26..0000000 --- a/workbench/plugin/client/src/Toasts.jsx +++ /dev/null @@ -1,74 +0,0 @@ -/** 失败告警浮层:轮询桥接告警,新失败弹出 toast,可一键发起诊断会话。 */ - -import { useEffect } from 'react' -import { useSyncExternalStore } from 'react' -import { getState, subscribe, update, dismissToast, pushToast } from './store.js' -import { api } from './api.js' - -function useWorkbench() { - return useSyncExternalStore(subscribe, getState) -} - -export function Toasts() { - const s = useWorkbench() - - useEffect(() => { - let stopped = false - let timer = null - const poll = async () => { - try { - const payload = await api.alerts() - const fresh = (payload.alerts ?? []).filter( - (alert) => !alert.seen && !getState().seenAlertIds[alert.id], - ) - if (fresh.length) { - const seen = { ...getState().seenAlertIds } - for (const alert of fresh) { - seen[alert.id] = true - pushToast({ kind: 'failed', text: `工作流「${alert.name}」运行失败`, alert }) - } - update({ seenAlertIds: seen }) - } - } catch { - // 桥接离线时静默 - } - if (!stopped) timer = setTimeout(poll, 30_000) - } - timer = setTimeout(poll, 8_000) - return () => { - stopped = true - if (timer) clearTimeout(timer) - } - }, []) - - if (!s.toasts.length) return null - return ( -
- {s.toasts.map((toast) => ( -
- {toast.text} - {toast.alert && ( - - )} - -
- ))} -
- ) -} diff --git a/workbench/plugin/client/src/api.js b/workbench/plugin/client/src/api.js deleted file mode 100644 index d67f797..0000000 --- a/workbench/plugin/client/src/api.js +++ /dev/null @@ -1,55 +0,0 @@ -/** 工作台桥接服务客户端:所有数据经由 gyxx-workbench 宿主插件的本机回环桥接。 */ - -export function bridgeBase() { - if (typeof window !== 'undefined') { - if (window.__GYXX_WB_BRIDGE__) return window.__GYXX_WB_BRIDGE__ - try { - const saved = window.localStorage?.getItem('gyxx.wb.bridge') - if (saved) return saved - } catch { - // localStorage 不可用时使用默认值 - } - } - return 'http://127.0.0.1:8790' -} - -async function request(method, path, body) { - const response = await fetch(`${bridgeBase()}${path}`, { - method, - headers: { - accept: 'application/json', - ...(body !== undefined - ? { 'content-type': 'application/json', 'x-gyxx-workbench': '1' } - : {}), - }, - body: body === undefined ? undefined : JSON.stringify(body), - }) - let payload - try { - payload = await response.json() - } catch { - payload = { ok: false, error: `桥接服务返回了非 JSON 内容(HTTP ${response.status})` } - } - if (!response.ok) { - throw new Error(payload?.error ?? `桥接服务错误(HTTP ${response.status})`) - } - return payload -} - -export const api = { - listWorkflows: () => request('GET', '/bridge/workflows'), - workflowDetail: (workflowId) => - request('GET', `/bridge/workflows/${encodeURIComponent(workflowId)}`), - workflowRuns: (workflowId, limit = 6) => - request('GET', `/bridge/workflows/${encodeURIComponent(workflowId)}/runs?limit=${limit}`), - diagnosis: (workflowId, runId) => - request( - 'GET', - `/bridge/workflows/${encodeURIComponent(workflowId)}/diagnosis${runId ? `?run_id=${encodeURIComponent(runId)}` : ''}`, - ), - alerts: () => request('GET', '/bridge/alerts'), - markAlertSeen: (id) => request('POST', '/bridge/alerts/seen', { id }), - trigger: (payload) => request('POST', '/bridge/trigger', payload), - cancel: (payload) => request('POST', '/bridge/cancel', payload), - ask: (payload) => request('POST', '/bridge/ask', payload), -} diff --git a/workbench/plugin/client/src/index.jsx b/workbench/plugin/client/src/index.jsx deleted file mode 100644 index 42a9e52..0000000 --- a/workbench/plugin/client/src/index.jsx +++ /dev/null @@ -1,85 +0,0 @@ -/** - * GYXX 工作台客户端插件入口。 - * 由 esbuild 打包为 CJS 并通过 window.__ModuleLoader__ 注册(见 scripts/build-client.mjs)。 - * 运行时只依赖 dsh 平台共享模块表中的 react。 - */ - -import { createElement, useSyncExternalStore } from 'react' -import { WorkflowPanel, refreshWorkflows } from './Panel.jsx' -import { Toasts } from './Toasts.jsx' -import { getState, subscribe, update } from './store.js' -import stylesText from './styles.css' - -export const inject = ['slots'] - -function useWorkbench() { - return useSyncExternalStore(subscribe, getState) -} - -function injectStyles() { - if (typeof document === 'undefined') return - if (document.getElementById('gyxxwb-styles')) return - const style = document.createElement('style') - style.id = 'gyxxwb-styles' - style.textContent = stylesText - document.head.appendChild(style) -} - -/** 侧边栏底部动作:工作流面板开关 + 聚合状态点。 */ -function WorkbenchFooterAction() { - const s = useWorkbench() - const failed = s.workflows.filter((w) => !w.active_run && w.last_run?.status === 'failed').length - const running = s.workflows.filter((w) => w.active_run).length - const tone = failed > 0 ? 'failed' : running > 0 ? 'running' : 'success' - return createElement( - 'button', - { - type: 'button', - className: `gyxxwb-footer-btn${s.open ? ' gyxxwb-footer-btn-active' : ''}`, - title: 'GYXX 工作流工作台', - onClick: () => { - const next = !getState().open - update({ open: next }) - if (next) refreshWorkflows() - }, - }, - createElement('span', { className: `gyxxwb-dot gyxxwb-dot-${tone}` }), - createElement('span', null, '工作流'), - ) -} - -/** 覆盖层根:左侧抽屉面板 + 失败 toast。 */ -function WorkbenchOverlay() { - return createElement( - 'div', - { className: 'gyxxwb-overlay' }, - createElement(WorkflowPanel, null), - createElement(Toasts, null), - ) -} - -export function apply(ctx) { - injectStyles() - // 插槽须等宿主声明后才能注册:slots.inject 会等待座位声明(顺序安全), - // 直接 register 在我们先于 ui-sidebar/ui-layout 加载时会报“slot is not declared”。 - ctx.effect( - () => - 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.inject('shell.overlay', () => - ctx.slots.register( - { name: 'shell.overlay', id: 'gyxx-workbench.overlay' }, - WorkbenchOverlay, - ), - ), - 'gyxx-workbench: overlay', - ) -} diff --git a/workbench/plugin/client/src/store.js b/workbench/plugin/client/src/store.js deleted file mode 100644 index f8d346c..0000000 --- a/workbench/plugin/client/src/store.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * GYXX 工作台面板 —— 极简外部存储(避免向客户端包引入额外依赖)。 - * React 18 的 useSyncExternalStore 直接订阅。 - */ - -const listeners = new Set() - -let state = { - open: false, - loading: false, - error: null, - generatedAt: null, - summary: null, - workflows: [], - selectedId: null, - detail: null, - detailLoading: false, - runs: [], - diagnosis: null, - diagnosisLoading: false, - question: '', - businessDate: defaultBusinessDate(), - alerts: [], - seenAlertIds: {}, - toasts: [], -} - -export function defaultBusinessDate() { - const now = new Date() - now.setDate(now.getDate() - 1) - const pad = (value) => String(value).padStart(2, '0') - return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}` -} - -export function getState() { - return state -} - -export function update(patch) { - state = { ...state, ...patch } - for (const listener of listeners) listener() -} - -export function subscribe(listener) { - listeners.add(listener) - return () => listeners.delete(listener) -} - -export function pushToast(toast) { - const id = `t-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}` - update({ toasts: [...state.toasts, { id, ...toast }] }) - return id -} - -export function dismissToast(id) { - update({ toasts: state.toasts.filter((toast) => toast.id !== id) }) -} diff --git a/workbench/plugin/client/src/styles.css b/workbench/plugin/client/src/styles.css deleted file mode 100644 index 3363ad1..0000000 --- a/workbench/plugin/client/src/styles.css +++ /dev/null @@ -1,471 +0,0 @@ -/* GYXX 工作台面板样式 —— 以文本形式内联进客户端包,运行时注入 - - -
-

GYXX 智能工作台

- - - -
-
-
-

在左侧选择一个工作流,可提问、诊断、修复或启动。

-
-
- - - diff --git a/workbench/plugin/gyxx-workbench.mjs b/workbench/plugin/gyxx-workbench.mjs deleted file mode 100644 index 3278110..0000000 --- a/workbench/plugin/gyxx-workbench.mjs +++ /dev/null @@ -1,937 +0,0 @@ -/** - * GYXX 智能工作台 — deepseek-harness 宿主插件。 - * - * 通过 gyxx-flow 控制台 HTTP API(默认 http://127.0.0.1:8765)把工作流 - * 暴露为 DeepSeek 智能体工具,并提供: - * - 7 个模型可调用的工作流工具(查询 / 诊断 / 触发 / 停止 / 定时配置) - * - 中文系统提示词(安全契约:默认试运行,正式执行必须用户确认) - * - 失败监控器(轮询 overview,发现新失败时发出告警,可选自动开诊断会话) - * - 本机回环桥接服务(默认 127.0.0.1:8790),供侧边栏面板读取数据、 - * 发起「提问 / 诊断 / 修复」会话;同时内置一个零依赖的独立面板页面 - * (GET /)作为 dsh 客户端插件加载失败时的降级 UI。 - * - * 本文件零运行时依赖(只用 Node 内置模块),经 cordis.yml --patch 以绝对 - * 路径插入 dsh Web 组合即可运行。 - */ - -import http from 'node:http' -import { randomUUID } from 'node:crypto' -import { readFileSync } from 'node:fs' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' - -export const name = 'gyxx-workbench' -export const inject = ['tools', 'systemPrompt'] - -const PLUGIN_DIR = dirname(fileURLToPath(import.meta.url)) -const MODULE_LABELS = { - content_marketing: '内容营销', - product_commerce: '商品经营', - shop_intelligence: '店铺情报', - supply_chain: '供应链', -} - -/* ------------------------------------------------------------------ */ -/* 控制台 HTTP 客户端 */ -/* ------------------------------------------------------------------ */ - -class ConsoleApiError extends Error { - constructor(status, message) { - super(message) - this.name = 'ConsoleApiError' - this.status = status - } -} - -function makeConsoleClient(config) { - const base = new URL(config.consoleBaseUrl) - return async function consoleApi(method, path, body, options = {}) { - const headers = { accept: 'application/json' } - if (config.consoleToken) headers.authorization = `Bearer ${config.consoleToken}` - if (body !== undefined) { - headers['content-type'] = 'application/json' - headers['x-gyxx-console'] = '1' - } - if (options.ifMatch) headers['if-match'] = options.ifMatch - let response - try { - response = await fetch(new URL(path, base), { - method, - headers, - body: body === undefined ? undefined : JSON.stringify(body), - signal: options.signal, - }) - } catch (error) { - if (error?.name === 'AbortError') throw error - throw new ConsoleApiError( - 0, - `无法连接 gyxx 控制台 ${base.origin},请先启动 gyxx console(${error?.message ?? error})`, - ) - } - const text = await response.text() - let payload - try { - payload = JSON.parse(text) - } catch { - payload = { raw: text } - } - if (!response.ok) { - const message = - payload && typeof payload.error === 'string' - ? payload.error - : `控制台返回 HTTP ${response.status}` - throw new ConsoleApiError(response.status, message) - } - return payload - } -} - -/* ------------------------------------------------------------------ */ -/* 数据压缩:把控制台载荷裁剪为适合模型阅读的体积 */ -/* ------------------------------------------------------------------ */ - -function compactRun(run) { - if (!run) return null - const compact = { - run_id: run.run_id, - business_date: run.business_date, - mode: run.mode, - shadow: run.shadow, - status: run.status, - started_at: run.started_at, - ended_at: run.ended_at, - duration_seconds: run.duration_seconds, - error: run.error, - } - if (Array.isArray(run.steps)) { - compact.steps = run.steps.map((step) => ({ - id: step.id, - status: step.status, - exit_code: step.exit_code, - error: step.error, - })) - } - return compact -} - -function compactWorkflow(item) { - const schedule = item.schedule ?? null - return { - id: item.id, - name: item.name, - module: item.module, - module_label: MODULE_LABELS[item.module] ?? item.module, - trigger: item.trigger, - registered: item.registered, - note: item.note ?? null, - schedule: schedule - ? { - enabled: schedule.enabled, - kind: schedule.kind, - at: schedule.at, - raw: schedule, - } - : null, - next_run_at: item.next_run_at ?? null, - last_run: compactRun(item.last_run), - active_run: item.active_run - ? { - operation_id: item.active_run.operation_id, - business_date: item.active_run.business_date, - mode: item.active_run.mode, - started_at: item.active_run.started_at, - } - : null, - } -} - -function compactDetail(payload) { - const workflow = payload.workflow ?? {} - const steps = Array.isArray(workflow.steps) - ? workflow.steps.map((step) => ({ - id: step.id, - name: step.name, - description: step.description, - entry: step.entry, - timeout_seconds: step.timeout_seconds, - replay_policy: step.replay_policy, - depends_on: step.depends_on, - })) - : [] - return { - workflow: { ...compactWorkflow(workflow), steps }, - schedule_revision: payload.schedule_revision ?? null, - warnings: payload.warnings ?? [], - } -} - -/* ------------------------------------------------------------------ */ -/* 系统提示词 */ -/* ------------------------------------------------------------------ */ - -const SYSTEM_PROMPT_TEXT = `\ -你是 GYXX 智能工作台的运维智能体。gyxx-flow 是一套 LangGraph 工作流编排系统, -包含四个业务模块:内容营销(content_marketing)、商品经营(product_commerce)、 -店铺情报(shop_intelligence)、供应链(supply_chain)。每个工作流由若干步骤组成, -运行日志(RunJournal)与运行索引位于数据根目录下。 - -你可以使用以下工具(数据来自本机 gyxx 控制台 HTTP API): -- gyxx_workflow_list:列出全部工作流及其调度、最近一次运行状态。 -- gyxx_workflow_detail:查看单个工作流的定义、步骤与调度详情。 -- gyxx_workflow_runs:查看某工作流的最近运行历史(含每个步骤的退出码与错误)。 -- gyxx_workflow_diagnose:获取一次运行的完整诊断包(日志尾部已脱敏)。 -- gyxx_workflow_trigger:触发一次运行。默认 execute=false 为试运行(无副作用)。 -- gyxx_workflow_cancel:停止正在运行的手动执行或定时执行。 -- gyxx_schedule_update:启用/停用或修改定时调度(读-改-写,需要 schedule_revision)。 - -安全契约(必须遵守): -1. 默认只做只读分析与试运行。execute=true 的正式执行会对外部系统 - (PostgreSQL、飞书、电商平台)产生真实写入,只有在用户于对话中明确确认后, - 才能把 confirmed=true 一并传入;否则工具会拒绝。 -2. 修复工作流时遵循「诊断 → 定位根因 → 给出方案 → 用户确认 → 执行修复」的顺序, - 不得跳过确认直接修改调度或正式重跑。 -3. 常见修复手段:登录态失效(提示用户运行 gyxx accounts login/sync)、 - 幂等冲突(force_refresh 需真实执行且谨慎)、参数错误(改动态配置或调度)、 - 临时性失败(先试运行验证再正式重跑)。 -4. 诊断结果用中文输出,包含:失败步骤、根因推测、建议操作、风险说明。 - -当用户从工作台侧边栏选中工作流发起提问/诊断/修复时,会话的第一条消息会 -带有工作流上下文,请直接开始分析,不要反问用户基本信息。` - -/* ------------------------------------------------------------------ */ -/* 工具定义 */ -/* ------------------------------------------------------------------ */ - -const JSON_OUTPUT = { - schema: { type: 'object' }, - render: (_args, value) => [ - { type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }, - ], -} - -function toolErrorResult(error) { - return { - ok: false, - error: error instanceof ConsoleApiError ? error.message : `工具执行失败:${error?.message ?? error}`, - } -} - -function defineWorkflowTools(ctx, consoleApi) { - const register = (definition) => ctx.tools.register(definition) - - register({ - name: 'gyxx_workflow_list', - description: - '列出 GYXX 全部调度工作流:名称、模块、调度时间、最近一次运行状态与进行中的执行。', - parameters: { - type: 'object', - properties: { - module: { - type: 'string', - enum: Object.keys(MODULE_LABELS), - description: '按业务模块过滤,缺省返回全部', - }, - status: { - type: 'string', - enum: ['failed', 'running', 'success', 'none'], - description: '按最近一次运行状态过滤(none = 从未运行)', - }, - }, - additionalProperties: false, - }, - output: JSON_OUTPUT, - async execute(args, exec) { - try { - const overview = await consoleApi('GET', '/api/overview', undefined, { - signal: exec?.signal, - }) - let workflows = (overview.workflows ?? []).map(compactWorkflow) - if (args?.module) workflows = workflows.filter((w) => w.module === args.module) - if (args?.status) { - workflows = workflows.filter((w) => { - const status = w.active_run ? 'running' : (w.last_run?.status ?? 'none') - return args.status === 'running' ? status === 'running' || w.active_run : status === args.status - }) - } - return { ok: true, summary: overview.summary ?? null, workflows } - } catch (error) { - return toolErrorResult(error) - } - }, - }) - - register({ - name: 'gyxx_workflow_detail', - description: '查看单个工作流的完整定义:步骤、调度、依赖、重放策略与最近运行。', - parameters: { - type: 'object', - properties: { - workflow_id: { type: 'string', description: '工作流 ID,例如 content.metrics.daily' }, - }, - required: ['workflow_id'], - additionalProperties: false, - }, - output: JSON_OUTPUT, - async execute(args, exec) { - try { - const payload = await consoleApi( - 'GET', - `/api/workflows/${encodeURIComponent(args.workflow_id)}`, - undefined, - { signal: exec?.signal }, - ) - return { ok: true, ...compactDetail(payload) } - } catch (error) { - return toolErrorResult(error) - } - }, - }) - - register({ - name: 'gyxx_workflow_runs', - description: '查看某工作流最近的运行历史(含步骤级状态、退出码与脱敏错误)。', - parameters: { - type: 'object', - properties: { - workflow_id: { type: 'string' }, - limit: { type: 'integer', minimum: 1, maximum: 20, description: '默认 8,最大 20' }, - }, - required: ['workflow_id'], - additionalProperties: false, - }, - output: JSON_OUTPUT, - async execute(args, exec) { - try { - const limit = args?.limit ?? 8 - const payload = await consoleApi( - 'GET', - `/api/workflows/${encodeURIComponent(args.workflow_id)}/runs?limit=${limit}`, - undefined, - { signal: exec?.signal }, - ) - return { - ok: true, - workflow_id: payload.workflow_id, - runs: (payload.runs ?? []).map(compactRun), - } - } catch (error) { - return toolErrorResult(error) - } - }, - }) - - register({ - name: 'gyxx_workflow_diagnose', - description: - '获取一次运行的完整诊断包:运行状态、步骤明细、journal 追踪路径与脱敏日志尾部。' + - '不传 run_id 时自动选择最近一次失败的运行(无失败则最近一次运行)。', - parameters: { - type: 'object', - properties: { - workflow_id: { type: 'string' }, - run_id: { type: 'string', description: '缺省时自动选择最近一次失败运行' }, - }, - required: ['workflow_id'], - additionalProperties: false, - }, - output: JSON_OUTPUT, - async execute(args, exec) { - try { - let runId = args?.run_id - if (!runId) { - const runs = await consoleApi( - 'GET', - `/api/workflows/${encodeURIComponent(args.workflow_id)}/runs?limit=8`, - undefined, - { signal: exec?.signal }, - ) - const list = runs.runs ?? [] - const failed = list.find((run) => run.status === 'failed') - const chosen = failed ?? list[0] - if (!chosen) { - return { ok: true, message: '该工作流还没有任何运行记录', workflow_id: args.workflow_id } - } - runId = chosen.run_id - } - const payload = await consoleApi( - 'GET', - `/api/workflows/${encodeURIComponent(args.workflow_id)}/runs/${encodeURIComponent(runId)}/diagnosis`, - undefined, - { signal: exec?.signal }, - ) - return { ok: true, ...payload, run: compactRun(payload.run) } - } catch (error) { - return toolErrorResult(error) - } - }, - }) - - register({ - name: 'gyxx_workflow_trigger', - description: - '触发工作流运行。默认 execute=false 为试运行(演练,无外部副作用);' + - '正式执行必须 execute=true 且 confirmed=true(只有在用户明确确认后才允许)。', - parameters: { - type: 'object', - properties: { - workflow_id: { type: 'string' }, - business_date: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$', description: '业务日期 YYYY-MM-DD' }, - execute: { type: 'boolean', description: '缺省 false = 试运行' }, - confirmed: { type: 'boolean', description: '正式执行确认标记,须先征得用户确认' }, - shadow: { type: 'boolean', description: '影子模式(对比旧链路),缺省 false' }, - force_refresh: { type: 'boolean', description: '忽略幂等跳过强制重采(需正式执行)' }, - }, - required: ['workflow_id', 'business_date'], - additionalProperties: false, - }, - output: JSON_OUTPUT, - async execute(args, exec) { - const execute = args?.execute === true - const confirmed = args?.confirmed === true - if (execute && !confirmed) { - return { - ok: false, - error: - '正式执行(execute=true)会对外部系统产生真实写入。请先用中文向用户说明将执行的操作与影响,' + - '在用户明确确认后,再以 confirmed=true 重新调用本工具。', - } - } - try { - const payload = await consoleApi( - 'POST', - `/api/workflows/${encodeURIComponent(args.workflow_id)}/runs`, - { - business_date: args.business_date, - execute, - shadow: args?.shadow === true, - confirmed, - force_refresh: args?.force_refresh === true, - }, - { signal: exec?.signal }, - ) - return { ok: true, launched: payload } - } catch (error) { - return toolErrorResult(error) - } - }, - }) - - register({ - name: 'gyxx_workflow_cancel', - description: '停止某工作流正在进行的执行:手动运行需要 operation_id;定时运行用 scheduled=true。', - parameters: { - type: 'object', - properties: { - workflow_id: { type: 'string' }, - operation_id: { type: 'string', description: '手动运行的操作 ID(active_run.operation_id)' }, - scheduled: { type: 'boolean', description: 'true 表示停止调度器正在执行的运行' }, - }, - required: ['workflow_id'], - additionalProperties: false, - }, - output: JSON_OUTPUT, - async execute(args, exec) { - try { - if (args?.scheduled === true) { - const payload = await consoleApi( - 'DELETE', - `/api/workflows/${encodeURIComponent(args.workflow_id)}/scheduled-run`, - {}, - { signal: exec?.signal }, - ) - return { ok: true, result: payload } - } - if (typeof args?.operation_id !== 'string' || !args.operation_id) { - return { ok: false, error: '停止手动运行必须提供 operation_id(见 active_run.operation_id)' } - } - const payload = await consoleApi( - 'DELETE', - `/api/workflows/${encodeURIComponent(args.workflow_id)}/runs/${encodeURIComponent(args.operation_id)}`, - {}, - { signal: exec?.signal }, - ) - return { ok: true, result: payload } - } catch (error) { - return toolErrorResult(error) - } - }, - }) - - register({ - name: 'gyxx_schedule_update', - description: - '修改工作流的定时调度(启用/停用、启动时间、周期)。采用读-改-写:' + - '先用 gyxx_workflow_detail 拿到 schedule 与 schedule_revision,再把完整调度对象' + - '与 revision 一并提交。修改前必须向用户说明并获得确认。', - parameters: { - type: 'object', - properties: { - workflow_id: { type: 'string' }, - schedule_revision: { type: 'string', description: 'gyxx_workflow_detail 返回的版本号' }, - kind: { type: 'string', enum: ['daily', 'weekly', 'monthly', 'interval_days'] }, - at: { - description: '启动时间,HH:MM 字符串或字符串数组', - anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], - }, - enabled: { type: 'boolean' }, - days: { type: 'array', items: { type: 'string' }, description: 'weekly 时的星期列表' }, - day_of_month: { type: 'integer', description: 'monthly 时的日期' }, - every_days: { type: 'integer', description: 'interval_days 时的间隔天数' }, - business_date_offset_days: { type: 'integer', description: '业务日期偏移,缺省 0' }, - }, - required: ['workflow_id', 'schedule_revision', 'kind', 'at'], - additionalProperties: false, - }, - output: JSON_OUTPUT, - async execute(args, exec) { - try { - const body = { - kind: args.kind, - at: args.at, - enabled: args?.enabled ?? true, - business_date_offset_days: args?.business_date_offset_days ?? 0, - } - for (const key of ['days', 'day_of_month', 'every_days']) { - if (args?.[key] !== undefined) body[key] = args[key] - } - const payload = await consoleApi( - 'PUT', - `/api/workflows/${encodeURIComponent(args.workflow_id)}/schedule`, - body, - { signal: exec?.signal, ifMatch: args.schedule_revision }, - ) - return { ok: true, result: payload } - } catch (error) { - return toolErrorResult(error) - } - }, - }) -} - -/* ------------------------------------------------------------------ */ -/* 提问 / 诊断 / 修复会话 */ -/* ------------------------------------------------------------------ */ - -function buildSessionPrompt(action, workflow, question) { - const label = `${workflow.id}(${workflow.name ?? workflow.id})` - const last = workflow.last_run - const context = last - ? `最近一次运行:${last.status},业务日期 ${last.business_date},run_id=${last.run_id}` + - (last.error ? `,错误:${last.error}` : '') - : '该工作流暂无运行记录' - if (action === 'diagnose') { - return ( - `【工作台诊断请求】请诊断工作流 ${label}。\n${context}\n` + - '请调用 gyxx_workflow_diagnose 获取最近一次失败运行的诊断包,' + - '按「失败步骤 → 根因推测 → 建议修复 → 风险说明」输出中文诊断报告。' - ) - } - if (action === 'repair') { - return ( - `【工作台修复请求】请修复工作流 ${label}。\n${context}\n` + - '先用 gyxx_workflow_diagnose 定位失败根因,给出候选修复方案并逐条说明影响;' + - '任何正式执行或调度修改都必须先征得我的确认(遵守安全契约),确认后再执行。' - ) - } - return ( - `【工作台提问】工作流 ${label}。\n${context}\n` + - `我的问题:${question ?? '这个工作流现在状态如何?'}\n` + - '请结合 gyxx_workflow_detail / gyxx_workflow_runs / gyxx_workflow_diagnose 回答。' - ) -} - -/* ------------------------------------------------------------------ */ -/* 桥接服务(供侧边栏面板使用) */ -/* ------------------------------------------------------------------ */ - -function isLoopbackOrigin(origin) { - if (!origin) return null - try { - const url = new URL(origin) - if (url.protocol !== 'http:' && url.protocol !== 'https:') return null - if (['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)) return origin - } catch { - return null - } - return null -} - -function readJsonBody(req) { - return new Promise((resolve, reject) => { - let size = 0 - const chunks = [] - req.on('data', (chunk) => { - size += chunk.length - if (size > 64 * 1024) { - reject(new Error('请求正文过大')) - req.destroy() - return - } - chunks.push(chunk) - }) - req.on('end', () => { - if (chunks.length === 0) return resolve({}) - try { - resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))) - } catch { - reject(new Error('请求 JSON 无效')) - } - }) - req.on('error', reject) - }) -} - -function createBridgeServer(ctx, config, consoleApi, alerts, openChatSession) { - const standalonePage = () => { - try { - return readFileSync(join(PLUGIN_DIR, 'client', 'standalone.html')) - } catch { - return Buffer.from('

gyxx-workbench bridge

', 'utf8') - } - } - - const sendJson = (req, res, status, payload) => { - const origin = isLoopbackOrigin(req.headers.origin) - const body = JSON.stringify(payload) - res.writeHead(status, { - 'content-type': 'application/json; charset=utf-8', - 'cache-control': 'no-store', - ...(origin ? { 'access-control-allow-origin': origin, vary: 'origin' } : {}), - }) - res.end(body) - } - - const server = http.createServer(async (req, res) => { - const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`) - const path = url.pathname - const origin = isLoopbackOrigin(req.headers.origin) - - if (req.method === 'OPTIONS') { - res.writeHead(204, { - ...(origin - ? { - 'access-control-allow-origin': origin, - 'access-control-allow-methods': 'GET,POST,OPTIONS', - 'access-control-allow-headers': 'content-type,x-gyxx-workbench', - 'access-control-max-age': '3600', - vary: 'origin', - } - : {}), - }) - res.end() - return - } - - try { - if (req.method === 'GET' && path === '/') { - res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }) - res.end(standalonePage()) - return - } - if (req.method === 'GET' && path === '/healthz') { - sendJson(req, res, 200, { ok: true, name }) - return - } - if (req.method === 'GET' && path === '/bridge/workflows') { - const overview = await consoleApi('GET', '/api/overview') - sendJson(req, res, 200, { - ok: true, - summary: overview.summary ?? null, - generated_at: overview.generated_at ?? null, - workflows: (overview.workflows ?? []).map(compactWorkflow), - }) - return - } - const detailMatch = /^\/bridge\/workflows\/([^/]+)$/.exec(path) - if (req.method === 'GET' && detailMatch) { - const payload = await consoleApi( - 'GET', - `/api/workflows/${encodeURIComponent(decodeURIComponent(detailMatch[1]))}`, - ) - sendJson(req, res, 200, { ok: true, ...compactDetail(payload) }) - return - } - const runsMatch = /^\/bridge\/workflows\/([^/]+)\/runs$/.exec(path) - if (req.method === 'GET' && runsMatch) { - const limit = Math.min(Math.max(Number(url.searchParams.get('limit')) || 8, 1), 20) - const payload = await consoleApi( - 'GET', - `/api/workflows/${encodeURIComponent(decodeURIComponent(runsMatch[1]))}/runs?limit=${limit}`, - ) - sendJson(req, res, 200, { - ok: true, - workflow_id: payload.workflow_id, - runs: (payload.runs ?? []).map(compactRun), - }) - return - } - const diagnosisMatch = /^\/bridge\/workflows\/([^/]+)\/diagnosis$/.exec(path) - if (req.method === 'GET' && diagnosisMatch) { - const workflowId = decodeURIComponent(diagnosisMatch[1]) - let runId = url.searchParams.get('run_id') || '' - if (!runId) { - const runs = await consoleApi( - 'GET', - `/api/workflows/${encodeURIComponent(workflowId)}/runs?limit=8`, - ) - const list = runs.runs ?? [] - const chosen = list.find((run) => run.status === 'failed') ?? list[0] - if (!chosen) { - sendJson(req, res, 200, { ok: true, message: '该工作流暂无运行记录', logs: [] }) - return - } - runId = chosen.run_id - } - const payload = await consoleApi( - 'GET', - `/api/workflows/${encodeURIComponent(workflowId)}/runs/${encodeURIComponent(runId)}/diagnosis`, - ) - sendJson(req, res, 200, { ok: true, ...payload }) - return - } - if (req.method === 'GET' && path === '/bridge/alerts') { - sendJson(req, res, 200, { ok: true, alerts: alerts.list() }) - return - } - - // —— 以下为写操作:要求自定义头,防跨站表单提交 —— - if (req.method === 'POST' && (path === '/bridge/trigger' || path === '/bridge/cancel' || path === '/bridge/ask' || path === '/bridge/alerts/seen')) { - if (req.headers['x-gyxx-workbench'] !== '1') { - sendJson(req, res, 403, { ok: false, error: '缺少 x-gyxx-workbench 写操作标识' }) - return - } - const contentType = String(req.headers['content-type'] ?? '').split(';')[0].trim().toLowerCase() - if (contentType !== 'application/json') { - sendJson(req, res, 415, { ok: false, error: '写操作只接受 application/json' }) - return - } - const body = await readJsonBody(req) - - if (path === '/bridge/trigger') { - const execute = body.execute === true - const confirmed = body.confirmed === true - if (execute && !confirmed) { - sendJson(req, res, 400, { ok: false, error: '正式执行必须 confirmed=true' }) - return - } - const payload = await consoleApi( - 'POST', - `/api/workflows/${encodeURIComponent(String(body.workflow_id ?? ''))}/runs`, - { - business_date: String(body.business_date ?? ''), - execute, - shadow: body.shadow === true, - confirmed, - force_refresh: body.force_refresh === true, - }, - ) - sendJson(req, res, 200, { ok: true, launched: payload }) - return - } - if (path === '/bridge/cancel') { - const workflowId = String(body.workflow_id ?? '') - if (body.scheduled === true) { - const payload = await consoleApi( - 'DELETE', - `/api/workflows/${encodeURIComponent(workflowId)}/scheduled-run`, - {}, - ) - sendJson(req, res, 200, { ok: true, result: payload }) - return - } - const operationId = String(body.operation_id ?? '') - if (!operationId) { - sendJson(req, res, 400, { ok: false, error: '缺少 operation_id' }) - return - } - const payload = await consoleApi( - 'DELETE', - `/api/workflows/${encodeURIComponent(workflowId)}/runs/${encodeURIComponent(operationId)}`, - {}, - ) - sendJson(req, res, 200, { ok: true, result: payload }) - return - } - if (path === '/bridge/alerts/seen') { - alerts.markSeen(typeof body.id === 'string' ? body.id : null) - sendJson(req, res, 200, { ok: true }) - return - } - // /bridge/ask —— 创建工作流上下文会话 - const workflowId = String(body.workflow_id ?? '') - const action = ['ask', 'diagnose', 'repair'].includes(body.action) ? body.action : 'ask' - let workflow - try { - const detail = await consoleApi('GET', `/api/workflows/${encodeURIComponent(workflowId)}`) - workflow = compactWorkflow(detail.workflow ?? {}) - } catch { - workflow = { id: workflowId, name: workflowId, last_run: null } - } - const prompt = buildSessionPrompt(action, workflow, typeof body.question === 'string' ? body.question : '') - const sessionId = await openChatSession(prompt) - if (!sessionId) { - sendJson(req, res, 503, { - ok: false, - error: '当前 dsh 组合不支持编程式会话创建,请在对话中直接提问', - }) - return - } - sendJson(req, res, 200, { ok: true, session_id: sessionId, action }) - return - } - - sendJson(req, res, 404, { ok: false, error: '接口不存在' }) - } catch (error) { - const status = error instanceof ConsoleApiError && error.status ? error.status : 500 - sendJson(req, res, status, { - ok: false, - error: error?.message ?? '桥接服务内部错误', - }) - } - }) - - return server -} - -/* ------------------------------------------------------------------ */ -/* 插件入口 */ -/* ------------------------------------------------------------------ */ - -export function apply(ctx, rawConfig) { - const config = { - consoleBaseUrl: process.env.GYXX_CONSOLE_URL ?? 'http://127.0.0.1:8765', - consoleToken: process.env.GYXX_CONSOLE_TOKEN ?? '', - bridgeHost: '127.0.0.1', - bridgePort: 8790, - pollIntervalMs: 30_000, - autoDiagnose: false, - projectRoot: process.cwd(), - ...(rawConfig && typeof rawConfig === 'object' ? rawConfig : {}), - } - const consoleApi = makeConsoleClient(config) - - // 1) 智能体工具 - defineWorkflowTools(ctx, consoleApi) - - // 2) 系统提示词 - ctx.systemPrompt.section({ - name: 'gyxx-workbench', - order: 700, - text: SYSTEM_PROMPT_TEXT, - }) - - // 3) 失败监控器 + 告警缓存 - const state = { - seen: new Map(), - items: [], - } - const alerts = { - list: () => state.items.map((item) => ({ ...item })), - markSeen(id) { - if (id === null) state.items.forEach((item) => (item.seen = true)) - else { - const target = state.items.find((item) => item.id === id) - if (target) target.seen = true - } - }, - push(alert) { - state.items.unshift(alert) - if (state.items.length > 50) state.items.length = 50 - }, - } - - const openChatSession = async (prompt) => { - const agents = typeof ctx.get === 'function' ? ctx.get('agents') : ctx.agents - if (!agents || typeof agents.create !== 'function') return null - const sessionId = `gyxx-wb-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}` - const handle = await agents.create({ - sessionId, - meta: { cwd: config.projectRoot, origin: 'gyxx-workbench', isSeeded: true }, - }) - handle.agent.followup({ - id: randomUUID(), - role: 'user', - content: [{ type: 'text', text: prompt }], - source: { kind: 'user' }, - }) - return sessionId - } - - ctx.effect(() => { - let stopped = false - let timer = null - const poll = async () => { - try { - const overview = await consoleApi('GET', '/api/overview') - for (const item of overview.workflows ?? []) { - const last = item.last_run - if (!last || last.status !== 'failed') { - if (last && last.status !== 'failed') state.seen.delete(item.id) - continue - } - if (state.seen.get(item.id) === last.run_id) continue - state.seen.set(item.id, last.run_id) - const alert = { - id: randomUUID(), - kind: 'workflow-failed', - workflow_id: item.id, - name: item.name ?? item.id, - run_id: last.run_id, - business_date: last.business_date ?? null, - error: last.error ?? null, - at: new Date().toISOString(), - seen: false, - } - alerts.push(alert) - try { - ctx.emit('gyxx-workbench/alert', alert) - } catch { - // 自定义事件在某些组合上不可用时忽略 - } - if (config.autoDiagnose) { - const prompt = buildSessionPrompt('diagnose', compactWorkflow(item), '') - openChatSession(prompt)?.catch(() => {}) - } - } - } catch { - // 控制台离线时静默,下一轮继续 - } - if (!stopped) timer = setTimeout(poll, config.pollIntervalMs) - } - timer = setTimeout(poll, 5_000) - return () => { - stopped = true - if (timer) clearTimeout(timer) - } - }, 'gyxx-workbench: monitor') - - // 4) 桥接服务(侧边栏面板数据 + 会话创建 + 降级页面) - ctx.effect(async () => { - const server = createBridgeServer(ctx, config, consoleApi, alerts, openChatSession) - try { - await new Promise((resolve, reject) => { - server.once('error', reject) - server.listen(config.bridgePort, config.bridgeHost, resolve) - }) - } catch (error) { - // 端口占用等问题不应阻断插件加载(工具与监控仍可用) - ctx.logger?.warn?.( - `[gyxx-workbench] 桥接服务启动失败(${error?.message ?? error}),侧边栏面板不可用`, - ) - try { - server.close() - } catch { - // 忽略关闭异常 - } - return () => {} - } - const address = server.address() - ctx.logger?.info?.( - `[gyxx-workbench] 桥接服务已启动: http://${config.bridgeHost}:${address?.port ?? config.bridgePort}/`, - ) - return () => - new Promise((resolve) => { - server.close(() => resolve()) - }) - }, 'gyxx-workbench: bridge') -} diff --git a/workbench/plugin/lib/client.js b/workbench/plugin/lib/client.js deleted file mode 100644 index ab80fa2..0000000 --- a/workbench/plugin/lib/client.js +++ /dev/null @@ -1,580 +0,0 @@ -window.__ModuleLoader__.load({ id: "@gyxx/dsh-plugin-gyxx-workbench", factory: (require) => { -var module = { exports: {} }; -var exports = module.exports; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); - -// client/src/index.jsx -var index_exports = {}; -__export(index_exports, { - apply: () => apply, - inject: () => inject -}); -module.exports = __toCommonJS(index_exports); -var import_react4 = require("react"); - -// client/src/Panel.jsx -var import_react = require("react"); - -// client/src/store.js -var listeners = /* @__PURE__ */ new Set(); -var state = { - open: false, - loading: false, - error: null, - generatedAt: null, - summary: null, - workflows: [], - selectedId: null, - detail: null, - detailLoading: false, - runs: [], - diagnosis: null, - diagnosisLoading: false, - question: "", - businessDate: defaultBusinessDate(), - alerts: [], - seenAlertIds: {}, - toasts: [] -}; -function defaultBusinessDate() { - const now = /* @__PURE__ */ new Date(); - now.setDate(now.getDate() - 1); - const pad = (value) => String(value).padStart(2, "0"); - return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; -} -function getState() { - return state; -} -function update(patch) { - state = { ...state, ...patch }; - for (const listener of listeners) listener(); -} -function subscribe(listener) { - listeners.add(listener); - return () => listeners.delete(listener); -} -function pushToast(toast) { - const id = `t-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`; - update({ toasts: [...state.toasts, { id, ...toast }] }); - return id; -} -function dismissToast(id) { - update({ toasts: state.toasts.filter((toast) => toast.id !== id) }); -} - -// client/src/api.js -function bridgeBase() { - if (typeof window !== "undefined") { - if (window.__GYXX_WB_BRIDGE__) return window.__GYXX_WB_BRIDGE__; - try { - const saved = window.localStorage?.getItem("gyxx.wb.bridge"); - if (saved) return saved; - } catch { - } - } - return "http://127.0.0.1:8790"; -} -async function request(method, path, body) { - const response = await fetch(`${bridgeBase()}${path}`, { - method, - headers: { - accept: "application/json", - ...body !== void 0 ? { "content-type": "application/json", "x-gyxx-workbench": "1" } : {} - }, - body: body === void 0 ? void 0 : JSON.stringify(body) - }); - let payload; - try { - payload = await response.json(); - } catch { - payload = { ok: false, error: `\u6865\u63A5\u670D\u52A1\u8FD4\u56DE\u4E86\u975E JSON \u5185\u5BB9\uFF08HTTP ${response.status}\uFF09` }; - } - if (!response.ok) { - throw new Error(payload?.error ?? `\u6865\u63A5\u670D\u52A1\u9519\u8BEF\uFF08HTTP ${response.status}\uFF09`); - } - return payload; -} -var api = { - listWorkflows: () => request("GET", "/bridge/workflows"), - workflowDetail: (workflowId) => request("GET", `/bridge/workflows/${encodeURIComponent(workflowId)}`), - workflowRuns: (workflowId, limit = 6) => request("GET", `/bridge/workflows/${encodeURIComponent(workflowId)}/runs?limit=${limit}`), - diagnosis: (workflowId, runId) => request( - "GET", - `/bridge/workflows/${encodeURIComponent(workflowId)}/diagnosis${runId ? `?run_id=${encodeURIComponent(runId)}` : ""}` - ), - alerts: () => request("GET", "/bridge/alerts"), - markAlertSeen: (id) => request("POST", "/bridge/alerts/seen", { id }), - trigger: (payload) => request("POST", "/bridge/trigger", payload), - cancel: (payload) => request("POST", "/bridge/cancel", payload), - ask: (payload) => request("POST", "/bridge/ask", payload) -}; - -// client/src/Panel.jsx -var import_jsx_runtime = require("react/jsx-runtime"); -function useWorkbench() { - return (0, import_react.useSyncExternalStore)(subscribe, getState); -} -var STATUS_LABELS = { - success: "\u6210\u529F", - failed: "\u5931\u8D25", - running: "\u8FD0\u884C\u4E2D", - cancelled: "\u5DF2\u505C\u6B62", - none: "\u672A\u8FD0\u884C" -}; -function statusOf(workflow) { - if (workflow.active_run) return "running"; - return workflow.last_run?.status ?? "none"; -} -function formatTime(value) { - if (!value) return "\u2014"; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return String(value); - const pad = (n) => String(n).padStart(2, "0"); - return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`; -} -function formatDuration(seconds) { - if (seconds == null) return ""; - if (seconds < 60) return `${seconds}s`; - return `${Math.floor(seconds / 60)}m${seconds % 60 ? `${seconds % 60}s` : ""}`; -} -async function refreshWorkflows() { - update({ loading: true, error: null }); - try { - const payload = await api.listWorkflows(); - update({ - loading: false, - workflows: payload.workflows ?? [], - summary: payload.summary ?? null, - generatedAt: payload.generated_at ?? null - }); - } catch (error) { - update({ loading: false, error: error.message }); - } -} -async function selectWorkflow(workflowId) { - update({ selectedId: workflowId, detailLoading: true, diagnosis: null, runs: [] }); - try { - const [detail, runs] = await Promise.all([ - api.workflowDetail(workflowId).catch(() => null), - api.workflowRuns(workflowId, 6).catch(() => ({ runs: [] })) - ]); - update({ detailLoading: false, detail, runs: runs.runs ?? [] }); - } catch (error) { - update({ detailLoading: false, error: error.message }); - } -} -async function loadDiagnosis(workflowId, runId) { - update({ diagnosisLoading: true }); - try { - const payload = await api.diagnosis(workflowId, runId); - update({ diagnosisLoading: false, diagnosis: payload }); - } catch (error) { - update({ diagnosisLoading: false }); - pushToast({ kind: "error", text: `\u8BCA\u65AD\u6570\u636E\u52A0\u8F7D\u5931\u8D25\uFF1A${error.message}` }); - } -} -async function doAsk(action) { - const s = getState(); - if (!s.selectedId) return; - const question = s.question.trim(); - try { - const payload = await api.ask({ - workflow_id: s.selectedId, - action, - question - }); - const label = { ask: "\u63D0\u95EE", diagnose: "\u8BCA\u65AD", repair: "\u4FEE\u590D" }[action] ?? "\u4F1A\u8BDD"; - pushToast({ - kind: "ok", - 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` - }); - update({ question: "" }); - } catch (error) { - pushToast({ kind: "error", text: `\u521B\u5EFA\u4F1A\u8BDD\u5931\u8D25\uFF1A${error.message}` }); - } -} -async function doTrigger(execute) { - const s = getState(); - if (!s.selectedId) return; - if (execute) { - const ok = window.confirm( - `\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` - ); - if (!ok) return; - } - try { - await api.trigger({ - workflow_id: s.selectedId, - business_date: s.businessDate, - execute, - confirmed: execute - }); - 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" }); - setTimeout(refreshWorkflows, 1500); - } catch (error) { - pushToast({ kind: "error", text: `\u542F\u52A8\u5931\u8D25\uFF1A${error.message}` }); - } -} -async function doCancel() { - const s = getState(); - const active = s.detail?.workflow?.active_run; - if (!s.selectedId) return; - try { - if (active?.operation_id) { - await api.cancel({ workflow_id: s.selectedId, operation_id: active.operation_id }); - } else { - await api.cancel({ workflow_id: s.selectedId, scheduled: true }); - } - pushToast({ kind: "ok", text: "\u5DF2\u53D1\u9001\u505C\u6B62\u6307\u4EE4" }); - setTimeout(refreshWorkflows, 1500); - } catch (error) { - pushToast({ kind: "error", text: `\u505C\u6B62\u5931\u8D25\uFF1A${error.message}` }); - } -} -function SummaryPills({ summary, workflows }) { - const failed = workflows.filter((w) => statusOf(w) === "failed").length; - const running = workflows.filter((w) => w.active_run).length; - return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-pills", children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "gyxxwb-pill", children: [ - "\u5171 ", - summary?.total ?? workflows.length - ] }), - running > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "gyxxwb-pill gyxxwb-pill-running", children: [ - "\u8FD0\u884C\u4E2D ", - running - ] }), - failed > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "gyxxwb-pill gyxxwb-pill-failed", children: [ - "\u5931\u8D25 ", - failed - ] }) - ] }); -} -function WorkflowRow({ workflow, selected, onSelect }) { - const status = statusOf(workflow); - return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( - "button", - { - type: "button", - className: `gyxxwb-row ${selected ? "gyxxwb-row-selected" : ""}`, - onClick: () => onSelect(workflow.id), - children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `gyxxwb-dot gyxxwb-dot-${status}` }), - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "gyxxwb-row-main", children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gyxxwb-row-name", children: workflow.name }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gyxxwb-row-id", children: workflow.id }) - ] }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gyxxwb-row-meta", children: workflow.last_run ? formatTime(workflow.last_run.ended_at ?? workflow.last_run.started_at) : "\u2014" }) - ] - } - ); -} -function RunList({ runs, onDiagnose }) { - if (!runs.length) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "gyxxwb-empty", children: "\u6682\u65E0\u8FD0\u884C\u8BB0\u5F55" }); - return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "gyxxwb-runs", children: runs.map((run) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( - "button", - { - type: "button", - className: "gyxxwb-run", - onClick: () => onDiagnose(run.run_id), - title: "\u70B9\u51FB\u67E5\u770B\u8BCA\u65AD\u8BE6\u60C5", - children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `gyxxwb-dot gyxxwb-dot-${run.status}` }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: run.business_date }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gyxxwb-run-mode", children: run.mode === "execute" ? "\u6B63\u5F0F" : "\u8BD5\u8FD0\u884C" }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: formatDuration(run.duration_seconds) }), - run.error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gyxxwb-run-error", children: run.error.slice(0, 60) }) - ] - }, - run.run_id - )) }); -} -function DiagnosisView({ diagnosis, loading }) { - if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "gyxxwb-empty", children: "\u6B63\u5728\u52A0\u8F7D\u8BCA\u65AD\u6570\u636E\u2026" }); - if (!diagnosis) return null; - if (diagnosis.message) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "gyxxwb-empty", children: diagnosis.message }); - const run = diagnosis.run ?? {}; - const steps = Array.isArray(run.steps) ? run.steps : []; - const logs = Array.isArray(diagnosis.logs) ? diagnosis.logs : []; - return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-diagnosis", children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-diagnosis-head", children: [ - "\u8BCA\u65AD\uFF1A", - run.run_id, - "\uFF08", - run.status, - "\uFF09" - ] }), - steps.filter((step) => step.status === "failed").map((step) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-step-fail", children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-step-id", children: [ - "\u2717 ", - step.id, - "\uFF08\u9000\u51FA\u7801 ", - step.exit_code ?? "\u2014", - "\uFF09" - ] }), - step.error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { className: "gyxxwb-pre", children: step.error }) - ] }, step.id)), - logs.map((log) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-log", children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "gyxxwb-log-path", children: log.path }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { className: "gyxxwb-pre", children: log.tail }) - ] }, log.path)), - !steps.some((s) => s.status === "failed") && !logs.length && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "gyxxwb-empty", children: "\u8BE5\u8FD0\u884C\u6CA1\u6709\u5931\u8D25\u6B65\u9AA4\u6216\u65E5\u5FD7" }) - ] }); -} -function WorkflowPanel() { - const s = useWorkbench(); - (0, import_react.useEffect)(() => { - if (!s.open) return void 0; - refreshWorkflows(); - const timer = setInterval(refreshWorkflows, 1e4); - return () => clearInterval(timer); - }, [s.open]); - const onSelect = (0, import_react.useCallback)((workflowId) => { - selectWorkflow(workflowId); - }, []); - const groups = []; - const byModule = /* @__PURE__ */ new Map(); - for (const workflow of s.workflows) { - const key = workflow.module_label ?? workflow.module; - if (!byModule.has(key)) byModule.set(key, []); - byModule.get(key).push(workflow); - } - for (const [label, items] of byModule) groups.push({ label, items }); - const selected = s.workflows.find((w) => w.id === s.selectedId) ?? null; - return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `gyxxwb-panel ${s.open ? "gyxxwb-panel-open" : ""}`, children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-panel-head", children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gyxxwb-title", children: "GYXX \u5DE5\u4F5C\u6D41" }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SummaryPills, { summary: s.summary, workflows: s.workflows }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "gyxxwb-icon-btn", title: "\u5237\u65B0", onClick: refreshWorkflows, children: "\u27F3" }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - "button", - { - type: "button", - className: "gyxxwb-icon-btn", - title: "\u5173\u95ED", - onClick: () => update({ open: false }), - children: "\u2715" - } - ) - ] }), - s.error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-error", children: [ - "\u65E0\u6CD5\u8FDE\u63A5\u5DE5\u4F5C\u53F0\u6865\u63A5\u670D\u52A1\uFF1A", - s.error, - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {}), - "\u8BF7\u786E\u8BA4 dsh \u5DF2\u901A\u8FC7 workbench/cordis.yml \u542F\u52A8\u3002" - ] }), - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-panel-body", children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-list", children: [ - groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-group", children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "gyxxwb-group-label", children: group.label }), - group.items.map((workflow) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - WorkflowRow, - { - workflow, - selected: workflow.id === s.selectedId, - onSelect - }, - workflow.id - )) - ] }, group.label)), - !s.loading && !s.workflows.length && !s.error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "gyxxwb-empty", children: "\u672A\u53D1\u73B0\u5DE5\u4F5C\u6D41" }) - ] }), - selected && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-detail", children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-detail-title", children: [ - selected.name, - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `gyxxwb-status gyxxwb-status-${statusOf(selected)}`, children: STATUS_LABELS[statusOf(selected)] }) - ] }), - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-detail-sub", children: [ - selected.id, - " \xB7 \u8C03\u5EA6", - " ", - selected.schedule ? `${selected.schedule.enabled ? "\u542F\u7528" : "\u505C\u7528"} ${Array.isArray(selected.schedule.at) ? selected.schedule.at.join("/") : selected.schedule.at ?? ""}` : "\u65E0", - selected.next_run_at ? ` \xB7 \u4E0B\u6B21 ${formatTime(selected.next_run_at)}` : "" - ] }), - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-actions", children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => doAsk("diagnose"), children: "\u8BCA\u65AD" }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => doAsk("repair"), children: "\u4FEE\u590D" }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => doTrigger(false), children: "\u8BD5\u8FD0\u884C" }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "gyxxwb-danger", onClick: () => doTrigger(true), children: "\u6B63\u5F0F\u8FD0\u884C" }), - selected.active_run && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "gyxxwb-danger", onClick: doCancel, children: "\u505C\u6B62" }) - ] }), - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-ask", children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - "input", - { - value: s.question, - placeholder: "\u5C31\u8FD9\u4E2A\u5DE5\u4F5C\u6D41\u63D0\u95EE\uFF0C\u4F8B\u5982\uFF1A\u6628\u5929\u4E3A\u4EC0\u4E48\u5931\u8D25\uFF1F", - onChange: (event) => update({ question: event.target.value }), - onKeyDown: (event) => { - if (event.key === "Enter") doAsk("ask"); - } - } - ), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => doAsk("ask"), children: "\u63D0\u95EE" }) - ] }), - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gyxxwb-date-row", children: [ - "\u4E1A\u52A1\u65E5\u671F", - " ", - /* @__PURE__ */ (0, import_jsx_runtime.jsx)( - "input", - { - value: s.businessDate, - onChange: (event) => update({ businessDate: event.target.value }), - pattern: "\\d{4}-\\d{2}-\\d{2}" - } - ) - ] }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "gyxxwb-section-label", children: "\u6700\u8FD1\u8FD0\u884C" }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RunList, { runs: s.runs, onDiagnose: (runId) => loadDiagnosis(selected.id, runId) }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiagnosisView, { diagnosis: s.diagnosis, loading: s.diagnosisLoading }) - ] }) - ] }) - ] }); -} - -// client/src/Toasts.jsx -var import_react2 = require("react"); -var import_react3 = require("react"); -var import_jsx_runtime2 = require("react/jsx-runtime"); -function useWorkbench2() { - return (0, import_react3.useSyncExternalStore)(subscribe, getState); -} -function Toasts() { - const s = useWorkbench2(); - (0, import_react2.useEffect)(() => { - let stopped = false; - let timer = null; - const poll = async () => { - try { - const payload = await api.alerts(); - const fresh = (payload.alerts ?? []).filter( - (alert) => !alert.seen && !getState().seenAlertIds[alert.id] - ); - if (fresh.length) { - const seen = { ...getState().seenAlertIds }; - for (const alert of fresh) { - seen[alert.id] = true; - pushToast({ kind: "failed", text: `\u5DE5\u4F5C\u6D41\u300C${alert.name}\u300D\u8FD0\u884C\u5931\u8D25`, alert }); - } - update({ seenAlertIds: seen }); - } - } catch { - } - if (!stopped) timer = setTimeout(poll, 3e4); - }; - timer = setTimeout(poll, 8e3); - return () => { - stopped = true; - if (timer) clearTimeout(timer); - }; - }, []); - if (!s.toasts.length) return null; - return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gyxxwb-toasts", children: s.toasts.map((toast) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `gyxxwb-toast gyxxwb-toast-${toast.kind}`, children: [ - /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gyxxwb-toast-text", children: toast.text }), - toast.alert && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( - "button", - { - type: "button", - onClick: async () => { - try { - await api.ask({ workflow_id: toast.alert.workflow_id, action: "diagnose" }); - await api.markAlertSeen(toast.alert.id).catch(() => { - }); - pushToast({ kind: "ok", text: "\u5DF2\u521B\u5EFA\u8BCA\u65AD\u4F1A\u8BDD\uFF0C\u8BF7\u5728\u5DE6\u4FA7\u4F1A\u8BDD\u5217\u8868\u67E5\u770B" }); - } catch (error) { - pushToast({ kind: "error", text: `\u521B\u5EFA\u4F1A\u8BDD\u5931\u8D25\uFF1A${error.message}` }); - } - dismissToast(toast.id); - }, - children: "\u7ACB\u5373\u8BCA\u65AD" - } - ), - /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", title: "\u5173\u95ED", onClick: () => dismissToast(toast.id), children: "\u2715" }) - ] }, toast.id)) }); -} - -// client/src/styles.css -var styles_default = "/* GYXX \u5DE5\u4F5C\u53F0\u9762\u677F\u6837\u5F0F \u2014\u2014 \u4EE5\u6587\u672C\u5F62\u5F0F\u5185\u8054\u8FDB\u5BA2\u6237\u7AEF\u5305\uFF0C\u8FD0\u884C\u65F6\u6CE8\u5165