Recover the WorkBuddy 5.8.3 个性化 page from REA evidence (PersonalizationPanel-D16rRGjy.css + tokens): 45 module rules plus scoped light/dark tokens, the five-card 1160px layout (identity card 638x100 with inline editors, reply preference card 638x204, three 638x264 long-text cards with 598x160 previews) and the 640x520 editor modal. Persist identity, tone, welcome tips, custom instructions, persona and long-term memory in config.json, expose personalization:get/update over IPC, and append the resulting personalization context to each turn's prompt for both the OMP and built-in kernels. Wire 欢迎语 to the streaming loading tips and keep 显示文件修改过程 visibly disabled because YCode has no incremental file-change renderer yet. Verify with scripts/verify-personalization-settings.cjs: 29 assertions covering geometry, tone options, switches, inline identity editing with blur-discard, editor modal save/cancel, config persistence, reopen, dark surfaces and no renderer errors. Add a pure prompt-builder regression and keep every existing settings regression green.
334 lines
26 KiB
JavaScript
334 lines
26 KiB
JavaScript
// TODO 13 验收:设置 → 个性化页的 1:1 结构/几何 + 身份编辑/回复风格/开关/长文本弹窗 + 持久化与主题。
|
||
// 真实隔离 YCode(真实 preload/IPC/渲染进程/文件系统)+ 原生设置窗口,视口 900×680(与原版一致)。
|
||
//
|
||
// 原版依据(REA ev_af920…/ev_88af07…/ev_d6e0a2…/ev_523e07…/ev_4f4f76…):
|
||
// 面板 [232,40,638,1160];身份卡 638×100(两行 [244,48,614,34]、胶囊 172×34);
|
||
// 回复偏好卡 638×204(回复风格 select 120×32 @734,174;开关 34×20 @820,248/316);
|
||
// 指令/人设/长期记忆三张长卡 638×264(y=376/656/936),预览 598×160;编辑弹窗 [130,80,640,520]。
|
||
const fs = require('node:fs/promises')
|
||
const path = require('node:path')
|
||
const assert = require('node:assert/strict')
|
||
|
||
const TONE_LABELS = ['默认', '专业严谨', '亲和友善', '直言不讳', '天马行空', '高效务实', '毒舌吐槽', '启发引导']
|
||
const TONE_HINTS = ['不设定特定风格。', '清晰、准确、值得信赖。', '温暖、平易近人、鼓励支持。', '简明扼要、不废话、直击要点。',
|
||
'富有想象力、善用比喻类比。', '最少文字、最大信息量。', '犀利吐槽、但绝不伤人。', '用提问引导思考、授人以渔。']
|
||
|
||
if (!process.versions.electron) {
|
||
;(async () => {
|
||
const directory = await fs.mkdtemp(path.resolve('.tmp/personalization-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('PERSONALIZATION_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 = 0.75) => 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 setInput = (selector, value) => `(()=>{const input=document.querySelector(${JSON.stringify(selector)})
|
||
const proto=input instanceof HTMLTextAreaElement?HTMLTextAreaElement.prototype:HTMLInputElement.prototype
|
||
Object.getOwnPropertyDescriptor(proto,'value').set.call(input,${JSON.stringify(value)})
|
||
input.dispatchEvent(new Event('input',{bubbles:true}));return true})()`
|
||
|
||
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('.personalization-panel')
|
||
return {
|
||
panel: rect(panel),
|
||
identityCard: rect(document.querySelector('.local-memory-identity-card')),
|
||
rows: [...document.querySelectorAll('.local-memory-identity-row')].map(row => ({
|
||
id: row.id,
|
||
label: row.querySelector('.local-memory-identity-row__label')?.textContent?.trim(),
|
||
rect: rect(row),
|
||
pill: (() => { const node = row.querySelector('.local-memory-pill')
|
||
return node ? { rect: rect(node), text: node.textContent.trim(), placeholder: !!node.querySelector('.local-memory-pill__placeholder') } : null })(),
|
||
input: (() => { const node = row.querySelector('.local-memory-inline__input')
|
||
return node ? { rect: rect(node), value: node.value } : null })(),
|
||
actions: row.querySelectorAll('.local-memory-inline__icon-btn').length,
|
||
confirmDisabled: row.querySelector('.local-memory-inline__icon-btn--confirm')?.disabled ?? null
|
||
})),
|
||
preference: rect(document.querySelector('.general-settings-panel__group-card')),
|
||
sections: [...document.querySelectorAll('.general-settings-panel__group-card > .general-settings-panel__section')].map(section => ({
|
||
id: section.id,
|
||
title: section.querySelector('.general-settings-panel__section-title')?.textContent?.trim(),
|
||
desc: section.querySelector('.general-settings-panel__section-desc')?.textContent?.trim(),
|
||
rect: rect(section),
|
||
select: rect(section.querySelector('.wb-select')),
|
||
switch: rect(section.querySelector('.wb-switch__track')),
|
||
switchChecked: section.querySelector('.wb-switch__input')?.checked ?? null,
|
||
switchDisabled: section.querySelector('.wb-switch__input')?.disabled ?? null,
|
||
unavailable: section.querySelector('[data-settings-unavailable=true]')?.getAttribute('title')
|
||
})),
|
||
longCards: [...document.querySelectorAll('.local-memory-long-card')].map(card => ({
|
||
id: card.id,
|
||
rect: rect(card),
|
||
title: card.querySelector('.general-settings-panel__section-title')?.textContent?.trim(),
|
||
desc: card.querySelector('.general-settings-panel__section-desc')?.textContent?.trim(),
|
||
edit: rect(card.querySelector('.general-settings-panel__section-action .wb-button')),
|
||
preview: rect(card.querySelector('.local-memory-long__preview')),
|
||
text: card.querySelector('.local-memory-long__value,.local-memory-long__placeholder')?.textContent?.trim(),
|
||
placeholder: !!card.querySelector('.local-memory-long__placeholder')
|
||
})),
|
||
toneLabel: document.querySelector('.personalization-panel__tone-trigger .wb-select__value')?.textContent?.trim(),
|
||
popover: rect(document.querySelector('.personalization-panel__tone-popover')),
|
||
options: [...document.querySelectorAll('.personalization-panel__tone-popover [role=option]')].map(option => ({
|
||
name: option.querySelector('.personalization-panel__option-name')?.textContent?.trim(),
|
||
hint: option.querySelector('.personalization-panel__option-hint')?.textContent?.trim(),
|
||
selected: option.getAttribute('aria-selected')
|
||
})),
|
||
modal: rect(document.querySelector('.local-memory-edit-modal')),
|
||
modalTitle: document.querySelector('.local-memory-edit-modal .wb-modal__title')?.textContent?.trim(),
|
||
editorValue: document.querySelector('.local-memory-editor')?.value ?? null,
|
||
editorPlaceholder: document.querySelector('.local-memory-editor')?.getAttribute('placeholder') ?? null,
|
||
modalButtons: [...document.querySelectorAll('.local-memory-edit-modal .wb-modal__footer .wb-button')].map(button => ({ text: button.textContent.trim(), rect: rect(button) })),
|
||
rowBackground: getComputedStyle(document.querySelector('.local-memory-identity-card')).backgroundColor,
|
||
rowColor: getComputedStyle(document.querySelector('.local-memory-identity-row__label')).color,
|
||
loadingTips: localStorage.getItem('ycode:loading-tips'),
|
||
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('open-personalization-settings')
|
||
await openSettings('personalization'); observe(settings)
|
||
await wait('!!document.querySelector(".personalization-panel")', 'personalization panel', settings)
|
||
const nav = await read(`(()=>{const item=document.querySelector('[data-settings-tab="personalization"]')
|
||
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, 'personalization'])
|
||
|
||
const light = await read(layoutSource, settings)
|
||
snapshots.light = light
|
||
check('panel keeps the recovered 638x1160 box at 232,40',
|
||
[near(light.panel.x, 232), near(light.panel.y, 40), near(light.panel.width, 638), near(light.panel.height, 1160)], [true, true, true, true])
|
||
check('identity card keeps 638x100 with two 614x34 rows',
|
||
[near(light.identityCard.width, 638), near(light.identityCard.height, 100),
|
||
near(light.rows[0].rect.y, 48), near(light.rows[0].rect.width, 614), near(light.rows[0].rect.height, 34),
|
||
near(light.rows[1].rect.y, 98)], [true, true, true, true, true, true])
|
||
check('identity rows keep the recovered labels and empty pills',
|
||
[light.rows.map(row => row.label), light.rows.every(row => row.pill?.text === '暂无内容,点击编辑添加' && row.pill?.placeholder),
|
||
light.rows.every(row => row.pill && near(row.pill.rect.x, 686) && near(row.pill.rect.width, 172) && near(row.pill.rect.height, 34))],
|
||
[['YCode 对你的称呼', 'YCode 的名字'], true, true])
|
||
check('reply preference card keeps 638x204 with three recovered rows',
|
||
[near(light.preference.y, 156), near(light.preference.width, 638), near(light.preference.height, 204),
|
||
light.sections.map(section => section.title)], [true, true, true, ['回复风格', '欢迎语', '显示文件修改过程']])
|
||
check('reply style select keeps 120x32 and the default tone',
|
||
[near(light.sections[0].select.x, 734), near(light.sections[0].select.y, 174), near(light.sections[0].select.width, 120),
|
||
near(light.sections[0].select.height, 32), light.toneLabel], [true, true, true, true, '默认'])
|
||
check('switches keep 34x20 with 欢迎语 on and 显示文件修改过程 disabled',
|
||
[[near(light.sections[1].switch.width, 34), near(light.sections[1].switch.height, 20), light.sections[1].switchChecked].every(Boolean),
|
||
light.sections[2].switchChecked === false && light.sections[2].switchDisabled === true && light.sections[2].unavailable === '暂未接入'], [true, true])
|
||
check('three long cards keep 638x264 at y=376/656/936',
|
||
light.longCards.map((card, index) => [near(card.rect.y, [376, 656, 936][index]), near(card.rect.width, 638), near(card.rect.height, 264)].every(Boolean)),
|
||
[true, true, true])
|
||
check('long cards keep the recovered titles, descriptions and 598x160 previews',
|
||
[light.longCards.map(card => card.title), light.longCards.map(card => card.desc),
|
||
light.longCards.every(card => near(card.preview.width, 598) && near(card.preview.height, 160)),
|
||
light.longCards.every(card => card.edit && near(card.edit.width, 68) && near(card.edit.height, 32))], [
|
||
['给 YCode 的指令', '人设设定', '长期记忆'],
|
||
['写下你希望 YCode 一直遵守的要求,之后每次对话和任务都会默认生效。', '设定 YCode 的身份、性格与表达风格,对所有回复生效。', '跨会话保存的重要信息与结论,可随时查看和修改。'],
|
||
true, true])
|
||
check('long previews keep their original content and placeholders',
|
||
[light.longCards[0].placeholder, light.longCards[0].text, light.longCards[1].text?.startsWith("You're not a chatbot."), light.longCards[2].placeholder],
|
||
[true, '例如:回答尽量简洁,先给结论', true, true])
|
||
|
||
await step('personalization-tone')
|
||
await read(`document.querySelector('.personalization-panel__tone-trigger').click()`, settings)
|
||
const popover = await wait(`(()=>{const node=document.querySelector('.personalization-panel__tone-popover')
|
||
return node?{rect:(()=>{const r=node.getBoundingClientRect();return {x:r.x,width:r.width}})(),options:[...node.querySelectorAll('[role=option]')].length}:null})()`, 'tone popover', settings)
|
||
check('回复风格 popover keeps the recovered 240px width and 8 options', [near(popover.rect.width, 240), popover.options], [true, 8])
|
||
const options = await read(`[...document.querySelectorAll('.personalization-panel__tone-popover [role=option]')].map(option => [
|
||
option.querySelector('.personalization-panel__option-name')?.textContent?.trim(),
|
||
option.querySelector('.personalization-panel__option-hint')?.textContent?.trim()])`, settings)
|
||
check('tone options keep every recovered label and hint', [options.map(item => item[0]), options.map(item => item[1])], [TONE_LABELS, TONE_HINTS])
|
||
await read(`[...document.querySelectorAll('.personalization-panel__tone-popover [role=option]')][1].click()`, settings)
|
||
await wait(`document.querySelector('.personalization-panel__tone-trigger .wb-select__value')?.textContent?.trim()==='专业严谨'`, 'tone applied', settings)
|
||
const toneStored = await read(`window.ycode.personalization.get().then(view => view.tone)`, settings)
|
||
check('choosing 专业严谨 persists the tone', toneStored, 'professional')
|
||
|
||
await step('personalization-switches')
|
||
await read(`document.querySelector('#settings-loading-tips .wb-switch__input').click()`, settings)
|
||
await wait(`localStorage.getItem('ycode:loading-tips')==='0'`, 'loading tips mirror', settings)
|
||
check('欢迎语 switches off, persists and mirrors to the renderer',
|
||
[await read(`window.ycode.personalization.get().then(view => view.loadingTips)`, settings), await read(`localStorage.getItem('ycode:loading-tips')`, settings)], [false, '0'])
|
||
await read(`document.querySelector('#settings-loading-tips .wb-switch__input').click()`, settings)
|
||
await wait(`localStorage.getItem('ycode:loading-tips')==='1'`, 'loading tips restored', settings)
|
||
check('欢迎语 switches back on', await read(`window.ycode.personalization.get().then(view => view.loadingTips)`, settings), true)
|
||
await read(`document.querySelector('#settings-file-changes .wb-switch__input').click()`, settings)
|
||
check('显示文件修改过程 stays disabled and keeps the stored default', await read(`window.ycode.personalization.get().then(view => view.showFileChanges)`, settings), false)
|
||
|
||
await step('personalization-identity')
|
||
await read(`document.querySelector('#settings-local-memory-user-call-name .local-memory-pill').click()`, settings)
|
||
const editing = await wait(`(()=>{const row=document.querySelector('#settings-local-memory-user-call-name')
|
||
const input=row.querySelector('.local-memory-inline__input')
|
||
if(!input)return null
|
||
const r=input.getBoundingClientRect()
|
||
return { width:r.width, height:r.height, actions:row.querySelectorAll('.local-memory-inline__icon-btn').length,
|
||
confirmDisabled:row.querySelector('.local-memory-inline__icon-btn--confirm')?.disabled }})()`, 'identity editing', settings)
|
||
check('clicking the pill opens the recovered 172x34 inline editor with cancel/confirm',
|
||
[near(editing.width, 172), near(editing.height, 34), editing.actions, editing.confirmDisabled], [true, true, 2, true])
|
||
await read(setInput('#settings-local-memory-user-call-name .local-memory-inline__input', '小明'), settings)
|
||
await pause(120)
|
||
await read(`document.querySelector('#settings-local-memory-user-call-name .local-memory-inline__icon-btn--confirm').click()`, settings)
|
||
const savedIdentity = await wait(`(()=>{const pill=document.querySelector('#settings-local-memory-user-call-name .local-memory-pill')
|
||
return pill?pill.textContent.trim():null})()`, 'identity saved', settings)
|
||
check('confirming a non-empty identity persists and renders it',
|
||
[savedIdentity, await read(`window.ycode.personalization.get().then(view => view.userCallName)`, settings)], ['小明', '小明'])
|
||
await read(`document.querySelector('#settings-local-memory-ai-name .local-memory-pill').click()`, settings)
|
||
await wait(`!!document.querySelector('#settings-local-memory-ai-name .local-memory-inline__input')`, 'ai name editor', settings)
|
||
await read(setInput('#settings-local-memory-ai-name .local-memory-inline__input', '临时草稿'), settings)
|
||
// 真实焦点离开本行(点不可聚焦的 div 不会触发 blur,这里把焦点交给回复风格按钮)。
|
||
await read(`document.querySelector('.personalization-panel__tone-trigger').focus();true`, settings)
|
||
const afterBlur = await wait(`(()=>{const row=document.querySelector('#settings-local-memory-ai-name')
|
||
return row.querySelector('.local-memory-inline__input')?null:row.querySelector('.local-memory-pill')?.textContent?.trim()})()`, 'blur discards draft', settings)
|
||
check('blurring outside the row discards the identity draft like the original',
|
||
[afterBlur, await read(`window.ycode.personalization.get().then(view => view.aiName)`, settings)], ['暂无内容,点击编辑添加', ''])
|
||
|
||
await step('personalization-editors')
|
||
await read(`document.querySelector('#settings-local-memory-ai-name .local-memory-pill').click()`, settings)
|
||
await wait(`!!document.querySelector('#settings-local-memory-ai-name .local-memory-inline__input')`, 'ai name editor again', settings)
|
||
await read(setInput('#settings-local-memory-ai-name .local-memory-inline__input', '小助手'), settings)
|
||
await pause(100)
|
||
await read(`document.querySelector('#settings-local-memory-ai-name .local-memory-inline__icon-btn--confirm').click()`, settings)
|
||
await wait(`document.querySelector('#settings-local-memory-ai-name .local-memory-pill')?.textContent?.trim()==='小助手'`, 'ai name saved', settings)
|
||
await read(`document.querySelector('#settings-custom-prompt .local-memory-long__preview').click()`, settings)
|
||
const modal = await wait(`(()=>{const node=document.querySelector('.local-memory-edit-modal')
|
||
if(!node)return null
|
||
const r=node.getBoundingClientRect()
|
||
return { x:r.x, y:r.y, width:r.width, height:r.height,
|
||
title:node.querySelector('.wb-modal__title')?.textContent?.trim(),
|
||
placeholder:node.querySelector('.local-memory-editor')?.getAttribute('placeholder'),
|
||
buttons:[...node.querySelectorAll('.wb-modal__footer .wb-button')].map(button=>({text:button.textContent.trim(),width:button.getBoundingClientRect().width,height:button.getBoundingClientRect().height})) }})()`, 'prompt editor modal', settings)
|
||
snapshots.modal = modal
|
||
check('指令 editor keeps the recovered 640x520 modal with 保存/取消 68x32 buttons',
|
||
[[near(modal.x, 130), near(modal.y, 80), near(modal.width, 640), near(modal.height, 520)].every(Boolean),
|
||
modal.title, modal.buttons.map(button => button.text), modal.buttons.every(button => near(button.width, 68) && near(button.height, 32))],
|
||
[true, '给 YCode 的指令', ['取消', '保存'], true])
|
||
check('empty 指令 editor keeps the recovered placeholder', [modal.placeholder], ['当前内容为空,点击开始输入'])
|
||
await read(setInput('.local-memory-editor', '先给结论,再给依据。'), settings)
|
||
await pause(120)
|
||
await read(`[...document.querySelectorAll('.local-memory-edit-modal .wb-modal__footer .wb-button')].find(button=>button.textContent.trim()==='保存').click()`, settings)
|
||
await wait(`!document.querySelector('.local-memory-edit-modal')`, 'modal closed after save', settings)
|
||
const promptCard = await read(`(()=>{const card=document.querySelector('#settings-custom-prompt')
|
||
return { text:card.querySelector('.local-memory-long__value')?.textContent?.trim(), placeholder:!!card.querySelector('.local-memory-long__placeholder') }})()`, settings)
|
||
check('saving the 指令 updates the preview and persists',
|
||
[promptCard.text, promptCard.placeholder, await read(`window.ycode.personalization.get().then(view => view.customPrompt)`, settings)],
|
||
['先给结论,再给依据。', false, '先给结论,再给依据。'])
|
||
await read(`document.querySelector('#settings-local-memory-soul .local-memory-long__preview').click()`, settings)
|
||
const personaOpen = await wait(`(()=>{const node=document.querySelector('.local-memory-edit-modal')
|
||
return node?{title:node.querySelector('.wb-modal__title')?.textContent?.trim(), value:node.querySelector('.local-memory-editor')?.value}:null})()`, 'persona editor', settings)
|
||
check('人设 editor opens with the recovered default text',
|
||
[personaOpen.title, personaOpen.value?.startsWith("You're not a chatbot.")], ['人设设定', true])
|
||
await read(`[...document.querySelectorAll('.local-memory-edit-modal .wb-modal__footer .wb-button')].find(button=>button.textContent.trim()==='取消').click()`, settings)
|
||
await wait(`!document.querySelector('.local-memory-edit-modal')`, 'persona modal cancelled', settings)
|
||
check('cancelling the persona editor keeps the stored default', await read(`window.ycode.personalization.get().then(view => view.persona.startsWith("You're not a chatbot."))`, settings), true)
|
||
|
||
await step('personalization-persistence')
|
||
const config = JSON.parse(await fs.readFile(path.join(directory, 'data', 'config.json'), 'utf8'))
|
||
snapshots.config = config.personalization
|
||
check('config.json keeps the identity, tone and prompt values',
|
||
[config.personalization?.userCallName, config.personalization?.aiName, config.personalization?.tone, config.personalization?.customPrompt,
|
||
config.personalization?.loadingTips], ['小明', '小助手', 'professional', '先给结论,再给依据。', true])
|
||
settings.close()
|
||
await pause(500)
|
||
await openSettings('personalization')
|
||
const reopened = await read(layoutSource, settings)
|
||
check('reopened settings restores the persisted personalization',
|
||
[reopened.rows[0].pill?.text, reopened.rows[1].pill?.text, reopened.toneLabel, reopened.longCards[0].text],
|
||
['小明', '小助手', '专业严谨', '先给结论,再给依据。'])
|
||
|
||
await step('personalization-dark-theme')
|
||
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('personalization')
|
||
await wait(`document.documentElement.dataset.theme==='dark'&&!!document.querySelector('.personalization-panel')`, 'dark personalization panel', settings)
|
||
const dark = await read(layoutSource, settings)
|
||
snapshots.dark = dark
|
||
check('dark theme keeps the recovered card surface and text',
|
||
[dark.rowBackground, dark.rowColor], ['rgb(31, 31, 31)', 'rgb(242, 243, 245)'])
|
||
check('dark theme keeps every recovered geometry',
|
||
[near(dark.panel.width, 638), near(dark.panel.height, 1160), near(dark.preference.height, 204),
|
||
dark.longCards.every(card => near(card.preview.width, 598) && near(card.preview.height, 160))], [true, true, true, true])
|
||
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('personalization-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('PERSONALIZATION_SETTINGS_' + result + ' ' + directory)
|
||
for (const failure of checks.filter(item => !item.pass)) console.log('PERSONALIZATION_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)
|
||
})
|
||
}
|