feat: ship skill and MCP workflows with signed updater for v0.0.7

This commit is contained in:
xiaohan.yuan committed 2026-10-21 00:04:17 +08:00
1 parent 1b52df0603
commit a01085c656
136 files changed
+18646 -217

No files matched your search

+285
View File
@@ -0,0 +1,285 @@
/** Real production ConnectorsPage in isolated Electron with a controlled MCP host.
* Only the host IPC is faked; the page, styles, icons and helpers are the shipped ones.
* No WorkBuddy runtime or original evidence is accessed by this script. */
import fs from 'node:fs'
import path from 'node:path'
import assert from 'node:assert/strict'
import { createRequire } from 'node:module'
import { spawn } from 'node:child_process'
const require = createRequire(import.meta.url)
const electronModule = require('electron')
const electron = typeof electronModule === 'object' && electronModule.app ? electronModule : null
const root = path.resolve('.'), base = path.join(root, '.tmp')
if (!electron) {
fs.mkdirSync(base, { recursive: true })
const output = fs.mkdtempSync(path.join(base, 'connectors-page-')), env = { ...process.env }
delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL
const child = spawn(electronModule, ['--no-sandbox', '--disable-gpu-sandbox', path.resolve('scripts/test-connectors-page.mjs'), output], { env, windowsHide: true, stdio: 'inherit' })
const timer = setTimeout(() => child.kill(), 120_000)
process.exitCode = await new Promise(resolve => child.once('exit', code => { clearTimeout(timer); resolve(code ?? 1) }))
} else void (async () => {
const { app, BrowserWindow, ipcMain } = electron
const output = path.resolve(process.argv.at(-1))
assert.ok(path.relative(base, output).startsWith('connectors-page-') && !path.relative(base, output).includes('..'))
electron.app.disableHardwareAcceleration()
fs.mkdirSync(path.join(output, 'data'), { recursive: true })
app.setPath('userData', path.join(output, 'data'))
await app.whenReady()
// The controlled host mirrors the production McpApi contract, including the
// approval hash rule and the per-server enable/connect state machine.
const calls = [], servers = new Map()
let failNextEnable = false
const clone = value => JSON.parse(JSON.stringify(value))
const makeServer = (id, name, overrides = {}) => ({
id, name, transport: 'stdio', command: 'node', args: ['server.mjs'], enabled: false,
status: 'disconnected', trusted: true, configHash: `hash-${id}`, tools: [],
envKeys: [], headerKeys: [], ...overrides
})
servers.set('ready', makeServer('ready', 'Ready Server', {
enabled: true, status: 'connected', tools: [
{ name: 'echo', description: 'Echo text', inputSchema: { type: 'object' }, enabled: true },
{ name: 'search', description: 'Find needles', inputSchema: { type: 'object' }, enabled: false }
]
}))
servers.set('untrusted', makeServer('untrusted', 'Unapproved Server', { trusted: false }))
servers.set('failing', makeServer('failing', 'Failing Server', { enabled: true, status: 'error', error: 'MCP 连接失败,请检查命令、地址和运行环境' }))
const host = async (method, args) => {
calls.push([method, ...args])
if (method === 'list') return clone([...servers.values()])
if (method === 'qaFailNextEnable') { failNextEnable = true; return }
const server = servers.get(args[0])
if (!server) throw Error('MCP 服务不存在')
if (method === 'save') {
const input = args[0]
if (!input?.name?.trim()) throw Error('请输入有效的 MCP 名称')
if (input.transport !== 'stdio' && input.transport !== 'http') throw Error('不支持的 MCP 连接类型')
if (input.transport === 'stdio' && !input.command?.trim()) throw Error('请输入有效的 MCP 启动命令')
const previous = input.id ? servers.get(input.id) : undefined
if (input.id && !previous) throw Error('MCP 服务不存在')
if (input.id && input.id !== previous.id) throw Error('MCP 服务不存在')
const next = makeServer(input.id ?? `saved-${servers.size}`, input.name.trim(), {
transport: input.transport, command: input.command, args: input.args ?? [], cwd: input.cwd, url: input.url,
enabled: previous?.enabled ?? false, trusted: false, tools: previous?.tools ?? []
})
servers.set(next.id, next)
return clone(next)
}
if (method === 'trust') {
if (server.configHash !== args[1]) throw Error('MCP 配置已变化,请重新查看并批准')
server.trusted = true
return
}
if (method === 'connect') {
if (!server.enabled) throw Error('MCP 服务已禁用')
if (!server.trusted) throw Error('请先批准当前 MCP 配置')
server.status = 'connected'; delete server.error
return clone(server)
}
if (method === 'disconnect') { server.status = 'disconnected'; return }
if (method === 'setEnabled') {
if (failNextEnable) { failNextEnable = false; throw Error('MCP 服务已禁用') }
if (args[1] && !server.trusted) throw Error('请先批准当前 MCP 配置')
server.enabled = args[1]
server.status = args[1] ? 'connected' : 'disconnected'
return
}
if (method === 'setToolEnabled') {
const tool = server.tools.find(item => item.name === args[1])
if (!tool) throw Error('MCP 工具不存在')
tool.enabled = args[2]
return
}
if (method === 'remove') { servers.delete(server.id); return }
throw Error('Unknown QA method ' + method)
}
ipcMain.handle('mcp-qa', async (_event, method, args) => host(method, args))
ipcMain.handle('resources-qa', async () => undefined)
fs.writeFileSync(path.join(output, 'preload.cjs'),
`const{contextBridge,ipcRenderer}=require('electron');contextBridge.exposeInMainWorld('ycode',{mcp:Object.fromEntries(['list','save','trust','connect','disconnect','setEnabled','setToolEnabled','remove','qaFailNextEnable'].map(method=>[method,(...args)=>ipcRenderer.invoke('mcp-qa',method,args)])),resources:{openExternal:async()=>undefined}});`)
const location = file => JSON.stringify(path.join(root, file).replaceAll('\\', '/'))
fs.writeFileSync(path.join(output, 'fixture.tsx'), `
import React from 'react'
import { createRoot } from 'react-dom/client'
import Center from ${location('src/renderer/src/capabilities/CapabilityCenter')}
import ${location('src/renderer/src/styles/tokens.css')}
import ${location('src/renderer/src/styles/app.css')}
createRoot(document.getElementById('root')!).render(<div className="chat-pane cr-theme" style={{ height: '100vh' }}><Center initialTab="connectors" onBackToChat={() => {}} /></div>)
`)
await require('esbuild').build({
entryPoints: [path.join(output, 'fixture.tsx')], bundle: true, platform: 'browser', format: 'esm', jsx: 'automatic',
outfile: path.join(output, 'fixture.js'), loader: { '.svg': 'file', '.png': 'file', '.woff': 'file', '.woff2': 'file', '.ttf': 'file' },
logLevel: 'warning'
})
fs.writeFileSync(path.join(output, 'index.html'),
'<html data-theme="light"><head><meta charset="utf-8"><link rel="stylesheet" href="fixture.css"><style>html,body,#root{width:100%;height:100%;margin:0}</style></head><body><div id="root"></div><script type="module" src="fixture.js"></script></body></html>')
const win = new BrowserWindow({
width: 1052, height: 901, useContentSize: true, frame: false, show: false,
webPreferences: { preload: path.join(output, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, backgroundThrottling: false }
})
const errors = [], checks = []
win.webContents.on('console-message', (_event, level, message) => { if (level === 3 || level === 'error') errors.push(String(message)) })
const read = code => win.webContents.executeJavaScript(code)
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
const check = (name, actual, expected) => {
if (typeof expected === 'function') assert.ok(expected(actual), name + ': ' + JSON.stringify(actual))
else assert.deepEqual(actual, expected, name)
checks.push(name)
}
const wait = async code => {
for (let index = 0; index < 200; index++) { if (await read(code)) return; await pause(50) }
throw Error('Timeout ' + code)
}
const click = async selector => {
const point = await read(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});if(!e)throw Error('Missing '+${JSON.stringify(selector)});const r=e.getBoundingClientRect();return{x:r.x+r.width/2,y:r.y+r.height/2}})()`)
win.webContents.sendInputEvent({ type: 'mouseDown', x: point.x, y: point.y, button: 'left', clickCount: 1 })
win.webContents.sendInputEvent({ type: 'mouseUp', x: point.x, y: point.y, button: 'left', clickCount: 1 })
await pause(120)
}
const clickText = async (selector, label) => {
const match = await read(`(()=>{const e=[...document.querySelectorAll(${JSON.stringify(selector)})].find(e=>e.textContent.trim()===${JSON.stringify(label)});if(!e)throw Error('Missing text '+${JSON.stringify(label)});e.dataset.qaTarget='true';return '[data-qa-target="true"]'})()`)
await click(match)
await read(`document.querySelector(${JSON.stringify(match)})?.removeAttribute('data-qa-target')`)
}
const capture = async name => {
win.webContents.invalidate()
// A hidden window keeps the previous frame, so warm the paint first, give
// the compositor a frame, and keep the second capture.
await win.webContents.capturePage()
await pause(220)
const image = await win.webContents.capturePage()
fs.writeFileSync(path.join(output, name + '.png'), image.toPNG())
return !image.isEmpty()
}
try {
await win.loadFile(path.join(output, 'index.html'))
// An inactive window still composites real frames; a hidden one does not.
win.showInactive()
await wait('!!document.querySelector(".ycode-connectors")')
check('the maintenance placeholder is gone', await read('!!document.querySelector("[data-capability-maintenance]")'), false)
check('the page renders the real connector list', await read('document.querySelectorAll(".connector-card").length'), 3)
check('the connector card names come from the host', await read('[...document.querySelectorAll(".connector-card-name")].map(e=>e.textContent)'), ['Ready Server', 'Unapproved Server', 'Failing Server'])
check('the toolbar keeps the shared search and the custom connector action', await read('[!!document.querySelector(".connectors-toolbar .ec-search-wrapper"), !!document.querySelector(".connector-topbar-action-btn")]'), [true, true])
await capture('connector-list')
check('the detail overlay opens for a selected connector', await (async () => {
await click('.connector-card')
await wait('!!document.querySelector(".connector-detail-modal")')
return read('document.querySelector(".connector-detail-name").textContent')
})(), 'Ready Server')
check('the detail overlay lists the real tool count', await read('document.querySelector(".connector-detail-desc").textContent'), '已连接 · 2 个工具')
await capture('connector-detail')
await click('.connector-detail-close')
check('the detail overlay closes', await read('!!document.querySelector(".connector-detail-modal")'), false)
await click('.connector-topbar-action-btn')
await wait('!!document.querySelector(".mcp-modal")')
check('the management modal opens with the real server list', await read('document.querySelectorAll(".mcp-item").length'), 3)
check('the panel reports the enabled count from the host', await read('document.querySelector(".mcp-panel-status-summary").textContent'), '2 启用 · 1 失败')
check('an unapproved server shows the approval notice', await read('!!document.querySelector(".mcp-item-approval")'), true)
check('an unapproved server cannot be enabled by the toggle', await read(`(()=>{const item=[...document.querySelectorAll('.mcp-item')].find(e=>e.textContent.includes('Unapproved Server'));return item.querySelector('.mcp-toggle').disabled})()`), true)
await capture('mcp-panel')
await clickText('.mcp-item-approval-btn', '批准配置')
await wait(`!document.querySelector('.mcp-item-approval')`)
check('approving the configuration reaches the host', calls.some(call => call[0] === 'trust' && call[1] === 'untrusted'), true)
check('approving also enables the approved server', await read(`(()=>{const item=[...document.querySelectorAll('.mcp-item')].find(e=>e.textContent.includes('Unapproved Server'));return item.querySelector('.mcp-toggle').getAttribute('aria-checked')})()`), 'true')
await click('.mcp-item-header')
await wait('!!document.querySelector(".mcp-item-tools")')
check('the expanded server lists its real tools', await read('[...document.querySelectorAll(".mcp-item-tool")].map(e=>e.textContent)'), ['echo', 'search'])
await clickText('.mcp-item-tool', 'search')
await wait(`!document.querySelector('.mcp-item-tool')?.classList.contains('mcp-item-tool--disabled')`)
check('toggling a tool reaches the host', calls.some(call => call[0] === 'setToolEnabled' && call[1] === 'ready' && call[2] === 'search'), true)
check('a failed server shows the real host error', await read('!!document.querySelector(".mcp-item-error")'), true)
await click('.mcp-item-error-link')
await wait(`!document.querySelector('.mcp-item-error')`)
check('the retry action reconnects the failed server', calls.some(call => call[0] === 'connect' && call[1] === 'failing'), true)
await clickText('.mcp-item-action', '编辑')
await wait('!!document.querySelector(".mcp-config-editor")')
check('the config editor opens with the real server document', await read(`(()=>{const ta=document.querySelector('.mcp-config-textarea');const editor=document.querySelector('.monaco-editor');return ta?ta.value:(document.querySelector('.mcp-config-editor-monaco-wrapper')?editor?'monaco':'loading':'')})()`),
value => value === 'loading' || value === 'monaco' || JSON.parse(value).mcpServers.ReadyServer)
await capture('mcp-editor')
await clickText('.mcp-config-editor-header-actions button', '取消')
await wait('!document.querySelector(".mcp-config-editor")')
await clickText('.mcp-modal-header-right button', '添加 MCP')
await wait('!!document.querySelector(".mcp-config-editor")')
check('the add editor starts from an empty stdio document', await read(`(()=>{const ta=document.querySelector('.mcp-config-textarea');if(ta)return JSON.parse(ta.value).mcpServers;const w=document.querySelector('.mcp-config-editor-monaco-wrapper');return w?'loading-or-monaco':''})()`),
value => value === 'loading-or-monaco' || !!value['my-mcp'])
await clickText('.mcp-config-editor-header-actions button', '取消')
await wait('!document.querySelector(".mcp-config-editor")')
// The card action opens the modal with exactly that server expanded.
await click('.mcp-modal-close')
await wait('!document.querySelector(".mcp-modal")')
await read(`(()=>{const card=[...document.querySelectorAll('.connector-card')].find(e=>e.textContent.includes('Failing Server'));card.querySelector('.connector-connect-btn').click();return true})()`)
await wait('!!document.querySelector(".mcp-modal")')
await wait(`!![...document.querySelectorAll('.mcp-item')].find(e=>e.textContent.includes('Failing Server'))?.querySelector('.mcp-item-action[aria-label="删除 Failing Server"]')`)
await read(`(()=>{const item=[...document.querySelectorAll('.mcp-item')].find(e=>e.textContent.includes('Failing Server'));item.querySelector('.mcp-item-action[aria-label="删除 Failing Server"]').click();return true})()`)
await wait('!!document.querySelector(".yc-mcp-confirm")')
check('the delete confirmation names the real server', await read('document.querySelector(".yc-mcp-confirm").textContent.includes("Failing Server")'), true)
await clickText('.yc-mcp-confirm button', '取消')
await wait('!document.querySelector(".yc-mcp-confirm")')
check('cancelling the delete keeps the server', await read('document.querySelectorAll(".mcp-item").length'), 3)
const setSearch = async value => {
await read(`(()=>{const e=document.querySelector('.cb-search-input__input');if(!e)throw Error('missing MCP search input');
const setter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set;setter.call(e,${JSON.stringify(value)});
e.dispatchEvent(new Event('input',{bubbles:true}));return true})()`)
await pause(200)
}
await setSearch('fail')
await pause(200)
check('the management search filters the real list', await read('document.querySelectorAll(".mcp-item").length'), 1)
await setSearch('')
await click('.mcp-modal-close')
await wait('!document.querySelector(".mcp-modal")')
check('closing the management modal returns to the card list', await read('document.querySelectorAll(".connector-card").length'), 3)
// A rejected host call must stay visible inside the management modal.
await read(`window.ycode.mcp.qaFailNextEnable()`)
await read(`(()=>{document.querySelector('.connector-topbar-action-btn').click();return true})()`)
await wait('!!document.querySelector(".mcp-modal")')
await wait(`!![...document.querySelectorAll('.mcp-item')].find(e=>e.textContent.includes('Ready Server'))`)
await read(`(()=>{const item=[...document.querySelectorAll('.mcp-item')].find(e=>e.textContent.includes('Ready Server'));item.querySelector('.mcp-toggle').click();return true})()`)
await wait('!!document.querySelector(".mcp-modal [role=alert]")')
check('a rejected action reports the real host error inside the modal',
await read('document.querySelector(".mcp-modal [role=alert]").textContent'),
value => typeof value === 'string' && value.includes('MCP 服务已禁用'))
check('the error banner is not only behind the modal overlay',
await read(`(()=>{const el=document.querySelector('.mcp-modal [role=alert]');const r=el.getBoundingClientRect();return r.width>0&&r.height>0})()`), true)
check('the error banner is painted inside the visible modal body',
await read(`(()=>{const el=document.querySelector('.mcp-modal [role=alert]');const r=el.getBoundingClientRect();
const body=document.querySelector('.mcp-modal-body').getBoundingClientRect();
const s=getComputedStyle(el);
return {text:el.textContent,top:Math.round(r.top),bottom:Math.round(r.bottom),bodyTop:Math.round(body.top),
bodyBottom:Math.round(body.bottom),color:s.color,background:s.backgroundColor,visible:s.visibility,opacity:s.opacity}})()`),
value => value.text.includes('MCP 服务已禁用') && value.top >= value.bodyTop && value.bottom <= value.bodyBottom + 1 &&
value.color !== 'rgba(0, 0, 0, 0)' && value.background !== 'rgba(0, 0, 0, 0)' && value.visible === 'visible' && value.opacity === '1')
await capture('mcp-error')
check('no renderer errors', errors, [])
fs.writeFileSync(path.join(output, 'report.json'), JSON.stringify({
result: 'PASS', checks,
calls,
limits: 'Controlled MCP host in isolated Electron; the shipped page, helpers, icons and styles are used. Not WorkBuddy pixel acceptance and not a real MCP server connection.'
}, null, 2))
console.log(JSON.stringify({ result: 'PASS', checks: checks.length, output }))
win.destroy(); app.exit(0)
} catch (error) {
await capture('failure')
fs.writeFileSync(path.join(output, 'failure.json'), JSON.stringify({ error: String(error), checks, errors, calls }, null, 2))
console.error(error)
win.destroy(); app.exit(1)
}
})().catch(error => { console.error(error); electron.app.exit(1) })