@lobehub/lobehub 2.0.0-next.265 → 2.0.0-next.267

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +14 -0
  3. package/e2e/CLAUDE.md +34 -73
  4. package/e2e/docs/local-setup.md +67 -219
  5. package/e2e/scripts/setup.ts +529 -0
  6. package/e2e/src/features/home/sidebarAgent.feature +62 -0
  7. package/e2e/src/features/home/sidebarGroup.feature +62 -0
  8. package/e2e/src/steps/home/sidebarAgent.steps.ts +373 -0
  9. package/e2e/src/steps/home/sidebarGroup.steps.ts +168 -0
  10. package/e2e/src/steps/hooks.ts +2 -0
  11. package/locales/ar/chat.json +1 -0
  12. package/locales/ar/modelProvider.json +20 -0
  13. package/locales/ar/models.json +33 -10
  14. package/locales/ar/plugin.json +1 -0
  15. package/locales/ar/providers.json +1 -0
  16. package/locales/ar/setting.json +2 -0
  17. package/locales/bg-BG/chat.json +1 -0
  18. package/locales/bg-BG/modelProvider.json +20 -0
  19. package/locales/bg-BG/models.json +27 -7
  20. package/locales/bg-BG/plugin.json +1 -0
  21. package/locales/bg-BG/providers.json +1 -0
  22. package/locales/bg-BG/setting.json +2 -0
  23. package/locales/de-DE/chat.json +1 -0
  24. package/locales/de-DE/modelProvider.json +20 -0
  25. package/locales/de-DE/models.json +44 -10
  26. package/locales/de-DE/plugin.json +1 -0
  27. package/locales/de-DE/providers.json +1 -0
  28. package/locales/de-DE/setting.json +2 -0
  29. package/locales/en-US/chat.json +1 -0
  30. package/locales/en-US/modelProvider.json +20 -0
  31. package/locales/en-US/models.json +10 -10
  32. package/locales/en-US/providers.json +1 -0
  33. package/locales/en-US/setting.json +2 -1
  34. package/locales/es-ES/chat.json +1 -0
  35. package/locales/es-ES/modelProvider.json +20 -0
  36. package/locales/es-ES/models.json +53 -10
  37. package/locales/es-ES/plugin.json +1 -0
  38. package/locales/es-ES/providers.json +1 -0
  39. package/locales/es-ES/setting.json +2 -0
  40. package/locales/fa-IR/chat.json +1 -0
  41. package/locales/fa-IR/modelProvider.json +20 -0
  42. package/locales/fa-IR/models.json +33 -10
  43. package/locales/fa-IR/plugin.json +1 -0
  44. package/locales/fa-IR/providers.json +1 -0
  45. package/locales/fa-IR/setting.json +2 -0
  46. package/locales/fr-FR/chat.json +1 -0
  47. package/locales/fr-FR/modelProvider.json +20 -0
  48. package/locales/fr-FR/models.json +27 -7
  49. package/locales/fr-FR/plugin.json +1 -0
  50. package/locales/fr-FR/providers.json +1 -0
  51. package/locales/fr-FR/setting.json +2 -0
  52. package/locales/it-IT/chat.json +1 -0
  53. package/locales/it-IT/modelProvider.json +20 -0
  54. package/locales/it-IT/models.json +10 -10
  55. package/locales/it-IT/plugin.json +1 -0
  56. package/locales/it-IT/providers.json +1 -0
  57. package/locales/it-IT/setting.json +2 -0
  58. package/locales/ja-JP/chat.json +1 -0
  59. package/locales/ja-JP/modelProvider.json +20 -0
  60. package/locales/ja-JP/models.json +5 -10
  61. package/locales/ja-JP/plugin.json +1 -0
  62. package/locales/ja-JP/providers.json +1 -0
  63. package/locales/ja-JP/setting.json +2 -0
  64. package/locales/ko-KR/chat.json +1 -0
  65. package/locales/ko-KR/modelProvider.json +20 -0
  66. package/locales/ko-KR/models.json +36 -10
  67. package/locales/ko-KR/plugin.json +1 -0
  68. package/locales/ko-KR/providers.json +1 -0
  69. package/locales/ko-KR/setting.json +2 -0
  70. package/locales/nl-NL/chat.json +1 -0
  71. package/locales/nl-NL/modelProvider.json +20 -0
  72. package/locales/nl-NL/models.json +35 -4
  73. package/locales/nl-NL/plugin.json +1 -0
  74. package/locales/nl-NL/providers.json +1 -0
  75. package/locales/nl-NL/setting.json +2 -0
  76. package/locales/pl-PL/chat.json +1 -0
  77. package/locales/pl-PL/modelProvider.json +20 -0
  78. package/locales/pl-PL/models.json +37 -7
  79. package/locales/pl-PL/plugin.json +1 -0
  80. package/locales/pl-PL/providers.json +1 -0
  81. package/locales/pl-PL/setting.json +2 -0
  82. package/locales/pt-BR/chat.json +1 -0
  83. package/locales/pt-BR/modelProvider.json +20 -0
  84. package/locales/pt-BR/models.json +51 -9
  85. package/locales/pt-BR/plugin.json +1 -0
  86. package/locales/pt-BR/providers.json +1 -0
  87. package/locales/pt-BR/setting.json +2 -0
  88. package/locales/ru-RU/chat.json +1 -0
  89. package/locales/ru-RU/modelProvider.json +20 -0
  90. package/locales/ru-RU/models.json +48 -7
  91. package/locales/ru-RU/plugin.json +1 -0
  92. package/locales/ru-RU/providers.json +1 -0
  93. package/locales/ru-RU/setting.json +2 -0
  94. package/locales/tr-TR/chat.json +1 -0
  95. package/locales/tr-TR/modelProvider.json +20 -0
  96. package/locales/tr-TR/models.json +48 -7
  97. package/locales/tr-TR/plugin.json +1 -0
  98. package/locales/tr-TR/providers.json +1 -0
  99. package/locales/tr-TR/setting.json +2 -0
  100. package/locales/vi-VN/chat.json +1 -0
  101. package/locales/vi-VN/modelProvider.json +20 -0
  102. package/locales/vi-VN/models.json +5 -5
  103. package/locales/vi-VN/plugin.json +1 -0
  104. package/locales/vi-VN/providers.json +1 -0
  105. package/locales/vi-VN/setting.json +2 -0
  106. package/locales/zh-CN/modelProvider.json +20 -20
  107. package/locales/zh-CN/models.json +49 -8
  108. package/locales/zh-CN/providers.json +1 -0
  109. package/locales/zh-CN/setting.json +2 -1
  110. package/locales/zh-TW/chat.json +1 -0
  111. package/locales/zh-TW/modelProvider.json +20 -0
  112. package/locales/zh-TW/models.json +29 -10
  113. package/locales/zh-TW/plugin.json +1 -0
  114. package/locales/zh-TW/providers.json +1 -0
  115. package/locales/zh-TW/setting.json +2 -0
  116. package/package.json +3 -3
  117. package/packages/utils/src/multimodalContent.test.ts +302 -0
  118. package/packages/utils/src/server/__tests__/sse.test.ts +353 -0
  119. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Body.tsx +1 -1
  120. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/CronTopicGroup.tsx +84 -0
  121. package/src/app/[variants]/(main)/agent/_layout/Sidebar/{Topic/CronTopicList → Cron}/CronTopicItem.tsx +1 -1
  122. package/src/app/[variants]/(main)/agent/_layout/Sidebar/{Topic/CronTopicList → Cron}/index.tsx +23 -33
  123. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/List/Item/Editing.tsx +12 -49
  124. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/List/index.tsx +3 -1
  125. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/Editing.tsx +12 -40
  126. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/hooks/useTopicNavigation.ts +5 -1
  127. package/src/app/[variants]/(main)/agent/profile/features/AgentCronJobs/CronJobCards.tsx +1 -1
  128. package/src/app/[variants]/(main)/agent/profile/features/AgentCronJobs/CronJobForm.tsx +1 -1
  129. package/src/app/[variants]/(main)/group/_layout/Sidebar/AddGroupMemberModal/AvailableAgentList.tsx +0 -1
  130. package/src/app/[variants]/(main)/group/_layout/Sidebar/AddGroupMemberModal/index.tsx +5 -1
  131. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/Editing.tsx +4 -11
  132. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx +3 -3
  133. package/src/components/InlineRename/index.tsx +121 -0
  134. package/src/features/ChatInput/ActionBar/Params/Controls.tsx +42 -7
  135. package/src/features/NavPanel/components/NavItem.tsx +1 -1
  136. package/src/locales/default/setting.ts +2 -0
  137. package/src/store/agent/slices/cron/action.ts +108 -0
  138. package/src/store/agent/slices/cron/index.ts +1 -0
  139. package/src/store/agent/store.ts +3 -0
  140. package/src/store/home/slices/sidebarUI/action.ts +9 -0
  141. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/CronTopicList/CronTopicGroup.tsx +0 -74
  142. package/src/app/[variants]/(main)/group/features/ChangelogModal.tsx +0 -11
  143. package/src/hooks/useFetchCronTopicsWithJobInfo.ts +0 -56
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Home Sidebar Agent Steps
3
+ *
4
+ * Step definitions for Home page Agent management E2E tests
5
+ * - Rename
6
+ * - Pin/Unpin
7
+ * - Delete
8
+ */
9
+ import { Given, Then, When } from '@cucumber/cucumber';
10
+ import { expect } from '@playwright/test';
11
+
12
+ import { TEST_USER } from '../../support/seedTestUser';
13
+ import { CustomWorld } from '../../support/world';
14
+
15
+ /**
16
+ * Create a test agent directly in database
17
+ */
18
+ async function createTestAgent(title: string = 'Test Agent'): Promise<string> {
19
+ const databaseUrl = process.env.DATABASE_URL;
20
+ if (!databaseUrl) throw new Error('DATABASE_URL not set');
21
+
22
+ const { default: pg } = await import('pg');
23
+ const client = new pg.Client({ connectionString: databaseUrl });
24
+
25
+ try {
26
+ await client.connect();
27
+
28
+ const now = new Date().toISOString();
29
+ const agentId = `agent_e2e_test_${Date.now()}`;
30
+ const slug = `test-agent-${Date.now()}`;
31
+
32
+ await client.query(
33
+ `INSERT INTO agents (id, slug, title, user_id, created_at, updated_at)
34
+ VALUES ($1, $2, $3, $4, $5, $5)
35
+ ON CONFLICT DO NOTHING`,
36
+ [agentId, slug, title, TEST_USER.id, now],
37
+ );
38
+
39
+ console.log(` 📍 Created test agent in DB: ${agentId}`);
40
+ return agentId;
41
+ } finally {
42
+ await client.end();
43
+ }
44
+ }
45
+
46
+ // ============================================
47
+ // Given Steps
48
+ // ============================================
49
+
50
+ Given('用户在 Home 页面有一个 Agent', async function (this: CustomWorld) {
51
+ console.log(' 📍 Step: 在数据库中创建测试 Agent...');
52
+ const agentId = await createTestAgent('E2E Test Agent');
53
+ this.testContext.createdAgentId = agentId;
54
+
55
+ console.log(' 📍 Step: 导航到 Home 页面...');
56
+ await this.page.goto('/');
57
+ await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
58
+ await this.page.waitForTimeout(1000);
59
+
60
+ console.log(' 📍 Step: 查找新创建的 Agent...');
61
+ // Look for the newly created agent in the sidebar by its specific ID
62
+ const agentItem = this.page.locator(`a[href="/agent/${agentId}"]`).first();
63
+ await expect(agentItem).toBeVisible({ timeout: 10_000 });
64
+
65
+ // Store agent reference for later use
66
+ const agentLabel = await agentItem.getAttribute('aria-label');
67
+ this.testContext.targetItemId = agentLabel || agentId;
68
+ this.testContext.targetItemSelector = `a[href="/agent/${agentId}"]`;
69
+ this.testContext.targetType = 'agent';
70
+
71
+ console.log(` ✅ 找到 Agent: ${agentLabel}, id: ${agentId}`);
72
+ });
73
+
74
+ Given('该 Agent 未被置顶', async function (this: CustomWorld) {
75
+ console.log(' 📍 Step: 检查 Agent 未被置顶...');
76
+ // Check if the agent has a pin icon - if so, unpin it first
77
+ const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
78
+ const pinIcon = targetItem.locator('svg.lucide-pin');
79
+
80
+ if ((await pinIcon.count()) > 0) {
81
+ // Unpin it first
82
+ await targetItem.click({ button: 'right' });
83
+ await this.page.waitForTimeout(300);
84
+ const unpinOption = this.page.getByRole('menuitem', { name: /取消置顶|Unpin/i });
85
+ if ((await unpinOption.count()) > 0) {
86
+ await unpinOption.click();
87
+ await this.page.waitForTimeout(500);
88
+ }
89
+ // Close menu if still open
90
+ await this.page.click('body', { position: { x: 10, y: 10 } });
91
+ }
92
+
93
+ console.log(' ✅ Agent 未被置顶');
94
+ });
95
+
96
+ Given('该 Agent 已被置顶', async function (this: CustomWorld) {
97
+ console.log(' 📍 Step: 确保 Agent 已被置顶...');
98
+ // Check if the agent has a pin icon - if not, pin it first
99
+ const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
100
+ const pinIcon = targetItem.locator('svg.lucide-pin');
101
+
102
+ if ((await pinIcon.count()) === 0) {
103
+ // Pin it first
104
+ await targetItem.click({ button: 'right' });
105
+ await this.page.waitForTimeout(300);
106
+ const pinOption = this.page.getByRole('menuitem', { name: /置顶|Pin/i });
107
+ if ((await pinOption.count()) > 0) {
108
+ await pinOption.click();
109
+ await this.page.waitForTimeout(500);
110
+ }
111
+ // Close menu if still open
112
+ await this.page.click('body', { position: { x: 10, y: 10 } });
113
+ }
114
+
115
+ console.log(' ✅ Agent 已被置顶');
116
+ });
117
+
118
+ // ============================================
119
+ // When Steps
120
+ // ============================================
121
+
122
+ When('用户右键点击该 Agent', async function (this: CustomWorld) {
123
+ console.log(' 📍 Step: 右键点击 Agent...');
124
+
125
+ const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
126
+
127
+ // Right-click on the inner content (the NavItem Block component)
128
+ // The ContextMenuTrigger wraps the Block, not the Link
129
+ const innerBlock = targetItem.locator('> div').first();
130
+ if ((await innerBlock.count()) > 0) {
131
+ await innerBlock.click({ button: 'right' });
132
+ } else {
133
+ await targetItem.click({ button: 'right' });
134
+ }
135
+
136
+ await this.page.waitForTimeout(800);
137
+
138
+ // Debug: check what menus are visible
139
+ const menuItems = await this.page.locator('[role="menuitem"]').count();
140
+ console.log(` 📍 Debug: Found ${menuItems} menu items after right-click`);
141
+
142
+ console.log(' ✅ 已右键点击 Agent');
143
+ });
144
+
145
+ When('用户悬停在该 Agent 上', async function (this: CustomWorld) {
146
+ console.log(' 📍 Step: 悬停在 Agent 上...');
147
+
148
+ const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
149
+ await targetItem.hover();
150
+ await this.page.waitForTimeout(500);
151
+
152
+ console.log(' ✅ 已悬停在 Agent 上');
153
+ });
154
+
155
+ When('用户点击更多操作按钮', async function (this: CustomWorld) {
156
+ console.log(' 📍 Step: 点击更多操作按钮...');
157
+
158
+ const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
159
+ const moreButton = targetItem.locator('svg.lucide-ellipsis, svg.lucide-more-horizontal').first();
160
+
161
+ if ((await moreButton.count()) > 0) {
162
+ await moreButton.click();
163
+ } else {
164
+ // Fallback: find any visible ellipsis button
165
+ const allEllipsis = this.page.locator('svg.lucide-ellipsis');
166
+ for (let i = 0; i < (await allEllipsis.count()); i++) {
167
+ const ellipsis = allEllipsis.nth(i);
168
+ if (await ellipsis.isVisible()) {
169
+ await ellipsis.click();
170
+ break;
171
+ }
172
+ }
173
+ }
174
+
175
+ await this.page.waitForTimeout(500);
176
+ console.log(' ✅ 已点击更多操作按钮');
177
+ });
178
+
179
+ When('用户在菜单中选择重命名', async function (this: CustomWorld) {
180
+ console.log(' 📍 Step: 选择重命名选项...');
181
+
182
+ const renameOption = this.page.getByRole('menuitem', { name: /^(Rename|重命名)$/i });
183
+ await expect(renameOption).toBeVisible({ timeout: 5000 });
184
+ await renameOption.click();
185
+ await this.page.waitForTimeout(500);
186
+
187
+ console.log(' ✅ 已选择重命名选项');
188
+ });
189
+
190
+ When('用户在菜单中选择置顶', async function (this: CustomWorld) {
191
+ console.log(' 📍 Step: 选择置顶选项...');
192
+
193
+ const pinOption = this.page.getByRole('menuitem', { name: /^(Pin|置顶)$/i });
194
+ await expect(pinOption).toBeVisible({ timeout: 5000 });
195
+ await pinOption.click();
196
+ await this.page.waitForTimeout(500);
197
+
198
+ console.log(' ✅ 已选择置顶选项');
199
+ });
200
+
201
+ When('用户在菜单中选择取消置顶', async function (this: CustomWorld) {
202
+ console.log(' 📍 Step: 选择取消置顶选项...');
203
+
204
+ const unpinOption = this.page.getByRole('menuitem', { name: /^(Unpin|取消置顶)$/i });
205
+ await expect(unpinOption).toBeVisible({ timeout: 5000 });
206
+ await unpinOption.click();
207
+ await this.page.waitForTimeout(500);
208
+
209
+ console.log(' ✅ 已选择取消置顶选项');
210
+ });
211
+
212
+ When('用户在菜单中选择删除', async function (this: CustomWorld) {
213
+ console.log(' 📍 Step: 选择删除选项...');
214
+
215
+ const deleteOption = this.page.getByRole('menuitem', { name: /^(Delete|删除)$/i });
216
+ await expect(deleteOption).toBeVisible({ timeout: 5000 });
217
+ await deleteOption.click();
218
+ await this.page.waitForTimeout(300);
219
+
220
+ console.log(' ✅ 已选择删除选项');
221
+ });
222
+
223
+ When('用户在弹窗中确认删除', async function (this: CustomWorld) {
224
+ console.log(' 📍 Step: 确认删除...');
225
+
226
+ const confirmButton = this.page.locator('.ant-modal-confirm-btns button.ant-btn-dangerous');
227
+ await expect(confirmButton).toBeVisible({ timeout: 5000 });
228
+ await confirmButton.click();
229
+ await this.page.waitForTimeout(500);
230
+
231
+ console.log(' ✅ 已确认删除');
232
+ });
233
+
234
+ When('用户输入新的名称 {string}', async function (this: CustomWorld, newName: string) {
235
+ console.log(` 📍 Step: 输入新名称 "${newName}"...`);
236
+ await inputNewName.call(this, newName, false);
237
+ });
238
+
239
+ When(
240
+ '用户输入新的名称 {string} 并按 Enter',
241
+ async function (this: CustomWorld, newName: string) {
242
+ console.log(` 📍 Step: 输入新名称 "${newName}" 并按 Enter...`);
243
+ await inputNewName.call(this, newName, true);
244
+ },
245
+ );
246
+
247
+ // ============================================
248
+ // Then Steps
249
+ // ============================================
250
+
251
+ Then('该项名称应该更新为 {string}', async function (this: CustomWorld, expectedName: string) {
252
+ console.log(` 📍 Step: 验证名称为 "${expectedName}"...`);
253
+
254
+ await this.page.waitForTimeout(1000);
255
+ const renamedItem = this.page.getByText(expectedName, { exact: true }).first();
256
+ await expect(renamedItem).toBeVisible({ timeout: 5000 });
257
+
258
+ console.log(` ✅ 名称已更新为 "${expectedName}"`);
259
+ });
260
+
261
+ Then('Agent 应该显示置顶图标', async function (this: CustomWorld) {
262
+ console.log(' 📍 Step: 验证显示置顶图标...');
263
+
264
+ await this.page.waitForTimeout(500);
265
+ const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
266
+ const pinIcon = targetItem.locator('svg.lucide-pin');
267
+ await expect(pinIcon).toBeVisible({ timeout: 5000 });
268
+
269
+ console.log(' ✅ 置顶图标已显示');
270
+ });
271
+
272
+ Then('Agent 不应该显示置顶图标', async function (this: CustomWorld) {
273
+ console.log(' 📍 Step: 验证不显示置顶图标...');
274
+
275
+ await this.page.waitForTimeout(500);
276
+ const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
277
+ const pinIcon = targetItem.locator('svg.lucide-pin');
278
+ await expect(pinIcon).not.toBeVisible({ timeout: 5000 });
279
+
280
+ console.log(' ✅ 置顶图标未显示');
281
+ });
282
+
283
+ Then('Agent 应该从列表中移除', async function (this: CustomWorld) {
284
+ console.log(' 📍 Step: 验证 Agent 已移除...');
285
+
286
+ await this.page.waitForTimeout(500);
287
+
288
+ if (this.testContext.targetItemId) {
289
+ const deletedItem = this.page.locator(`a[aria-label="${this.testContext.targetItemId}"]`);
290
+ await expect(deletedItem).not.toBeVisible({ timeout: 5000 });
291
+ }
292
+
293
+ console.log(' ✅ Agent 已从列表中移除');
294
+ });
295
+
296
+ // ============================================
297
+ // Helper Functions
298
+ // ============================================
299
+
300
+ async function inputNewName(
301
+ this: CustomWorld,
302
+ newName: string,
303
+ pressEnter: boolean,
304
+ ): Promise<void> {
305
+ await this.page.waitForTimeout(300);
306
+
307
+ // Try to find the popover input
308
+ const popoverInputSelectors = [
309
+ '.ant-popover-inner input',
310
+ '.ant-popover-content input',
311
+ '.ant-popover input',
312
+ ];
313
+
314
+ let renameInput = null;
315
+
316
+ for (const selector of popoverInputSelectors) {
317
+ try {
318
+ const locator = this.page.locator(selector).first();
319
+ await locator.waitFor({ state: 'visible', timeout: 2000 });
320
+ renameInput = locator;
321
+ break;
322
+ } catch {
323
+ // Try next selector
324
+ }
325
+ }
326
+
327
+ if (!renameInput) {
328
+ // Fallback: find any visible input
329
+ const allInputs = this.page.locator('input:visible');
330
+ const count = await allInputs.count();
331
+
332
+ for (let i = 0; i < count; i++) {
333
+ const input = allInputs.nth(i);
334
+ const placeholder = (await input.getAttribute('placeholder').catch(() => '')) || '';
335
+ if (placeholder.includes('Search') || placeholder.includes('搜索')) continue;
336
+
337
+ const isInPopover = await input.evaluate((el) => {
338
+ return el.closest('.ant-popover') !== null || el.closest('[class*="popover"]') !== null;
339
+ });
340
+
341
+ if (isInPopover || count <= 2) {
342
+ renameInput = input;
343
+ break;
344
+ }
345
+ }
346
+ }
347
+
348
+ if (renameInput) {
349
+ await renameInput.click();
350
+ await renameInput.clear();
351
+ await renameInput.fill(newName);
352
+
353
+ if (pressEnter) {
354
+ await renameInput.press('Enter');
355
+ } else {
356
+ await this.page.click('body', { position: { x: 10, y: 10 } });
357
+ }
358
+ } else {
359
+ // Keyboard fallback
360
+ await this.page.keyboard.press('Meta+A');
361
+ await this.page.waitForTimeout(50);
362
+ await this.page.keyboard.type(newName, { delay: 20 });
363
+
364
+ if (pressEnter) {
365
+ await this.page.keyboard.press('Enter');
366
+ } else {
367
+ await this.page.click('body', { position: { x: 10, y: 10 } });
368
+ }
369
+ }
370
+
371
+ await this.page.waitForTimeout(1000);
372
+ console.log(` ✅ 已输入新名称 "${newName}"`);
373
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Home Sidebar Agent Group Steps
3
+ *
4
+ * Step definitions for Home page Agent Group management E2E tests
5
+ * - Rename
6
+ * - Pin/Unpin
7
+ * - Delete
8
+ */
9
+ import { Given, Then, When } from '@cucumber/cucumber';
10
+ import { expect } from '@playwright/test';
11
+
12
+ import { TEST_USER } from '../../support/seedTestUser';
13
+ import { CustomWorld } from '../../support/world';
14
+
15
+ /**
16
+ * Create a test chat group directly in database
17
+ */
18
+ async function createTestGroup(title: string = 'Test Group'): Promise<string> {
19
+ const databaseUrl = process.env.DATABASE_URL;
20
+ if (!databaseUrl) throw new Error('DATABASE_URL not set');
21
+
22
+ const { default: pg } = await import('pg');
23
+ const client = new pg.Client({ connectionString: databaseUrl });
24
+
25
+ try {
26
+ await client.connect();
27
+
28
+ const now = new Date().toISOString();
29
+ const groupId = `group_e2e_test_${Date.now()}`;
30
+
31
+ await client.query(
32
+ `INSERT INTO chat_groups (id, title, user_id, created_at, updated_at)
33
+ VALUES ($1, $2, $3, $4, $4)
34
+ ON CONFLICT DO NOTHING`,
35
+ [groupId, title, TEST_USER.id, now],
36
+ );
37
+
38
+ console.log(` 📍 Created test group in DB: ${groupId}`);
39
+ return groupId;
40
+ } finally {
41
+ await client.end();
42
+ }
43
+ }
44
+
45
+ // ============================================
46
+ // Given Steps
47
+ // ============================================
48
+
49
+ Given('用户在 Home 页面有一个 Agent Group', async function (this: CustomWorld) {
50
+ console.log(' 📍 Step: 在数据库中创建测试 Agent Group...');
51
+ const groupId = await createTestGroup('E2E Test Group');
52
+ this.testContext.createdGroupId = groupId;
53
+
54
+ console.log(' 📍 Step: 导航到 Home 页面...');
55
+ await this.page.goto('/');
56
+ await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
57
+ await this.page.waitForTimeout(1000);
58
+
59
+ console.log(' 📍 Step: 查找新创建的 Agent Group...');
60
+ const groupItem = this.page.locator(`a[href="/group/${groupId}"]`).first();
61
+ await expect(groupItem).toBeVisible({ timeout: 10_000 });
62
+
63
+ const groupLabel = await groupItem.getAttribute('aria-label');
64
+ this.testContext.targetItemId = groupLabel || groupId;
65
+ this.testContext.targetItemSelector = `a[href="/group/${groupId}"]`;
66
+ this.testContext.targetType = 'group';
67
+
68
+ console.log(` ✅ 找到 Agent Group: ${groupLabel}, id: ${groupId}`);
69
+ });
70
+
71
+ Given('该 Agent Group 未被置顶', async function (this: CustomWorld) {
72
+ console.log(' 📍 Step: 检查 Agent Group 未被置顶...');
73
+ const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
74
+ const pinIcon = targetItem.locator('svg.lucide-pin');
75
+
76
+ if ((await pinIcon.count()) > 0) {
77
+ await targetItem.click({ button: 'right' });
78
+ await this.page.waitForTimeout(300);
79
+ const unpinOption = this.page.getByRole('menuitem', { name: /取消置顶|Unpin/i });
80
+ if ((await unpinOption.count()) > 0) {
81
+ await unpinOption.click();
82
+ await this.page.waitForTimeout(500);
83
+ }
84
+ await this.page.click('body', { position: { x: 10, y: 10 } });
85
+ }
86
+
87
+ console.log(' ✅ Agent Group 未被置顶');
88
+ });
89
+
90
+ Given('该 Agent Group 已被置顶', async function (this: CustomWorld) {
91
+ console.log(' 📍 Step: 确保 Agent Group 已被置顶...');
92
+ const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
93
+ const pinIcon = targetItem.locator('svg.lucide-pin');
94
+
95
+ if ((await pinIcon.count()) === 0) {
96
+ await targetItem.click({ button: 'right' });
97
+ await this.page.waitForTimeout(300);
98
+ const pinOption = this.page.getByRole('menuitem', { name: /置顶|Pin/i });
99
+ if ((await pinOption.count()) > 0) {
100
+ await pinOption.click();
101
+ await this.page.waitForTimeout(500);
102
+ }
103
+ await this.page.click('body', { position: { x: 10, y: 10 } });
104
+ }
105
+
106
+ console.log(' ✅ Agent Group 已被置顶');
107
+ });
108
+
109
+ // ============================================
110
+ // When Steps
111
+ // ============================================
112
+
113
+ When('用户右键点击该 Agent Group', async function (this: CustomWorld) {
114
+ console.log(' 📍 Step: 右键点击 Agent Group...');
115
+
116
+ const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
117
+ await targetItem.click({ button: 'right' });
118
+ await this.page.waitForTimeout(500);
119
+
120
+ console.log(' ✅ 已右键点击 Agent Group');
121
+ });
122
+
123
+ When('用户悬停在该 Agent Group 上', async function (this: CustomWorld) {
124
+ console.log(' 📍 Step: 悬停在 Agent Group 上...');
125
+
126
+ const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
127
+ await targetItem.hover();
128
+ await this.page.waitForTimeout(500);
129
+
130
+ console.log(' ✅ 已悬停在 Agent Group 上');
131
+ });
132
+
133
+ // ============================================
134
+ // Then Steps
135
+ // ============================================
136
+
137
+ Then('Agent Group 应该显示置顶图标', async function (this: CustomWorld) {
138
+ console.log(' 📍 Step: 验证显示置顶图标...');
139
+
140
+ await this.page.waitForTimeout(500);
141
+ const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
142
+ const pinIcon = targetItem.locator('svg.lucide-pin');
143
+ await expect(pinIcon).toBeVisible({ timeout: 5000 });
144
+
145
+ console.log(' ✅ 置顶图标已显示');
146
+ });
147
+
148
+ Then('Agent Group 不应该显示置顶图标', async function (this: CustomWorld) {
149
+ console.log(' 📍 Step: 验证不显示置顶图标...');
150
+
151
+ await this.page.waitForTimeout(500);
152
+ const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
153
+ const pinIcon = targetItem.locator('svg.lucide-pin');
154
+ await expect(pinIcon).not.toBeVisible({ timeout: 5000 });
155
+
156
+ console.log(' ✅ 置顶图标未显示');
157
+ });
158
+
159
+ Then('Agent Group 应该从列表中移除', async function (this: CustomWorld) {
160
+ console.log(' 📍 Step: 验证 Agent Group 已移除...');
161
+
162
+ await this.page.waitForTimeout(500);
163
+
164
+ const deletedItem = this.page.locator(this.testContext.targetItemSelector);
165
+ await expect(deletedItem).not.toBeVisible({ timeout: 5000 });
166
+
167
+ console.log(' ✅ Agent Group 已从列表中移除');
168
+ });
@@ -84,6 +84,7 @@ Before(async function (this: CustomWorld, { pickle }) {
84
84
  (tag) =>
85
85
  tag.name.startsWith('@COMMUNITY-') ||
86
86
  tag.name.startsWith('@AGENT-') ||
87
+ tag.name.startsWith('@HOME-') ||
87
88
  tag.name.startsWith('@ROUTES-'),
88
89
  );
89
90
  console.log(`\n📝 Running: ${pickle.name}${testId ? ` (${testId.name.replace('@', '')})` : ''}`);
@@ -104,6 +105,7 @@ After(async function (this: CustomWorld, { pickle, result }) {
104
105
  (tag) =>
105
106
  tag.name.startsWith('@COMMUNITY-') ||
106
107
  tag.name.startsWith('@AGENT-') ||
108
+ tag.name.startsWith('@HOME-') ||
107
109
  tag.name.startsWith('@ROUTES-'),
108
110
  )
109
111
  ?.name.replace('@', '');
@@ -63,6 +63,7 @@
63
63
  "extendParams.reasoningEffort.title": "شدة التفكير",
64
64
  "extendParams.textVerbosity.title": "مستوى تفصيل النص الناتج",
65
65
  "extendParams.thinking.title": "مفتاح التفكير العميق",
66
+ "extendParams.thinkingBudget.title": "ميزانية التفكير",
66
67
  "extendParams.thinkingLevel.title": "مستوى التفكير",
67
68
  "extendParams.title": "ميزات توسيع النموذج",
68
69
  "extendParams.urlContext.desc": "عند التفعيل، سيتم تحليل الروابط تلقائيًا لاستخراج محتوى صفحة الويب الفعلي",
@@ -194,6 +194,26 @@
194
194
  "providerModels.item.modelConfig.deployName.title": "اسم نشر النموذج",
195
195
  "providerModels.item.modelConfig.displayName.placeholder": "يرجى إدخال اسم العرض للنموذج، مثل ChatGPT، GPT-4، إلخ.",
196
196
  "providerModels.item.modelConfig.displayName.title": "اسم عرض النموذج",
197
+ "providerModels.item.modelConfig.extendParams.extra": "اختر المعلمات الموسعة التي يدعمها النموذج. مرّر المؤشر فوق خيار لمعاينة عناصر التحكم. قد تؤدي التهيئة غير الصحيحة إلى فشل الطلب.",
198
+ "providerModels.item.modelConfig.extendParams.options.disableContextCaching.hint": "لنماذج Claude؛ يمكن أن يقلل التكلفة ويسرّع الاستجابات.",
199
+ "providerModels.item.modelConfig.extendParams.options.enableReasoning.hint": "لنماذج Claude وDeepSeek وغيرها من نماذج الاستدلال؛ يفعّل التفكير العميق.",
200
+ "providerModels.item.modelConfig.extendParams.options.gpt5ReasoningEffort.hint": "لسلسلة GPT-5؛ يتحكم في شدة الاستدلال.",
201
+ "providerModels.item.modelConfig.extendParams.options.gpt5_1ReasoningEffort.hint": "لسلسلة GPT-5.1؛ يتحكم في شدة الاستدلال.",
202
+ "providerModels.item.modelConfig.extendParams.options.gpt5_2ProReasoningEffort.hint": "لسلسلة GPT-5.2 Pro؛ يتحكم في شدة الاستدلال.",
203
+ "providerModels.item.modelConfig.extendParams.options.gpt5_2ReasoningEffort.hint": "لسلسلة GPT-5.2؛ يتحكم في شدة الاستدلال.",
204
+ "providerModels.item.modelConfig.extendParams.options.imageAspectRatio.hint": "لنماذج توليد الصور من Gemini؛ يتحكم في نسبة العرض إلى الارتفاع للصور المُولدة.",
205
+ "providerModels.item.modelConfig.extendParams.options.imageResolution.hint": "لنماذج توليد الصور من Gemini 3؛ يتحكم في دقة الصور المُولدة.",
206
+ "providerModels.item.modelConfig.extendParams.options.reasoningBudgetToken.hint": "لنماذج Claude وQwen3 وما شابهها؛ يتحكم في ميزانية الرموز المخصصة للاستدلال.",
207
+ "providerModels.item.modelConfig.extendParams.options.reasoningEffort.hint": "لنماذج OpenAI وغيرها من النماذج القادرة على الاستدلال؛ يتحكم في جهد الاستدلال.",
208
+ "providerModels.item.modelConfig.extendParams.options.textVerbosity.hint": "لسلسلة GPT-5+؛ يتحكم في تفصيل النص الناتج.",
209
+ "providerModels.item.modelConfig.extendParams.options.thinking.hint": "لبعض نماذج Doubao؛ يسمح للنموذج بتحديد ما إذا كان يجب التفكير بعمق.",
210
+ "providerModels.item.modelConfig.extendParams.options.thinkingBudget.hint": "لسلسلة Gemini؛ يتحكم في ميزانية التفكير.",
211
+ "providerModels.item.modelConfig.extendParams.options.thinkingLevel.hint": "لنماذج المعاينة السريعة من Gemini 3 Flash؛ يتحكم في عمق التفكير.",
212
+ "providerModels.item.modelConfig.extendParams.options.thinkingLevel2.hint": "لنماذج المعاينة الاحترافية من Gemini 3 Pro؛ يتحكم في عمق التفكير.",
213
+ "providerModels.item.modelConfig.extendParams.options.urlContext.hint": "لسلسلة Gemini؛ يدعم توفير سياق من خلال عنوان URL.",
214
+ "providerModels.item.modelConfig.extendParams.placeholder": "اختر المعلمات الموسعة لتفعيلها",
215
+ "providerModels.item.modelConfig.extendParams.previewFallback": "المعاينة غير متوفرة",
216
+ "providerModels.item.modelConfig.extendParams.title": "المعلمات الموسعة",
197
217
  "providerModels.item.modelConfig.files.extra": "تنفيذ رفع الملفات الحالي هو حل مؤقت، مخصص للتجربة الذاتية فقط. يرجى الانتظار حتى تتوفر إمكانيات رفع الملفات الكاملة في المستقبل.",
198
218
  "providerModels.item.modelConfig.files.title": "دعم رفع الملفات",
199
219
  "providerModels.item.modelConfig.functionCall.extra": "سيمكن هذا الإعداد النموذج من استخدام الأدوات، ولكن قدرة النموذج الفعلية على استخدامها تعتمد عليه بالكامل؛ يرجى الاختبار بنفسك.",