/** * 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') }