feat(settings): replicate the WorkBuddy agents panel and persist team-management preference (TODO 15)
This commit is contained in:
1 parent
b3be45715e
commit
143516b3a8
33 files changed
+1071
-21
No files matched your search
@@ -0,0 +1,243 @@
|
||||
// TODO 15 验收:设置 → 智能体的 1:1 布局 + 团队管理偏好真实持久化 + 未接入项标注 + 深浅主题。
|
||||
// 真实隔离 YCode(真实 preload/IPC/渲染进程/文件系统)+ 原生设置窗口,视口 900×680(与原版一致)。
|
||||
//
|
||||
// 原版依据(REA ev_bd0f4b…/ev_3384aa…/ev_01940b…/ev_1cc896…):
|
||||
// 页面根 [232,40,638,503];团队管理卡 638×90(section 606×66、下拉 120×32 @734,69);
|
||||
// 经济模式卡 638×90(开关 34×20 @820,181);浏览器操控标签 638×22 + Beta 39.391×20 @314,253;
|
||||
// 浏览器卡片 638×261(主 section 606×44、安装 68×32 @786,294、列表 606×193、行 68/68/56、按钮 102×32 @752)。
|
||||
const fs = require('node:fs/promises')
|
||||
const path = require('node:path')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
if (!process.versions.electron) {
|
||||
;(async () => {
|
||||
const directory = await fs.mkdtemp(path.resolve('.tmp/agents-settings-'))
|
||||
const data = path.join(directory, 'data'), home = path.join(directory, 'home')
|
||||
await fs.mkdir(data, { recursive: true })
|
||||
await fs.mkdir(home, { recursive: true })
|
||||
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data, YCODE_OMP_HOME: home,
|
||||
YCODE_DISABLE_GPU: '1', YCODE_DISABLE_SYSTEM_INTEGRATION: '1' }
|
||||
delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL
|
||||
const child = require('node:child_process').spawn(require('electron'),
|
||||
['--no-sandbox', '--disable-gpu-sandbox', __filename], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
let output = ''
|
||||
child.stdout.on('data', bytes => { output += bytes; if (bytes.toString().includes('AGENTS_SETTINGS_')) process.stdout.write(bytes) })
|
||||
child.stderr.on('data', bytes => { output += bytes })
|
||||
const timer = setTimeout(() => child.kill(), 300000)
|
||||
child.once('exit', async code => {
|
||||
clearTimeout(timer)
|
||||
await fs.writeFile(path.join(directory, 'process.log'), output)
|
||||
console.log(JSON.stringify({ directory, exitCode: code }))
|
||||
if (code) console.error(output.slice(-4000))
|
||||
process.exitCode = code ? 1 : 0
|
||||
})
|
||||
})().catch(error => { console.error(error); process.exitCode = 1 })
|
||||
} else {
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
const { pathToFileURL } = require('node:url')
|
||||
const directory = process.env.YCODE_QA_DIRECTORY
|
||||
app.setPath('home', path.join(directory, 'home'))
|
||||
const checks = [], snapshots = {}, rendererErrors = []
|
||||
let main, settings, phase = 'boot'
|
||||
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
async function read(source, win = main) {
|
||||
let timer
|
||||
try {
|
||||
return await Promise.race([win.webContents.executeJavaScript(source), new Promise((_, reject) => {
|
||||
timer = setTimeout(() => reject(Error('Renderer timeout: ' + source.slice(0, 120))), 15000)
|
||||
})])
|
||||
} finally { clearTimeout(timer) }
|
||||
}
|
||||
async function wait(source, label, win = main) {
|
||||
for (let index = 0; index < 300; index++) { const value = await read(source, win); if (value) return value; await pause(40) }
|
||||
throw Error('Timeout: ' + label)
|
||||
}
|
||||
async function step(label) { phase = label; await fs.writeFile(path.join(directory, 'phase.txt'), label) }
|
||||
function check(label, actual, expected) {
|
||||
const pass = typeof expected === 'function' ? expected(actual) : JSON.stringify(actual) === JSON.stringify(expected)
|
||||
checks.push({ label, pass, actual, expected: typeof expected === 'function' ? expected.toString() : expected })
|
||||
}
|
||||
const near = (value, target, tolerance = 1) => Math.abs(value - target) <= tolerance
|
||||
function observe(win) {
|
||||
win.webContents.on('console-message', event => { if (event.level === 'error' || event.level >= 3) rendererErrors.push({ windowId: win.id, message: event.message }) })
|
||||
}
|
||||
async function openSettings(section) {
|
||||
await read(`window.ycode.app.openSettings(${JSON.stringify(section)})`)
|
||||
for (let index = 0; index < 200; index++) {
|
||||
settings = BrowserWindow.getAllWindows().find(win => win !== main)
|
||||
if (settings && !settings.webContents.isLoading()) break
|
||||
await pause(25)
|
||||
}
|
||||
assert.ok(settings, 'settings window')
|
||||
await wait('!!document.querySelector(".settings-window-root")', 'settings root', settings)
|
||||
await pause(400)
|
||||
return settings
|
||||
}
|
||||
|
||||
const layoutSource = `(()=>{
|
||||
const round = value => Math.round(value * 1000) / 1000
|
||||
const rect = node => { if (!node) return null; const r = node.getBoundingClientRect()
|
||||
return { x: round(r.x), y: round(r.y), width: round(r.width), height: round(r.height) } }
|
||||
const panel = document.querySelector('.extensions-settings-panel')
|
||||
const cards = [...document.querySelectorAll('.extensions-settings-panel > .general-settings-panel__group-card')]
|
||||
const sections = [...document.querySelectorAll('.extensions-settings-panel .general-settings-panel__group-card > .general-settings-panel__section')]
|
||||
const buttons = [...document.querySelectorAll('.extensions-settings-panel__action-button')]
|
||||
const rows = [...document.querySelectorAll('.extensions-settings-panel__browser-row')]
|
||||
const select = document.querySelector('.extensions-settings-panel__team-dropdown .wb-select')
|
||||
const switchInput = document.querySelector('#agents-economy-switch, .extensions-settings-panel .wb-switch__input')
|
||||
return {
|
||||
panel: rect(panel),
|
||||
cards: cards.map(card => rect(card)),
|
||||
sections: sections.map(section => ({
|
||||
rect: rect(section),
|
||||
title: section.querySelector('.general-settings-panel__section-title')?.textContent?.trim(),
|
||||
desc: section.querySelector('.general-settings-panel__section-desc')?.textContent?.trim()
|
||||
})),
|
||||
select: select ? { rect: rect(select), value: select.querySelector('.wb-select__value')?.textContent?.trim(), title: select.getAttribute('title') } : null,
|
||||
switch: switchInput ? (() => { const track = switchInput.closest('.wb-switch')?.querySelector('.wb-switch__track')
|
||||
return { rect: rect(track), trackBackground: track ? getComputedStyle(track).backgroundColor : null,
|
||||
trackOpacity: track ? getComputedStyle(track).opacity : null, thumb: rect(track?.querySelector('.wb-switch__thumb')),
|
||||
checked: switchInput.checked, disabled: switchInput.disabled,
|
||||
unavailable: switchInput.closest('.extensions-settings-panel__unavailable-control')?.getAttribute('title') } })() : null,
|
||||
browserLabel: (() => { const node = document.querySelector('.extensions-settings-panel__browser-label'); return node ? { ...rect(node), text: node.firstChild?.textContent?.trim() } : null })(),
|
||||
beta: (() => { const node = document.querySelector('.extensions-settings-panel__beta'); return node ? { ...rect(node), text: node.textContent.trim() } : null })(),
|
||||
browserCard: rect(document.querySelector('.extensions-settings-panel__browser-card')),
|
||||
browserList: rect(document.querySelector('.extensions-settings-panel__browser-list')),
|
||||
rows: rows.map(row => ({
|
||||
rect: rect(row),
|
||||
title: row.querySelector('.extensions-settings-panel__browser-title')?.textContent?.trim(),
|
||||
desc: row.querySelector('.extensions-settings-panel__browser-desc')?.textContent?.trim(),
|
||||
icon: (() => { const node = row.querySelector('.extensions-settings-panel__browser-icon')
|
||||
return node ? { ...rect(node), tagName: node.tagName, src: node.getAttribute('src'), placeholderTitle: node.getAttribute('title'),
|
||||
complete: node.complete === true, naturalWidth: node.naturalWidth, naturalHeight: node.naturalHeight } : null })(),
|
||||
button: (() => { const node = row.querySelector('.extensions-settings-panel__action-button')
|
||||
return node ? { rect: rect(node), text: node.textContent.trim(), ariaDisabled: node.getAttribute('aria-disabled'), title: node.getAttribute('title'), arrow: !!node.querySelector('.wb-button__icon svg') } : null })()
|
||||
})),
|
||||
buttons: buttons.map(button => ({ text: button.textContent.trim(), rect: rect(button), ariaDisabled: button.getAttribute('aria-disabled'), title: button.getAttribute('title') })),
|
||||
anchors: [...document.querySelectorAll('a')].map(anchor => anchor.getAttribute('href')).filter(Boolean),
|
||||
cardBackground: cards[0] ? getComputedStyle(cards[0]).backgroundColor : null,
|
||||
titleColor: (() => { const node = document.querySelector('.extensions-settings-panel .general-settings-panel__section-title'); return node ? getComputedStyle(node).color : null })(),
|
||||
descColor: (() => { const node = document.querySelector('.extensions-settings-panel .general-settings-panel__section-desc'); return node ? getComputedStyle(node).color : null })(),
|
||||
selectBackground: select ? getComputedStyle(select).backgroundColor : null,
|
||||
selectBorder: select ? getComputedStyle(select).borderTopColor : null,
|
||||
buttonBackground: buttons[0] ? getComputedStyle(buttons[0]).backgroundColor : null,
|
||||
buttonBorder: buttons[0] ? getComputedStyle(buttons[0]).borderTopColor : null,
|
||||
betaBackground: (() => { const node = document.querySelector('.extensions-settings-panel__beta'); return node ? getComputedStyle(node).backgroundColor : null })(),
|
||||
theme: document.documentElement.dataset.theme ?? 'light'
|
||||
}})()`
|
||||
|
||||
;(async () => {
|
||||
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href); await app.whenReady()
|
||||
for (let index = 0; index < 200; index++) {
|
||||
main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode')
|
||||
if (main && !main.webContents.isLoading()) break
|
||||
await pause(25)
|
||||
}
|
||||
assert.ok(main, 'main window'); observe(main)
|
||||
await wait('!!window.ycode', 'preload api')
|
||||
|
||||
await step('agents-settings')
|
||||
await openSettings('extensions'); observe(settings)
|
||||
await wait('!!document.querySelector(".extensions-settings-panel")', 'agents panel', settings)
|
||||
const nav = await read(`(()=>{const item=document.querySelector('[data-settings-tab="extensions"]')
|
||||
return { exists: !!item, disabled: item?.getAttribute('aria-disabled'), active: document.querySelector('.settings-navigation__item--active')?.dataset.settingsTab }})()`, settings)
|
||||
check('智能体 nav entry is enabled and active', [nav.exists, nav.disabled !== 'true', nav.active], [true, true, 'extensions'])
|
||||
|
||||
const light = await read(layoutSource, settings)
|
||||
snapshots.light = light
|
||||
await fs.writeFile(path.join(directory, 'ycode-agents-light.png'), (await settings.webContents.capturePage()).toPNG())
|
||||
check('agents panel keeps the recovered 638x503 box at 232,40',
|
||||
[near(light.panel.x, 232), near(light.panel.y, 40), near(light.panel.width, 638), near(light.panel.height, 503)], [true, true, true, true])
|
||||
check('team-management and economy cards keep 638x90 with 606x66 sections',
|
||||
[light.cards.slice(0, 2).map(card => [near(card.width, 638), near(card.height, 90)].every(Boolean)),
|
||||
light.sections.slice(0, 2).map(section => [near(section.rect.width, 606), near(section.rect.height, 66)].every(Boolean)),
|
||||
near(light.cards[0].y, 40), near(light.cards[1].y, 146)], [[true, true], [true, true], true, true])
|
||||
check('team-management keeps the recovered title, description and 120x32 select',
|
||||
[light.sections[0].title, light.sections[0].desc,
|
||||
near(light.select.rect.x, 734), near(light.select.rect.y, 69), near(light.select.rect.width, 120), near(light.select.rect.height, 32),
|
||||
light.select.value, light.select.title],
|
||||
['智能体团队管理', '开启后,复杂任务会自动拆分多个智能体协作完成。专家团队任务不受此设置影响。',
|
||||
true, true, true, true, '开启', '仅保存偏好;多智能体协作执行尚未接入'])
|
||||
check('economy mode keeps the recovered copy, 34x20 switch and disabled annotation',
|
||||
[light.sections[1].title, light.sections[1].desc,
|
||||
near(light.switch.rect.x, 820), near(light.switch.rect.y, 181), near(light.switch.rect.width, 34), near(light.switch.rect.height, 20),
|
||||
light.switch.trackBackground, light.switch.checked, light.switch.disabled, light.switch.unavailable],
|
||||
['经济模式', '自动在合适时机触发上下文压缩,减少长对话中重复传输的上下文,降低积分消耗',
|
||||
true, true, true, true, 'rgb(0, 194, 154)', true, true, '暂未接入'])
|
||||
check('browser group keeps the recovered label 638x22 and Beta badge',
|
||||
[near(light.browserLabel.x, 232), near(light.browserLabel.y, 252), near(light.browserLabel.width, 638), near(light.browserLabel.height, 22),
|
||||
light.browserLabel.text, near(light.beta.x, 314), near(light.beta.y, 253), near(light.beta.width, 39.391), near(light.beta.height, 20), light.beta.text],
|
||||
[true, true, true, true, '浏览器操控', true, true, true, true, 'Beta'])
|
||||
check('browser card keeps 638x261 with 606x44 main section and 68x32 install button',
|
||||
[near(light.browserCard.y, 282), near(light.browserCard.width, 638), near(light.browserCard.height, 261),
|
||||
near(light.sections[2].rect.y, 294), near(light.sections[2].rect.width, 606), near(light.sections[2].rect.height, 44),
|
||||
light.sections[2].title, near(light.buttons[0].rect.x, 786), near(light.buttons[0].rect.width, 68), near(light.buttons[0].rect.height, 32),
|
||||
light.buttons[0].ariaDisabled, light.buttons[0].title],
|
||||
[true, true, true, true, true, true, '让 YCode 控制浏览器', true, true, true, 'true', '尚未配置'])
|
||||
check('browser list keeps the recovered 606x193 with 68/68/56 rows',
|
||||
[near(light.browserList.y, 338), near(light.browserList.width, 606), near(light.browserList.height, 193),
|
||||
light.rows.map(row => near(row.rect.height, row.title.includes('QQ') ? 56 : 68)),
|
||||
light.rows.every(row => row.icon && row.icon.tagName === 'IMG' && near(row.icon.width, 36) && near(row.icon.height, 36)
|
||||
&& row.icon.complete && row.icon.naturalWidth > 0 && row.icon.naturalHeight > 0 && row.icon.src && row.icon.placeholderTitle === null)],
|
||||
[true, true, true, [true, true, true], true])
|
||||
check('browser rows keep the recovered titles, descriptions and disabled 102x32 actions',
|
||||
[light.rows.map(row => row.title),
|
||||
light.rows.every(row => row.button && near(row.button.rect.width, 102) && near(row.button.rect.height, 32) && row.button.text === '前往安装' && row.button.ariaDisabled === 'true' && row.button.title === '尚未配置' && row.button.arrow)],
|
||||
[['YCode Chrome 扩展程序', 'YCode Edge 扩展程序', 'YCode QQ 浏览器扩展程序'], true])
|
||||
check('agents page contains no anchor links', light.anchors, [])
|
||||
|
||||
await step('agents-team-toggle')
|
||||
await read(`document.querySelector('.extensions-settings-panel__team-dropdown .wb-select').click()`, settings)
|
||||
const popover = await wait(`(()=>{const node=document.querySelector('.extensions-settings-panel__team-popover')
|
||||
return node?{options:[...node.querySelectorAll('[role=option]')].map(option=>option.textContent.trim())}:null})()`, 'team popover', settings)
|
||||
check('team-management dropdown offers exactly 开启 / 禁用', popover.options.map(text => text.replace(/\s+/g, '')), ['开启', '禁用'])
|
||||
await read(`[...document.querySelectorAll('.extensions-settings-panel__team-popover [role=option]')].find(option=>option.textContent.trim()==='禁用').click()`, settings)
|
||||
await wait(`document.querySelector('.extensions-settings-panel__team-dropdown .wb-select__value')?.textContent?.trim()==='禁用'`, 'team management disabled', settings)
|
||||
const stored = await read(`window.ycode.agents.get().then(view => view.teamManagement)`, settings)
|
||||
const config = JSON.parse(await fs.readFile(path.join(directory, 'data', 'config.json'), 'utf8'))
|
||||
snapshots.config = config.agentSettings
|
||||
check('selecting 禁用 persists the team-management preference', [stored, config.agentSettings?.teamManagement], [false, false])
|
||||
|
||||
await step('agents-persistence')
|
||||
settings.close()
|
||||
await pause(500)
|
||||
await openSettings('extensions')
|
||||
const reopened = await read(layoutSource, settings)
|
||||
check('reopened settings restores the 禁用 preference', reopened.select.value, '禁用')
|
||||
|
||||
await step('agents-dark')
|
||||
await openSettings('appearance')
|
||||
await wait('!!document.querySelector(".appearance-card")', 'appearance panel', settings)
|
||||
await read(`document.querySelector('[aria-label="深色"].appearance-card').click()`, settings)
|
||||
await wait(`document.documentElement.dataset.theme==='dark'`, 'dark theme', settings)
|
||||
await openSettings('extensions')
|
||||
await wait(`document.documentElement.dataset.theme==='dark'&&!!document.querySelector('.extensions-settings-panel')`, 'dark agents panel', settings)
|
||||
const dark = await read(layoutSource, settings)
|
||||
snapshots.dark = dark
|
||||
await fs.writeFile(path.join(directory, 'ycode-agents-dark.png'), (await settings.webContents.capturePage()).toPNG())
|
||||
check('dark theme keeps the recovered card, text, select and button surfaces',
|
||||
[dark.cardBackground, dark.titleColor, dark.descColor, dark.selectBackground, dark.selectBorder, dark.buttonBackground, dark.buttonBorder, dark.betaBackground],
|
||||
['rgb(31, 31, 31)', 'rgb(242, 243, 245)', 'rgba(255, 255, 255, 0.5)', 'rgb(31, 31, 31)', 'rgb(61, 61, 61)', 'rgb(38, 38, 38)', 'rgb(61, 61, 61)', 'rgb(31, 31, 31)'])
|
||||
check('dark theme keeps every recovered geometry',
|
||||
[near(dark.panel.width, 638), near(dark.panel.height, 503), near(dark.cards[0].height, 90),
|
||||
near(dark.browserCard.height, 261), near(dark.browserList.height, 193)], [true, true, true, true, true])
|
||||
check('dark economy switch keeps the recovered enabled brand track', dark.switch.trackBackground, 'rgb(34, 203, 168)')
|
||||
await openSettings('appearance')
|
||||
await wait('!!document.querySelector(".appearance-card")', 'appearance panel again', settings)
|
||||
await read(`document.querySelector('[aria-label="浅色"].appearance-card').click()`, settings)
|
||||
await wait(`document.documentElement.dataset.theme==='light'`, 'light restored', settings)
|
||||
|
||||
await step('agents-errors')
|
||||
check('no renderer error logs', rendererErrors, [])
|
||||
|
||||
const result = checks.every(item => item.pass) ? 'PASS' : 'FAIL'
|
||||
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result, phase, checks, snapshots }, null, 2))
|
||||
console.log('AGENTS_SETTINGS_' + result + ' ' + directory)
|
||||
for (const failure of checks.filter(item => !item.pass)) console.log('AGENTS_SETTINGS_FAILURE ' + JSON.stringify(failure))
|
||||
app.exit(result === 'PASS' ? 0 : 1)
|
||||
})().catch(async error => {
|
||||
console.error(error)
|
||||
try { await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'ERROR', phase, checks, snapshots, error: String(error?.stack ?? error) }, null, 2)) } catch { /* ignore */ }
|
||||
app.exit(1)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user