feat(settings): drop the assistant/device/data/app entries (TODO 16, 17)

This commit is contained in:
xiaohan.yuan committed 2026-10-17 08:18:54 +08:00
1 parent bde041c12d
commit 401926d7eb
3 files changed
+228 -12

No files matched your search

+185
View File
@@ -0,0 +1,185 @@
// TODO 16/17 验收:移除设置里的「助理 / 设备连接 / 数据管理 / 应用管理」入口,并保证旧导航目标不落空白页。
// 真实隔离 YCode(真实 preload/IPC/渲染进程)+ 原生设置窗口。
//
// 原版依据:REA `ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e`
// (WorkBuddy 5.8.3 设置窗口,viewport 记录于 .tmp/rea-assessment-settings-window/layout.json):
// 设置导航共 16 项、分 4 组(general 5 / feature 6 / dataSecurity 3 / about 2),
// 条目几何 190×32、x=12、y=111 起、组间距 76。
// 用户要求(TODO 16/17)在其中移除:助理(claw)、设备连接(deviceConnections)、数据管理(dataManagement)、
// 应用管理(buddyApps);其余条目保持原样(未接入的仍为禁用 + 「暂未接入」)。
const fs = require('node:fs/promises')
const path = require('node:path')
const assert = require('node:assert/strict')
const EXPECTED_TABS = ['settings', 'account', 'subscriptionUsage', 'appearance', 'keyboardShortcuts', 'personalization',
'memory', 'extensions', 'models', 'securityCenter', 'helpFeedback', 'getHelp']
const REMOVED_TABS = ['claw', 'deviceConnections', 'dataManagement', 'buddyApps']
if (!process.versions.electron) {
;(async () => {
const directory = await fs.mkdtemp(path.resolve('.tmp/settings-nav-'))
const data = path.join(directory, 'data')
const home = path.join(directory, 'home')
await fs.mkdir(data, { recursive: true })
await fs.mkdir(home, { recursive: true })
const now = Date.now()
await fs.writeFile(path.join(data, 'sessions.json'), JSON.stringify({ sessions: [{ id: 'settings-nav-session', title: 'Settings Nav',
cwd: home, createdAt: now, updatedAt: now + 1, messages: [{ id: 'settings-nav-user', role: 'user', content: '设置导航验收', createdAt: now, status: 'done' }] }] }))
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data, YCODE_OMP_HOME: home,
YCODE_DISABLE_GPU: '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('SETTINGS_NAV_')) process.stdout.write(bytes) })
child.stderr.on('data', bytes => { output += bytes })
const timer = setTimeout(() => child.kill(), 240000)
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 })
}
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(300)
return settings
}
const navSource = `(()=>{const items=[...document.querySelectorAll('.settings-navigation__item')];
const nav=document.querySelector('.settings-modal__nav');
return {tabs:items.map(node=>node.dataset.settingsTab),labels:items.map(node=>node.textContent.trim()),
disabled:items.map(node=>node.getAttribute('aria-disabled')),titles:items.map(node=>node.getAttribute('title')),
groups:[...document.querySelectorAll('.settings-navigation__group-title')].map(node=>node.textContent.trim()),
dividers:document.querySelectorAll('.settings-navigation__group-divider').length,
nav:[nav?Math.round(nav.getBoundingClientRect().width):0,nav?nav.clientWidth:0,nav?nav.scrollHeight-nav.clientHeight:0],
geometry:items.map(node=>{const r=node.getBoundingClientRect();return [Math.round(r.x),Math.round(r.width),Math.round(r.height)]})}})()`
;(async () => {
// 导入前不做任何 await,避免 Electron 先进入 ready(主进程 bundle 顶层会调用 disableHardwareAcceleration)。
const sync = require('node:fs')
sync.mkdirSync(process.env.YCODE_USER_DATA_DIR, { recursive: true })
app.setPath('userData', process.env.YCODE_USER_DATA_DIR)
app.setPath('sessionData', process.env.YCODE_USER_DATA_DIR)
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(40)
}
assert.ok(main, 'main window')
main.webContents.on('console-message', (_event, level, message) => { if (level >= 2) rendererErrors.push(message) })
await wait('!!window.ycode', 'main renderer')
await pause(1200)
await step('settings-nav-entries')
await openSettings('model')
settings.webContents.on('console-message', (_event, level, message) => { if (level >= 2) rendererErrors.push(message) })
const nav = await read(navSource, settings)
snapshots.nav = nav
check('settings nav keeps the 12 remaining entries in order', nav.tabs, EXPECTED_TABS)
check('removed entries are gone from the DOM', REMOVED_TABS.map(tab => nav.tabs.includes(tab)), [false, false, false, false])
check('removed entries are gone from labels and search metadata', [nav.labels.join('|').includes('助理'), nav.labels.join('|').includes('设备连接'),
nav.labels.join('|').includes('数据管理'), nav.labels.join('|').includes('应用管理')], [false, false, false, false])
// 原版 16 项在 680 高窗口溢出,thin 滚动条占 10px,故条目实测 190 = 200 - 10;
// 移除 4 项后本窗口不再溢出,条目宽度应等于导航栏内容宽(原版 CSS 无 scrollbar-gutter)。
const navContentWidth = nav.nav[1]
check('nav keeps the recovered 32px entry geometry at x=12 and fills the nav content width',
[nav.geometry.every(([x, width, height]) => x === 12 && height === 32), nav.geometry.every(([, width]) => width === navContentWidth), nav.nav[2] === 0], [true, true, true])
check('nav width reflects the recovered 200px column with a thin scrollbar only when overflowing',
[nav.nav[0] === 200 || nav.nav[0] === 190, nav.nav[2] === 0 ? navContentWidth === 200 : navContentWidth === 190], [true, true])
// 原版 4 组;about 组按组件约定渲染为分隔线而不是标题。
check('nav keeps the four recovered groups', [nav.groups.length, nav.dividers], [3, 1])
const expectedDisabled = EXPECTED_TABS.filter(tab => tab !== 'models' && tab !== 'appearance')
check('unwired entries stay disabled', nav.tabs.filter((tab, index) => nav.disabled[index] === 'true'), expectedDisabled)
check('unwired entries keep the 暂未接入 hint', nav.titles.filter(title => (title ?? '').includes('暂未接入')).length, expectedDisabled.length)
check('models and appearance stay enabled', nav.tabs.filter((tab, index) => !nav.disabled[index]), ['appearance', 'models'])
await step('settings-search-excludes-removed')
await read(`(()=>{const input=document.querySelector('.settings-navigation input[type="search"]');
if(!input)throw new Error('missing search');Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(input,'助理');
input.dispatchEvent(new Event('input',{bubbles:true}));return true})()`, settings)
await pause(240)
snapshots.search = await read(`(()=>{const results=[...document.querySelectorAll('.settings-navigation__item')];
return {tabs:results.map(node=>node.dataset.settingsTab),empty:!!document.querySelector('.source-settings-search-empty')}})()`, settings)
check('searching the removed 助理 entry finds nothing', [snapshots.search.tabs, snapshots.search.empty], [[], true])
await read(`(()=>{const input=document.querySelector('.settings-navigation input[type="search"]');
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(input,'');input.dispatchEvent(new Event('input',{bubbles:true}));return true})()`, settings)
await pause(200)
await step('settings-navigation-still-works')
await read(`(()=>{const node=document.querySelector('[data-settings-tab="appearance"]');node.click();return true})()`, settings)
await wait(`!!document.querySelector('.segmented')`, 'appearance panel', settings)
check('appearance entry still opens its panel', await read(`!!document.querySelector('.segmented')&&!document.querySelector('.models-settings-panel')`, settings), true)
await read(`(()=>{const node=document.querySelector('[data-settings-tab="models"]');node.click();return true})()`, settings)
await wait(`!!document.querySelector('.models-settings-panel')`, 'models panel', settings)
check('models entry still opens its panel', await read(`!!document.querySelector('.models-settings-panel')`, settings), true)
await step('stale-navigation-falls-back')
await read(`(()=>{const node=document.querySelector('[data-settings-tab="securityCenter"]');node.click();return true})()`, settings)
await pause(200)
snapshots.unwiredClick = await read(`({alert:(document.querySelector('.settings-modal__panel [role="alert"]')||{}).textContent||'',models:!!document.querySelector('.models-settings-panel')})`, settings)
check('unwired entry reports 该设置尚未接入 instead of a blank page', snapshots.unwiredClick.alert.includes('该设置尚未接入'), true)
// 旧深链接指向已移除入口:应回退到模型设置。
await read(`window.ycode.app.openSettings('claw')`)
await pause(400)
check('stale navigation to 助理 falls back to the models panel', await read(`!!document.querySelector('.models-settings-panel')`, settings), true)
const previous = settings.id
await read(`document.querySelector('.settings-window-close').click()`, settings)
for (let index = 0; index < 200 && !settings.isDestroyed(); index++) await pause(20)
check('settings window closed before the deep-link relaunch', settings.isDestroyed(), true)
// 新窗口直接以已移除的 section 启动:同样不能是空白页。
await openSettings('dataManagement')
check('fresh window with a removed section lands on the models panel', await read(`!!document.querySelector('.models-settings-panel')`, settings), true)
check('fresh window reuses a new settings window', settings.id !== previous, true)
const finalNav = await read(navSource, settings)
snapshots.finalNav = finalNav
check('nav still excludes removed entries after relaunch', finalNav.tabs, EXPECTED_TABS)
check('no renderer error logs', rendererErrors, [])
const report = { result: checks.every(item => item.pass) ? 'PASS' : 'FAIL',
scope: 'Real isolated YCode App/preload/IPC/renderer + native settings window; navigation entries, geometry, search, disabled hints and stale-target fallback measured live.',
sourceEvidence: ['ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e'], snapshots, checks, rendererErrors }
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
console.log('SETTINGS_NAV_' + report.result + ' ' + directory)
for (const failure of checks.filter(item => !item.pass)) console.log('SETTINGS_NAV_FAILURE ' + JSON.stringify(failure))
app.exit(report.result === 'PASS' ? 0 : 1)
})().catch(async error => {
await fs.writeFile(path.join(directory, 'failure.json'), JSON.stringify({ phase, error: String(error), stack: error.stack, checks, snapshots, rendererErrors }, null, 2))
console.error(error); app.exit(1)
})
}