Polish Agent configuration and unify advanced settings

This commit is contained in:
鲨鱼辣椒 committed 2026-10-12 18:40:33 +08:00
1 parent a7182ac8fe
commit 1166931a26
13 files changed
+422 -102

No files matched your search

@@ -137,6 +137,86 @@ async function expectImagesLoaded(page: Page, selector = 'img') {
}))).toBe(true);
}
async function expectConfigurationBounds(page: Page) {
await expectNoOverflow(page);
await expect.poll(() => page.getByTestId('cloud-agent-editor').getByLabel('名称', { exact: true })
.evaluate(element => element.getBoundingClientRect().width)).toBeGreaterThanOrEqual(96);
// Document width alone misses controls clipped by the editor's overflow container.
const clipped = await page.getByTestId('cloud-agent-editor').evaluate(editor => {
const scroll = editor.querySelector('.agent-config-scroll')!;
const scrollBounds = scroll.getBoundingClientRect();
return [...editor.querySelectorAll('button, input, textarea, select, h2, summary, .agent-config-card')]
.filter(element => element.getClientRects().length > 0 && !element.classList.contains('sr-only'))
.flatMap(element => {
const bounds = element.getBoundingClientRect();
const insideScroll = scroll.contains(element);
const left = insideScroll ? scrollBounds.left : 0;
const right = insideScroll ? scrollBounds.right : innerWidth;
return bounds.left < left - 1 || bounds.right > right + 1
? [{ element: element.tagName, label: element.getAttribute('aria-label') ?? element.textContent?.trim(), left: bounds.left, right: bounds.right }]
: [];
});
});
expect(clipped).toEqual([]);
}
for (const width of [375, 768, 1024, 1440]) {
test(`introduction cards and one-page advanced settings fit and scroll at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 850 });
await installFixture(page, [draftAgent]);
await page.goto(baseUrl);
await page.getByRole('button', { name: '继续制作', exact: true }).click();
const editor = page.getByTestId('cloud-agent-editor');
const tabs = editor.getByRole('tablist', { name: '编辑设置', exact: true });
await expect(tabs.getByRole('tab')).toHaveCount(2);
await expect(editor.getByRole('heading', { name: '一句话介绍', exact: true })).toBeVisible();
await expect(editor.getByRole('heading', { name: '提示词', exact: true })).toBeVisible();
await expect(editor.locator('section.agent-config-card[data-tone="blue"]')).toHaveCSS('background-color', 'rgb(241, 246, 255)');
await expect(editor.locator('.agent-config-prompt[data-tone="lilac"]')).toHaveCSS('background-color', 'rgb(246, 242, 255)');
await editor.getByLabel('一句话介绍', { exact: true }).focus();
await expect(editor.getByLabel('一句话介绍', { exact: true })).toBeFocused();
await page.keyboard.press('Tab');
await expect(editor.getByLabel('提示词', { exact: true })).toBeFocused();
await expectConfigurationBounds(page);
if (width === 375 || width === 1440) {
await page.screenshot({ path: test.info().outputPath(`agent-introduction-prompt-${width}.png`), animations: 'disabled' });
}
await tabs.getByRole('tab', { name: '高级设置', exact: true }).click();
const advanced = editor.locator('.agent-advanced-settings');
await expect(tabs.getByRole('tab', { name: '高级设置', exact: true })).toHaveAttribute('aria-selected', 'true');
await expect(advanced.getByRole('navigation')).toHaveCount(0);
await expect(advanced.getByRole('tablist')).toHaveCount(0);
const headings = ['模型', '工具箱(Tool)', '做事手册(Skill)', 'MCP 服务', '子智能体', '知识库', '费用上限', '运行限制'];
for (const name of headings) await expect(advanced.getByRole('heading', { name, exact: true })).toBeVisible();
await expect(advanced.getByRole('heading').first()).toHaveText('模型');
await expect(advanced.getByLabel('模型', { exact: true })).toHaveAttribute('required', '');
await expect(advanced.getByText('选它做事时能用的工具。', { exact: true })).toBeVisible();
await expect(advanced.getByText('给它一本手册,照着方法做。', { exact: true })).toBeVisible();
await expect(advanced.getByLabel('模型', { exact: true })).toBeInViewport();
await expectConfigurationBounds(page);
if (width === 375 || width === 1440) {
await page.screenshot({ path: test.info().outputPath(`agent-advanced-upper-${width}.png`), animations: 'disabled' });
}
await advanced.getByLabel('单次运行时限(秒)', { exact: true }).scrollIntoViewIfNeeded();
await expect(advanced.getByLabel('单次运行时限(秒)', { exact: true })).toBeInViewport();
await expect(advanced.getByLabel('每日上限(词元点数)', { exact: true })).toBeVisible();
await expectConfigurationBounds(page);
if (width === 375 || width === 1440) {
await page.screenshot({ path: test.info().outputPath(`agent-advanced-lower-${width}.png`), animations: 'disabled' });
}
// Returning from the bottom resets the view to the introduction, without changing a draft.
await tabs.getByRole('tab', { name: '介绍与提示词', exact: true }).click();
await expect(editor.getByLabel('一句话介绍', { exact: true })).toBeInViewport();
await expect(editor.getByLabel('提示词', { exact: true })).toHaveValue(draftAgent.system_prompt);
const calls = await page.evaluate(() => (window as unknown as FixtureWindow).__agentAvatarFixture.calls);
expect(calls.filter(call => call.method === 'PATCH' || call.path === '/api/cloud-agents/avatars/preset'
|| (call.method === 'POST' && call.path === '/api/cloud-agents/agents'))).toEqual([]);
expect(calls.filter(call => call.path === '/api/cloud-agents/actions')
.every(call => ['catalog', 'resources', 'knowledge', 'budget', 'access'].includes(String(call.input?.operation)))).toBe(true);
});
}
for (const width of [375, 1366]) {
test(`pastel Agent cards and explicit chat/adjust controls remain usable at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 850 });
+8 -4
View File
@@ -167,15 +167,19 @@ test('personal cloud Agent identity, configuration and separate published chat i
await prompt.fill(initialPrompt);
await page.getByRole('button', { name: '开始使用', exact: true }).click();
await expect(advancedTab).toHaveAttribute('aria-selected', 'true');
await expect(page.getByRole('button', { name: '模型', exact: true })).toHaveAttribute('aria-current', 'page');
await expect(page.getByRole('navigation', { name: '配置分类', exact: true })).toHaveCount(0);
await expect(page.getByLabel('模型', { exact: true })).toBeVisible();
await expect(page.getByLabel('模型', { exact: true })).toHaveValue('');
await expect(page.getByLabel('模型', { exact: true })).toHaveAttribute('required', '');
await expect(page.getByRole('button', { name: '确认开始使用', exact: true })).toHaveCount(0);
expect((await fixtureState()).operations).not.toContain('publish');
await page.getByLabel('模型', { exact: true }).selectOption('reasoning-model');
await page.getByRole('button', { name: '能力', exact: true }).click();
await page.getByRole('button', { name: '选择工具', exact: true }).click();
await expect(page.getByText('选它做事时能用的工具。', { exact: true })).toBeVisible();
await expect(page.getByText('给它一本手册,照着方法做。', { exact: true })).toBeVisible();
await expect(page.getByRole('group', { name: /^知识库/ })).toBeVisible();
await expect(page.getByRole('heading', { name: '费用上限', exact: true })).toBeVisible();
await expect(page.getByRole('heading', { name: '运行限制', exact: true })).toBeVisible();
await page.getByRole('button', { name: '选择工具箱', exact: true }).click();
await page.getByRole('checkbox', { name: '资料整理工具', exact: true }).check();
await instructionsTab.click();
await expect(introduction).toHaveValue('一起梳理故事和文章');
@@ -202,7 +206,7 @@ test('personal cloud Agent identity, configuration and separate published chat i
await expect(introduction).toHaveValue('');
await expect(prompt).toHaveValue(initialPrompt);
await advancedTab.click();
await expect(page.getByRole('button', { name: '模型', exact: true })).toHaveAttribute('aria-current', 'page');
await expect(page.getByRole('navigation', { name: '配置分类', exact: true })).toHaveCount(0);
await expect(page.getByLabel('模型', { exact: true })).toHaveValue('reasoning-model');
await page.screenshot({ path: testInfo.outputPath('cloud-agent-advanced-model.png') });
await instructionsTab.click();
+1 -1
View File
@@ -52,7 +52,7 @@ it('keeps one child creation intent after a lost response and waits for Skill in
await waitFor(() => expect(screen.queryByLabelText('子智能体名称')).not.toBeInTheDocument());
const calls = api.call.mock.calls.filter(call => call[0] === 'createChild');
expect(calls).toHaveLength(2); expect(calls[0][1]).toEqual(calls[1][1]);
fireEvent.click(screen.getByRole('button', { name: '上传 Skill' }));
fireEvent.click(screen.getByRole('button', { name: '上传做事手册' }));
const confirm = await screen.findByRole('button', { name: '确认安装到个人能力' });
expect(api.call.mock.calls.some(call => call[0] === 'confirmSkill')).toBe(false);
fireEvent.click(confirm);
+81 -16
View File
@@ -249,7 +249,7 @@ it('creates from name and avatar, then configures an optional introduction, adva
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '我的文章搭档' } });
expect(screen.getByLabelText('模型')).toHaveValue('');
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
expect(screen.getByRole('button', { name: '模型', exact: true })).toHaveAttribute('aria-current', 'page');
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
expect(screen.getByLabelText('模型')).toBeVisible();
expect(screen.getByLabelText('模型')).toBeRequired();
fireEvent.change(screen.getByLabelText('模型'), { target: { value: 'model-second' } });
@@ -440,9 +440,7 @@ it('requires an explicit advanced model choice before starting use but permits a
expect(api.save).toHaveBeenCalledWith(draft.slug, expect.objectContaining({ configuration: expect.objectContaining({ model: '' }) }));
fireEvent.click(screen.getByRole('button', { name: '开始使用', exact: true }));
expect(screen.getByRole('tab', { name: '高级设置' })).toHaveAttribute('aria-selected', 'true');
const categories = within(screen.getByRole('navigation', { name: '配置分类' })).getAllByRole('button');
expect(categories[0]).toHaveAccessibleName('模型');
expect(categories[0]).toHaveAttribute('aria-current', 'page');
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
expect(screen.getByLabelText('模型')).toBeVisible();
expect(screen.getByLabelText('模型')).toBeRequired();
expect(screen.getByRole('alert')).toHaveTextContent('请先在高级设置中选择模型。');
@@ -456,6 +454,77 @@ it('requires an explicit advanced model choice before starting use but permits a
expect(api.call.mock.calls.some(([operation]) => ['publish', 'preview', 'submit'].includes(operation))).toBe(false);
});
it('shows all advanced settings together and preserves unfinished selections, search and limits when switching pages', async () => {
const base = api.call.getMockImplementation()!;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'catalog') return {
models: [{ id: 'model-a', name: '模型甲' }],
resources: {
tools: [{ key: 'writing-tool', name: '文章整理工具' }],
skills: [{ key: 'writing-guide', name: '写作方法' }],
}, pricing: null,
};
return base(operation, input);
});
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
await waitFor(() => expect(screen.getByLabelText('模型')).toBeEnabled());
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
const model = screen.getByLabelText('模型');
const toolbox = screen.getByRole('group', { name: /^工具箱(Tool)/ });
const handbook = screen.getByRole('group', { name: /^做事手册(Skill)/ });
expect(model).toBeVisible();
expect(model).toBeRequired();
expect(toolbox).toBeVisible();
expect(handbook).toBeVisible();
expect(model.compareDocumentPosition(toolbox) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(screen.getByText('选它做事时能用的工具。', { exact: true })).toBeVisible();
expect(screen.getByText('给它一本手册,照着方法做。', { exact: true })).toBeVisible();
expect(screen.getByRole('group', { name: /^知识库/ })).toBeVisible();
expect(screen.getByRole('heading', { name: '费用上限', exact: true })).toBeVisible();
expect(screen.getByRole('heading', { name: '运行限制', exact: true })).toBeVisible();
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
expect(screen.getAllByRole('tab').map(tab => tab.textContent)).toEqual(['介绍与提示词', '高级设置']);
fireEvent.change(model, { target: { value: 'model-a' } });
fireEvent.click(screen.getByRole('button', { name: '选择做事手册', exact: true }));
fireEvent.click(screen.getByRole('checkbox', { name: '写作方法', exact: true }));
fireEvent.click(screen.getByRole('button', { name: '选择工具箱', exact: true }));
const search = screen.getByLabelText('搜索工具箱');
fireEvent.change(search, { target: { value: '文章' } });
fireEvent.click(screen.getByRole('checkbox', { name: '文章整理工具', exact: true }));
const steps = screen.getByLabelText('最多执行步数');
fireEvent.change(steps, { target: { value: '70' } });
const budget = screen.getByLabelText('每次任务上限(词元点数)');
await waitFor(() => expect(budget).toBeEnabled());
fireEvent.change(budget, { target: { value: '2.50' } });
fireEvent.mouseDown(screen.getByRole('tab', { name: '介绍与提示词' }), { button: 0, ctrlKey: false });
const prompt = ' 保留我的原始写作要求。\n一次讨论一个想法。\n';
fireEvent.change(screen.getByLabelText('提示词'), { target: { value: prompt } });
expect(model).not.toBeVisible();
expect(search).not.toBeVisible();
expect(budget).not.toBeVisible();
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
expect(screen.getByLabelText('模型')).toBe(model);
expect(model).toHaveValue('model-a');
expect(screen.getByLabelText('搜索工具箱')).toBe(search);
expect(search).toHaveValue('文章');
expect(screen.getByRole('checkbox', { name: '文章整理工具', exact: true })).toBeChecked();
expect(screen.getByRole('checkbox', { name: '写作方法', exact: true })).toBeChecked();
expect(steps).toHaveValue(70);
expect(budget).toHaveValue(2.5);
expect(screen.getByRole('button', { name: '保存费用上限', exact: true })).toBeEnabled();
expect(screen.getByLabelText('提示词')).toHaveValue(prompt);
const unload = new Event('beforeunload', { cancelable: true });
window.dispatchEvent(unload);
expect(unload.defaultPrevented).toBe(true);
expect(api.save).not.toHaveBeenCalled();
expect(api.call.mock.calls.some(([operation]) => ['saveBudget', 'publish', 'preview', 'submit'].includes(operation))).toBe(false);
});
it('retains requirements when model loading fails and offers a usable retry', async () => {
const base = api.call.getMockImplementation()!;
let fail = true;
@@ -650,7 +719,7 @@ it('saves an existing configured draft without creating or loading a configurati
expect(api.call.mock.calls.some(([operation]) => ['threads', 'history', 'preview', 'submit', 'publish'].includes(operation))).toBe(false);
});
it('switches peer editor tabs while retaining the advanced category and unfinished form inputs', async () => {
it('switches editor pages while retaining unfinished knowledge and budget form inputs', async () => {
api.list.mockResolvedValue({ agents: [{ ...draft, configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'test-model' } }], next_cursor: null });
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
@@ -669,18 +738,17 @@ it('switches peer editor tabs while retaining the advanced category and unfinish
expect(requirements).toHaveAttribute('aria-selected', 'false');
expect(screen.getByRole('tabpanel', { name: '高级设置' })).toBeVisible();
expect(instructions).not.toBeVisible();
expect(screen.getByRole('button', { name: '模型', exact: true })).toHaveAttribute('aria-current', 'page');
fireEvent.click(screen.getByRole('button', { name: '知识', exact: true }));
expect(screen.getByLabelText('模型')).toBeVisible();
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
fireEvent.click(screen.getByText('管理我的知识文档'));
fireEvent.click(screen.getByRole('button', { name: '新建知识库' }));
const knowledgeName = screen.getByLabelText('知识库名称');
fireEvent.change(knowledgeName, { target: { value: '还没创建的资料' } });
fireEvent.click(screen.getByRole('button', { name: '限制', exact: true }));
const budgetInput = screen.getByLabelText('每日上限(词元点数)');
await waitFor(() => expect(budgetInput).toBeEnabled());
fireEvent.change(budgetInput, { target: { value: '12.50' } });
fireEvent.mouseDown(advanced, { button: 0, ctrlKey: false });
expect(screen.getByRole('button', { name: '限制', exact: true })).toHaveAttribute('aria-current', 'page');
expect(knowledgeName).toBeVisible();
expect(budgetInput).toBeVisible();
fireEvent.mouseDown(requirements, { button: 0, ctrlKey: false });
@@ -689,10 +757,9 @@ it('switches peer editor tabs while retaining the advanced category and unfinish
expect(screen.getByLabelText('提示词')).toBe(instructions);
expect(instructions).toHaveValue('没有保存的要求');
fireEvent.mouseDown(advanced, { button: 0, ctrlKey: false });
expect(screen.getByRole('button', { name: '限制', exact: true })).toHaveAttribute('aria-current', 'page');
expect(screen.queryByRole('navigation', { name: '配置分类' })).not.toBeInTheDocument();
expect(screen.getByLabelText('每日上限(词元点数)')).toBe(budgetInput);
expect(budgetInput).toHaveValue(12.5);
fireEvent.click(screen.getByRole('button', { name: '知识', exact: true }));
expect(screen.getByLabelText('知识库名称')).toBe(knowledgeName);
expect(knowledgeName).toHaveValue('还没创建的资料');
expect(screen.queryByLabelText('消息')).not.toBeInTheDocument();
@@ -711,7 +778,6 @@ it('retains unsaved instructions while saving a budget independently of the draf
expect(screen.queryByLabelText('消息')).not.toBeInTheDocument();
expect(screen.getByLabelText('提示词')).toHaveValue('未保存的指令');
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
fireEvent.click(screen.getByRole('button', { name: '限制', exact: true }));
const budget = screen.getByLabelText('每日上限(词元点数)');
await waitFor(() => expect(budget).toBeEnabled());
fireEvent.change(budget, { target: { value: '12' } });
@@ -755,13 +821,12 @@ it('searches capabilities while retaining existing selections outside the result
open();
fireEvent.click(await screen.findByRole('button', { name: '继续制作', exact: true }));
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
fireEvent.click(screen.getByRole('button', { name: '能力', exact: true }));
expect(await screen.findByRole('checkbox', { name: /missing-tool/ })).toBeChecked();
fireEvent.click(screen.getByRole('button', { name: '选择工具', exact: true }));
fireEvent.change(screen.getByLabelText('搜索工具'), { target: { value: '资料' } });
fireEvent.click(screen.getByRole('button', { name: '选择工具箱', exact: true }));
fireEvent.change(screen.getByLabelText('搜索工具箱'), { target: { value: '资料' } });
fireEvent.mouseDown(screen.getByRole('tab', { name: '介绍与提示词' }), { button: 0, ctrlKey: false });
fireEvent.mouseDown(screen.getByRole('tab', { name: '高级设置' }), { button: 0, ctrlKey: false });
expect(screen.getByLabelText('搜索工具')).toHaveValue('资料');
expect(screen.getByLabelText('搜索工具箱')).toHaveValue('资料');
expect(screen.queryByRole('checkbox', { name: '当前时间' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('checkbox', { name: /资料搜索/ }));
fireEvent.click(screen.getByRole('button', { name: '完成选择' }));