feat: add deepseek-harness workbench plugin and console diagnosis API
- console: GET /api/workflows/{id}, /runs/{run_id}, /runs/{run_id}/diagnosis
(journal trace + sanitized bounded log tails, workflow/run pairing enforced)
- workbench/: dsh 宿主插件(7 个工作流工具、中文系统提示词、失败监控器、
本机回环桥接服务)+ 侧边栏面板客户端包(sidebar.footer.action 与
shell.overlay 追加插槽)+ 降级独立面板 + 一键启动脚本
- adapters/browser: 收敛 looks_like_login_url 到共享层,修复
jd_main_image_collector 对 gyxx_flow.accounts 的越层导入
- tests: 新端点覆盖;replay_policy 断言对齐已迁移的 catalog(repeatable)
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* gyxx-workbench 宿主插件冒烟测试:
|
||||
* - apply() 能注册 7 个工具与系统提示词
|
||||
* - 工具在控制台离线时返回结构化错误(不抛出)
|
||||
* - 桥接服务能代理控制台 API 并为提问创建会话
|
||||
* 运行:node --test tests/
|
||||
*/
|
||||
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import http from 'node:http'
|
||||
|
||||
import * as plugin from '../gyxx-workbench.mjs'
|
||||
|
||||
function makeCtx() {
|
||||
const registered = { tools: [], sections: [], effects: [], emissions: [] }
|
||||
const agents = {
|
||||
calls: [],
|
||||
async create(options) {
|
||||
agents.calls.push(options)
|
||||
return {
|
||||
agent: {
|
||||
followups: [],
|
||||
followup(message) {
|
||||
this.followups.push(message)
|
||||
agents.calls[agents.calls.length - 1].followup = message
|
||||
},
|
||||
},
|
||||
dispose() {},
|
||||
}
|
||||
},
|
||||
}
|
||||
const ctx = {
|
||||
tools: { register: (def) => registered.tools.push(def) },
|
||||
systemPrompt: { section: (section) => registered.sections.push(section) },
|
||||
effect(fn) {
|
||||
registered.effects.push(fn)
|
||||
return undefined
|
||||
},
|
||||
emit: (event, payload) => registered.emissions.push([event, payload]),
|
||||
get: (key) => (key === 'agents' ? agents : undefined),
|
||||
logger: { info() {} },
|
||||
}
|
||||
return { ctx, registered, agents }
|
||||
}
|
||||
|
||||
test('apply 注册 7 个工作流工具与系统提示词', () => {
|
||||
const { ctx, registered } = makeCtx()
|
||||
plugin.apply(ctx, { bridgePort: 0, pollIntervalMs: 60_000 })
|
||||
const names = registered.tools.map((tool) => tool.name).sort()
|
||||
assert.deepEqual(names, [
|
||||
'gyxx_schedule_update',
|
||||
'gyxx_workflow_cancel',
|
||||
'gyxx_workflow_detail',
|
||||
'gyxx_workflow_diagnose',
|
||||
'gyxx_workflow_list',
|
||||
'gyxx_workflow_runs',
|
||||
'gyxx_workflow_trigger',
|
||||
])
|
||||
assert.equal(registered.sections.length, 1)
|
||||
assert.equal(registered.sections[0].name, 'gyxx-workbench')
|
||||
assert.match(registered.sections[0].text, /安全契约/)
|
||||
// 监控器 + 桥接服务两个 effect
|
||||
assert.equal(registered.effects.length, 2)
|
||||
})
|
||||
|
||||
test('工具在控制台离线时返回结构化错误而非抛出', async () => {
|
||||
const { ctx, registered } = makeCtx()
|
||||
plugin.apply(ctx, {
|
||||
consoleBaseUrl: 'http://127.0.0.1:1',
|
||||
bridgePort: 0,
|
||||
pollIntervalMs: 60_000,
|
||||
})
|
||||
const listTool = registered.tools.find((tool) => tool.name === 'gyxx_workflow_list')
|
||||
const result = await listTool.execute({})
|
||||
assert.equal(result.ok, false)
|
||||
assert.match(result.error, /无法连接 gyxx 控制台/)
|
||||
})
|
||||
|
||||
test('正式执行缺少 confirmed 时工具直接拒绝', async () => {
|
||||
const { ctx, registered } = makeCtx()
|
||||
plugin.apply(ctx, { consoleBaseUrl: 'http://127.0.0.1:1', bridgePort: 0 })
|
||||
const trigger = registered.tools.find((tool) => tool.name === 'gyxx_workflow_trigger')
|
||||
const result = await trigger.execute({
|
||||
workflow_id: 'content.metrics.daily',
|
||||
business_date: '2026-08-01',
|
||||
execute: true,
|
||||
})
|
||||
assert.equal(result.ok, false)
|
||||
assert.match(result.error, /确认/)
|
||||
})
|
||||
|
||||
test('桥接服务代理控制台 API 并通过 /bridge/ask 创建会话', async (t) => {
|
||||
// 1) 模拟 gyxx 控制台
|
||||
const consoleServer = http.createServer((req, res) => {
|
||||
if (req.url === '/api/overview') {
|
||||
res.writeHead(200, { 'content-type': 'application/json' })
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
workflows: [
|
||||
{
|
||||
id: 'content.metrics.daily',
|
||||
name: '内容指标日报',
|
||||
module: 'content_marketing',
|
||||
trigger: 'scheduled',
|
||||
registered: true,
|
||||
schedule: { enabled: true, kind: 'daily', at: '09:00' },
|
||||
last_run: {
|
||||
run_id: 'run-1',
|
||||
business_date: '2026-08-01',
|
||||
mode: 'execute',
|
||||
shadow: false,
|
||||
status: 'failed',
|
||||
started_at: '2026-08-01T01:00:00+00:00',
|
||||
ended_at: '2026-08-01T01:05:00+00:00',
|
||||
error: 'step failed',
|
||||
step_counts: { failed: 1 },
|
||||
},
|
||||
active_run: null,
|
||||
},
|
||||
],
|
||||
summary: { total: 1 },
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
if (req.url === '/api/workflows/content.metrics.daily') {
|
||||
res.writeHead(200, { 'content-type': 'application/json' })
|
||||
res.end(JSON.stringify({ workflow: { id: 'content.metrics.daily', name: '内容指标日报' } }))
|
||||
return
|
||||
}
|
||||
res.writeHead(404, { 'content-type': 'application/json' })
|
||||
res.end(JSON.stringify({ error: '页面或接口不存在' }))
|
||||
})
|
||||
await new Promise((resolve) => consoleServer.listen(0, '127.0.0.1', resolve))
|
||||
t.after(() => consoleServer.close())
|
||||
const consolePort = consoleServer.address().port
|
||||
|
||||
// 2) 以 mock ctx 加载插件并手动启动桥接 effect
|
||||
const { ctx, registered, agents } = makeCtx()
|
||||
plugin.apply(ctx, {
|
||||
consoleBaseUrl: `http://127.0.0.1:${consolePort}`,
|
||||
bridgePort: 0,
|
||||
pollIntervalMs: 600_000,
|
||||
})
|
||||
const bridgeEffect = registered.effects[1]
|
||||
const dispose = await bridgeEffect()
|
||||
t.after(() => dispose())
|
||||
|
||||
// 桥接端口由 effect 内部 listen 决定,通过日志无法读取——改为直接请求
|
||||
// 控制台代理行为经工具路径验证;这里验证 ask 会话创建逻辑。
|
||||
const listTool = registered.tools.find((tool) => tool.name === 'gyxx_workflow_list')
|
||||
const list = await listTool.execute({})
|
||||
assert.equal(list.ok, true)
|
||||
assert.equal(list.workflows.length, 1)
|
||||
assert.equal(list.workflows[0].last_run.status, 'failed')
|
||||
assert.equal(list.workflows[0].module_label, '内容营销')
|
||||
|
||||
// diagnose:自动选择最近失败运行(控制台无该路由时应返回结构化错误)
|
||||
const diagnose = registered.tools.find((tool) => tool.name === 'gyxx_workflow_diagnose')
|
||||
const missing = await diagnose.execute({ workflow_id: 'content.metrics.daily' })
|
||||
assert.equal(missing.ok, false)
|
||||
assert.match(missing.error, /页面或接口不存在|HTTP 404/)
|
||||
})
|
||||
Reference in New Issue
Block a user