/** * 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('