@lobehub/lobehub 2.0.0-next.277 → 2.0.0-next.279

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 (91) hide show
  1. package/.cursor/rules/db-migrations.mdc +1 -1
  2. package/.cursor/rules/debug-usage.mdc +7 -5
  3. package/.cursor/rules/desktop-controller-tests.mdc +2 -1
  4. package/.cursor/rules/desktop-feature-implementation.mdc +9 -5
  5. package/.cursor/rules/desktop-local-tools-implement.mdc +67 -66
  6. package/.cursor/rules/desktop-menu-configuration.mdc +21 -9
  7. package/.cursor/rules/desktop-window-management.mdc +17 -2
  8. package/.cursor/rules/drizzle-schema-style-guide.mdc +6 -6
  9. package/.cursor/rules/hotkey.mdc +1 -0
  10. package/.cursor/rules/i18n.mdc +1 -0
  11. package/.cursor/rules/project-structure.mdc +16 -3
  12. package/.cursor/rules/react.mdc +17 -5
  13. package/.cursor/rules/recent-data-usage.mdc +2 -1
  14. package/.cursor/rules/testing-guide/testing-guide.mdc +262 -238
  15. package/.cursor/rules/testing-guide/zustand-store-action-test.mdc +1 -1
  16. package/.cursor/rules/zustand-action-patterns.mdc +1 -1
  17. package/.cursor/rules/zustand-slice-organization.mdc +4 -4
  18. package/CHANGELOG.md +51 -0
  19. package/CLAUDE.md +1 -1
  20. package/GEMINI.md +1 -1
  21. package/changelog/v1.json +14 -0
  22. package/docs/development/database-schema.dbml +16 -0
  23. package/locales/en-US/chat.json +24 -0
  24. package/locales/zh-CN/chat.json +24 -0
  25. package/package.json +1 -1
  26. package/packages/business/const/src/index.ts +3 -0
  27. package/packages/database/migrations/0069_add_topic_shares_table.sql +22 -0
  28. package/packages/database/migrations/meta/0069_snapshot.json +9704 -0
  29. package/packages/database/migrations/meta/_journal.json +7 -0
  30. package/packages/database/src/models/__tests__/topicShare.test.ts +318 -0
  31. package/packages/database/src/models/topicShare.ts +177 -0
  32. package/packages/database/src/schemas/topic.ts +44 -2
  33. package/packages/types/src/conversation.ts +5 -0
  34. package/packages/types/src/topic/topic.ts +46 -0
  35. package/src/app/[variants]/(main)/agent/features/Conversation/Header/ShareButton/index.tsx +24 -9
  36. package/src/app/[variants]/(main)/agent/features/Conversation/ThreadHydration.tsx +2 -1
  37. package/src/app/[variants]/(main)/agent/features/Portal/_layout/Mobile.tsx +3 -3
  38. package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/GroupMember.tsx +3 -2
  39. package/src/app/[variants]/(main)/group/features/Conversation/Header/ShareButton/index.tsx +26 -9
  40. package/src/app/[variants]/(main)/group/features/Conversation/ThreadHydration.tsx +2 -1
  41. package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +3 -3
  42. package/src/app/[variants]/(main)/group/profile/features/MemberProfile/AgentTool.tsx +4 -1
  43. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/AspectRatioSelect/index.tsx +1 -2
  44. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ImageNum.tsx +54 -173
  45. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ResolutionSelect.tsx +22 -67
  46. package/src/app/[variants]/(mobile)/router/mobileRouter.config.tsx +18 -0
  47. package/src/app/[variants]/router/desktopRouter.config.tsx +18 -0
  48. package/src/app/[variants]/share/t/[id]/SharedMessageList.tsx +54 -0
  49. package/src/app/[variants]/share/t/[id]/_layout/index.tsx +170 -0
  50. package/src/app/[variants]/share/t/[id]/features/Portal/index.tsx +66 -0
  51. package/src/app/[variants]/share/t/[id]/index.tsx +112 -0
  52. package/src/app/robots.tsx +1 -1
  53. package/src/business/client/BusinessMobileRoutes.tsx +1 -1
  54. package/src/features/Conversation/ChatList/index.tsx +17 -6
  55. package/src/features/Conversation/Messages/AssistantGroup/Tool/Render/index.tsx +8 -4
  56. package/src/features/Conversation/Messages/AssistantGroup/Tool/index.tsx +15 -10
  57. package/src/features/Conversation/Messages/AssistantGroup/Tools.tsx +3 -1
  58. package/src/features/Conversation/Messages/AssistantGroup/components/ContentBlock.tsx +3 -2
  59. package/src/features/Conversation/Messages/AssistantGroup/components/GroupItem.tsx +2 -2
  60. package/src/features/Conversation/Messages/Supervisor/components/ContentBlock.tsx +25 -26
  61. package/src/features/Conversation/Messages/Supervisor/components/Group.tsx +4 -2
  62. package/src/features/Conversation/Messages/Tool/Tool/index.tsx +16 -12
  63. package/src/features/Conversation/Messages/Tool/index.tsx +20 -11
  64. package/src/features/Conversation/Messages/index.tsx +1 -1
  65. package/src/features/Conversation/store/slices/data/action.test.ts +42 -0
  66. package/src/features/Conversation/store/slices/data/action.ts +4 -2
  67. package/src/features/Portal/GroupThread/Header/index.tsx +2 -2
  68. package/src/features/Portal/MessageDetail/Body/index.tsx +3 -3
  69. package/src/features/Portal/components/Header.tsx +3 -3
  70. package/src/features/ProfileEditor/AgentTool.tsx +50 -19
  71. package/src/features/SharePopover/index.tsx +215 -0
  72. package/src/features/SharePopover/style.ts +10 -0
  73. package/src/hooks/useNavigateToAgent.ts +3 -3
  74. package/src/libs/next/proxy/define-config.ts +4 -1
  75. package/src/locales/default/chat.ts +26 -0
  76. package/src/proxy.ts +1 -0
  77. package/src/server/routers/lambda/__tests__/message.test.ts +152 -0
  78. package/src/server/routers/lambda/__tests__/share.test.ts +227 -0
  79. package/src/server/routers/lambda/__tests__/topic.test.ts +174 -0
  80. package/src/server/routers/lambda/index.ts +2 -0
  81. package/src/server/routers/lambda/message.ts +37 -4
  82. package/src/server/routers/lambda/share.ts +55 -0
  83. package/src/server/routers/lambda/topic.ts +45 -0
  84. package/src/services/message/index.ts +1 -0
  85. package/src/services/topic/index.ts +16 -0
  86. package/src/store/chat/slices/portal/action.test.ts +0 -41
  87. package/src/store/chat/slices/portal/action.ts +0 -25
  88. package/src/store/chat/slices/thread/action.test.ts +10 -6
  89. package/src/store/chat/slices/thread/action.ts +10 -3
  90. package/src/app/[variants]/(main)/group/features/Portal/features/Portal.tsx +0 -105
  91. package/src/app/[variants]/(main)/group/features/Portal/features/PortalPanel.tsx +0 -23
@@ -11,34 +11,33 @@ import { Tools } from '../../AssistantGroup/Tools';
11
11
  import Reasoning from '../../components/Reasoning';
12
12
  import MessageContent from './MessageContent';
13
13
 
14
- const ContentBlock = memo<AssistantContentBlock>(({ id, tools, content, reasoning, error }) => {
15
- const errorContent = useErrorContent(error);
16
- const isReasoning = useConversationStore(messageStateSelectors.isMessageInReasoning(id));
17
- const hasTools = tools && tools.length > 0;
18
- const showReasoning =
19
- (!!reasoning && reasoning.content?.trim() !== '') || (!reasoning && isReasoning);
20
-
21
- if (error && (content === LOADING_FLAT || !content))
22
- return (
23
- <ErrorContent
24
- error={
25
- errorContent && error && (content === LOADING_FLAT || !content) ? errorContent : undefined
26
- }
27
- id={id}
28
- />
29
- );
14
+ interface ContentBlockProps extends AssistantContentBlock {
15
+ disableEditing?: boolean;
16
+ }
17
+
18
+ const ContentBlock = memo<ContentBlockProps>(
19
+ ({ id, tools, content, reasoning, error, disableEditing }) => {
20
+ const errorContent = useErrorContent(error);
21
+ const isReasoning = useConversationStore(messageStateSelectors.isMessageInReasoning(id));
22
+ const hasTools = tools && tools.length > 0;
23
+ const showReasoning =
24
+ (!!reasoning && reasoning.content?.trim() !== '') || (!reasoning && isReasoning);
30
25
 
31
- return (
32
- <Flexbox gap={8} id={id}>
33
- {showReasoning && <Reasoning {...reasoning} id={id} />}
26
+ if (error && (content === LOADING_FLAT || !content))
27
+ return <ErrorContent error={errorContent} id={id} />;
34
28
 
35
- {/* Content - markdown text */}
36
- <MessageContent content={content} hasTools={hasTools} id={id} />
29
+ return (
30
+ <Flexbox gap={8} id={id}>
31
+ {showReasoning && <Reasoning {...reasoning} id={id} />}
32
+
33
+ {/* Content - markdown text */}
34
+ <MessageContent content={content} hasTools={hasTools} id={id} />
37
35
 
38
- {/* Tools */}
39
- {hasTools && <Tools messageId={id} tools={tools} />}
40
- </Flexbox>
41
- );
42
- });
36
+ {/* Tools */}
37
+ {hasTools && <Tools disableEditing={disableEditing} messageId={id} tools={tools} />}
38
+ </Flexbox>
39
+ );
40
+ },
41
+ );
43
42
 
44
43
  export default ContentBlock;
@@ -29,7 +29,7 @@ interface GroupChildrenProps {
29
29
  messageIndex: number;
30
30
  }
31
31
 
32
- const Group = memo<GroupChildrenProps>(({ blocks, id, content }) => {
32
+ const Group = memo<GroupChildrenProps>(({ blocks, id, content, disableEditing }) => {
33
33
  const isCollapsed = useConversationStore(messageStateSelectors.isMessageCollapsed(id));
34
34
  const contextValue = useMemo(() => ({ assistantGroupId: id }), [id]);
35
35
 
@@ -46,7 +46,9 @@ const Group = memo<GroupChildrenProps>(({ blocks, id, content }) => {
46
46
  <MessageAggregationContext value={contextValue}>
47
47
  <Flexbox className={styles.container} gap={8}>
48
48
  {blocks.map((item) => {
49
- return <ContentBlock {...item} key={id + '.' + item.id} />;
49
+ return (
50
+ <ContentBlock {...item} disableEditing={disableEditing} key={id + '.' + item.id} />
51
+ );
50
52
  })}
51
53
  </Flexbox>
52
54
  </MessageAggregationContext>
@@ -20,6 +20,7 @@ const Render = dynamic(() => import('../../AssistantGroup/Tool/Render'), {
20
20
  export interface InspectorProps {
21
21
  apiName: string;
22
22
  arguments?: string;
23
+ disableEditing?: boolean;
23
24
  identifier: string;
24
25
  index: number;
25
26
  messageId: string;
@@ -32,7 +33,7 @@ export interface InspectorProps {
32
33
  * Tool message component - adapts Tool message data to use AssistantGroup/Tool components
33
34
  */
34
35
  const Tool = memo<InspectorProps>(
35
- ({ arguments: requestArgs, apiName, messageId, toolCallId, index, identifier, type }) => {
36
+ ({ arguments: requestArgs, apiName, disableEditing, messageId, toolCallId, index, identifier, type }) => {
36
37
  const [showDebug, setShowDebug] = useState(false);
37
38
  const [showPluginRender, setShowPluginRender] = useState(false);
38
39
  const [expand, setExpand] = useState(true);
@@ -66,16 +67,18 @@ const Tool = memo<InspectorProps>(
66
67
  >
67
68
  <AccordionItem
68
69
  action={
69
- <Actions
70
- assistantMessageId={messageId}
71
- handleExpand={(expand) => setExpand(!!expand)}
72
- identifier={identifier}
73
- setShowDebug={setShowDebug}
74
- setShowPluginRender={setShowPluginRender}
75
- showCustomPluginRender={false}
76
- showDebug={showDebug}
77
- showPluginRender={showPluginRender}
78
- />
70
+ !disableEditing && (
71
+ <Actions
72
+ assistantMessageId={messageId}
73
+ handleExpand={(expand) => setExpand(!!expand)}
74
+ identifier={identifier}
75
+ setShowDebug={setShowDebug}
76
+ setShowPluginRender={setShowPluginRender}
77
+ showCustomPluginRender={false}
78
+ showDebug={showDebug}
79
+ showPluginRender={showPluginRender}
80
+ />
81
+ )
79
82
  }
80
83
  itemKey={'tool'}
81
84
  paddingBlock={4}
@@ -83,7 +86,7 @@ const Tool = memo<InspectorProps>(
83
86
  title={<Inspectors apiName={apiName} identifier={identifier} result={result} />}
84
87
  >
85
88
  <Flexbox gap={8} paddingBlock={8}>
86
- {showDebug && (
89
+ {showDebug && !disableEditing && (
87
90
  <Debug
88
91
  apiName={apiName}
89
92
  identifier={identifier}
@@ -96,6 +99,7 @@ const Tool = memo<InspectorProps>(
96
99
  <Render
97
100
  apiName={apiName}
98
101
  arguments={requestArgs}
102
+ disableEditing={disableEditing}
99
103
  identifier={identifier}
100
104
  messageId={messageId}
101
105
  result={result}
@@ -8,11 +8,12 @@ import { dataSelectors, useConversationStore } from '../../store';
8
8
  import Tool from './Tool';
9
9
 
10
10
  interface ToolMessageProps {
11
+ disableEditing?: boolean;
11
12
  id: string;
12
13
  index: number;
13
14
  }
14
15
 
15
- const ToolMessage = memo<ToolMessageProps>(({ id, index }) => {
16
+ const ToolMessage = memo<ToolMessageProps>(({ disableEditing, id, index }) => {
16
17
  const { t } = useTranslation('plugin');
17
18
  const item = useConversationStore(dataSelectors.getDbMessageById(id), isEqual) as UIChatMessage;
18
19
  const deleteToolMessage = useConversationStore((s) => s.deleteToolMessage);
@@ -29,17 +30,25 @@ const ToolMessage = memo<ToolMessageProps>(({ id, index }) => {
29
30
 
30
31
  return (
31
32
  <Flexbox gap={4} paddingBlock={12}>
32
- <Alert
33
- action={
34
- <Button loading={loading} onClick={handleDelete} size={'small'} type={'primary'}>
35
- {t('inspector.delete')}
36
- </Button>
37
- }
38
- title={t('inspector.orphanedToolCall')}
39
- type={'secondary'}
40
- />
33
+ {!disableEditing && (
34
+ <Alert
35
+ action={
36
+ <Button loading={loading} onClick={handleDelete} size={'small'} type={'primary'}>
37
+ {t('inspector.delete')}
38
+ </Button>
39
+ }
40
+ title={t('inspector.orphanedToolCall')}
41
+ type={'secondary'}
42
+ />
43
+ )}
41
44
  {item.plugin && (
42
- <Tool {...item.plugin} index={index} messageId={id} toolCallId={item.tool_call_id!} />
45
+ <Tool
46
+ {...item.plugin}
47
+ disableEditing={disableEditing}
48
+ index={index}
49
+ messageId={id}
50
+ toolCallId={item.tool_call_id!}
51
+ />
43
52
  )}
44
53
  </Flexbox>
45
54
  );
@@ -158,7 +158,7 @@ const MessageItem = memo<MessageItemProps>(
158
158
  }
159
159
 
160
160
  case 'tool': {
161
- return <ToolMessage id={id} index={index} />;
161
+ return <ToolMessage disableEditing={disableEditing} id={id} index={index} />;
162
162
  }
163
163
  }
164
164
 
@@ -505,6 +505,48 @@ describe('DataSlice', () => {
505
505
  );
506
506
  });
507
507
 
508
+ it('should not fetch when topicId is null (new conversation state)', () => {
509
+ const store = createStore({
510
+ context: { agentId: 'test-session', topicId: null, threadId: null },
511
+ });
512
+
513
+ store.getState().useFetchMessages({
514
+ agentId: 'test-session',
515
+ topicId: null,
516
+ threadId: null,
517
+ });
518
+
519
+ // SWR should be called with null key when topicId is null
520
+ // This prevents fetching empty data that would overwrite local optimistic updates
521
+ expect(vi.mocked(useClientDataSWRWithSync)).toHaveBeenCalledWith(
522
+ null,
523
+ expect.any(Function),
524
+ expect.any(Object),
525
+ );
526
+
527
+ // messageService.getMessages should NOT be called
528
+ expect(messageService.getMessages).not.toHaveBeenCalled();
529
+ });
530
+
531
+ it('should not fetch when topicId is undefined (new conversation state)', () => {
532
+ const store = createStore({
533
+ context: { agentId: 'test-session', topicId: null, threadId: null },
534
+ });
535
+
536
+ store.getState().useFetchMessages({
537
+ agentId: 'test-session',
538
+ topicId: undefined as any,
539
+ threadId: null,
540
+ });
541
+
542
+ // SWR should be called with null key when topicId is undefined
543
+ expect(vi.mocked(useClientDataSWRWithSync)).toHaveBeenCalledWith(
544
+ null,
545
+ expect.any(Function),
546
+ expect.any(Object),
547
+ );
548
+ });
549
+
508
550
  it('should use different SWR keys for different threadIds', () => {
509
551
  const store1 = createStore({
510
552
  context: { agentId: 'session-1', topicId: 'topic-1', threadId: 'thread-1' },
@@ -103,13 +103,15 @@ export const dataSlice: StateCreator<
103
103
  useFetchMessages: (context, skipFetch) => {
104
104
  // When skipFetch is true, SWR key is null - no fetch occurs
105
105
  // This is used when external messages are provided (e.g., creating new thread)
106
- const shouldFetch = !skipFetch && !!context.agentId;
106
+ // Also skip fetch when topicId is null (new conversation state) - there's no server data,
107
+ // only local optimistic updates. Fetching would return empty array and overwrite local data.
108
+ const shouldFetch = !skipFetch && !!context.agentId && !!context.topicId;
107
109
 
108
110
  return useClientDataSWRWithSync<UIChatMessage[]>(
109
111
  shouldFetch ? ['CONVERSATION_FETCH_MESSAGES', context] : null,
110
112
 
111
113
  async () => {
112
- return messageService.getMessages(context as any);
114
+ return messageService.getMessages(context);
113
115
  },
114
116
  {
115
117
  onData: (data) => {
@@ -12,10 +12,10 @@ import { useSessionStore } from '@/store/session';
12
12
  import { sessionSelectors } from '@/store/session/selectors';
13
13
 
14
14
  const Header = memo(() => {
15
- const togglePortal = useChatStore((s) => s.togglePortal);
15
+ const clearPortalStack = useChatStore((s) => s.clearPortalStack);
16
16
  const close = () => {
17
17
  useAgentGroupStore.setState({ activeThreadAgentId: '' });
18
- togglePortal(false);
18
+ clearPortalStack();
19
19
  };
20
20
  const activeThreadAgentId = useAgentGroupStore((s) => s.activeThreadAgentId);
21
21
 
@@ -15,9 +15,9 @@ const md = css`
15
15
  `;
16
16
 
17
17
  const MessageDetailBody = () => {
18
- const [messageDetailId, togglePortal] = useChatStore((s) => [
18
+ const [messageDetailId, clearPortalStack] = useChatStore((s) => [
19
19
  chatPortalSelectors.messageDetailId(s),
20
- s.togglePortal,
20
+ s.clearPortalStack,
21
21
  ]);
22
22
 
23
23
  const message = useChatStore(dbMessageSelectors.getDbMessageById(messageDetailId || ''), isEqual);
@@ -26,7 +26,7 @@ const MessageDetailBody = () => {
26
26
 
27
27
  useEffect(() => {
28
28
  if (!message) {
29
- togglePortal(false);
29
+ clearPortalStack();
30
30
  }
31
31
  }, [message]);
32
32
 
@@ -10,10 +10,10 @@ import { useChatStore } from '@/store/chat';
10
10
  import { chatPortalSelectors } from '@/store/chat/selectors';
11
11
 
12
12
  const Header = memo<{ title: ReactNode }>(({ title }) => {
13
- const [canGoBack, goBack, togglePortal] = useChatStore((s) => [
13
+ const [canGoBack, goBack, clearPortalStack] = useChatStore((s) => [
14
14
  chatPortalSelectors.canGoBack(s),
15
15
  s.goBack,
16
- s.togglePortal,
16
+ s.clearPortalStack,
17
17
  ]);
18
18
 
19
19
  return (
@@ -30,7 +30,7 @@ const Header = memo<{ title: ReactNode }>(({ title }) => {
30
30
  <ActionIcon
31
31
  icon={PanelRightCloseIcon}
32
32
  onClick={() => {
33
- togglePortal(false);
33
+ clearPortalStack();
34
34
  }}
35
35
  size={DESKTOP_HEADER_ICON_SIZE}
36
36
  />
@@ -16,7 +16,7 @@ import PluginStore from '@/features/PluginStore';
16
16
  import { useCheckPluginsIsInstalled } from '@/hooks/useCheckPluginsIsInstalled';
17
17
  import { useFetchInstalledPlugins } from '@/hooks/useFetchInstalledPlugins';
18
18
  import { useAgentStore } from '@/store/agent';
19
- import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
19
+ import { agentSelectors, chatConfigByIdSelectors } from '@/store/agent/selectors';
20
20
  import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
21
21
  import { useToolStore } from '@/store/tool';
22
22
  import {
@@ -82,6 +82,11 @@ const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label
82
82
  });
83
83
 
84
84
  export interface AgentToolProps {
85
+ /**
86
+ * Optional agent ID to use instead of currentAgentConfig
87
+ * Used in group profile to specify which member's plugins to display
88
+ */
89
+ agentId?: string;
85
90
  /**
86
91
  * Whether to filter tools by availableInWeb property
87
92
  * @default false
@@ -100,15 +105,17 @@ export interface AgentToolProps {
100
105
  }
101
106
 
102
107
  const AgentTool = memo<AgentToolProps>(
103
- ({ showWebBrowsing = false, filterAvailableInWeb = false, useAllMetaList = false }) => {
108
+ ({ agentId, showWebBrowsing = false, filterAvailableInWeb = false, useAllMetaList = false }) => {
104
109
  const { t } = useTranslation('setting');
105
- const config = useAgentStore(agentSelectors.currentAgentConfig, isEqual);
110
+ const activeAgentId = useAgentStore((s) => s.activeAgentId);
111
+ const effectiveAgentId = agentId || activeAgentId || '';
112
+ const config = useAgentStore(agentSelectors.getAgentConfigById(effectiveAgentId), isEqual);
106
113
 
107
114
  // Plugin state management
108
115
  const plugins = config?.plugins || [];
109
116
 
110
- const toggleAgentPlugin = useAgentStore((s) => s.toggleAgentPlugin);
111
- const updateAgentChatConfig = useAgentStore((s) => s.updateAgentChatConfig);
117
+ const updateAgentConfigById = useAgentStore((s) => s.updateAgentConfigById);
118
+ const updateAgentChatConfigById = useAgentStore((s) => s.updateAgentChatConfigById);
112
119
  const installedPluginList = useToolStore(pluginSelectors.installedPluginMetaList, isEqual);
113
120
 
114
121
  // Use appropriate builtin list based on prop
@@ -117,8 +124,8 @@ const AgentTool = memo<AgentToolProps>(
117
124
  isEqual,
118
125
  );
119
126
 
120
- // Web browsing uses searchMode instead of plugins array
121
- const isSearchEnabled = useAgentStore(agentChatConfigSelectors.isAgentEnableSearch);
127
+ // Web browsing uses searchMode instead of plugins array - use byId selector
128
+ const isSearchEnabled = useAgentStore(chatConfigByIdSelectors.isEnableSearchById(effectiveAgentId));
122
129
 
123
130
  // Klavis 相关状态
124
131
  const allKlavisServers = useToolStore(klavisStoreSelectors.getServers, isEqual);
@@ -144,11 +151,34 @@ const AgentTool = memo<AgentToolProps>(
144
151
  // 使用 SWR 加载用户的 Klavis 集成(从数据库)
145
152
  useFetchUserKlavisServers(isKlavisEnabledInEnv);
146
153
 
147
- // Toggle web browsing via searchMode
154
+ // Toggle web browsing via searchMode - use byId action
148
155
  const toggleWebBrowsing = useCallback(async () => {
156
+ if (!effectiveAgentId) return;
149
157
  const nextMode = isSearchEnabled ? 'off' : 'auto';
150
- await updateAgentChatConfig({ searchMode: nextMode });
151
- }, [isSearchEnabled, updateAgentChatConfig]);
158
+ await updateAgentChatConfigById(effectiveAgentId, { searchMode: nextMode });
159
+ }, [isSearchEnabled, updateAgentChatConfigById, effectiveAgentId]);
160
+
161
+ // Toggle a plugin - use byId action
162
+ const togglePlugin = useCallback(
163
+ async (pluginId: string, state?: boolean) => {
164
+ if (!effectiveAgentId) return;
165
+ const currentPlugins = plugins;
166
+ const hasPlugin = currentPlugins.includes(pluginId);
167
+ const shouldEnable = state !== undefined ? state : !hasPlugin;
168
+
169
+ let newPlugins: string[];
170
+ if (shouldEnable && !hasPlugin) {
171
+ newPlugins = [...currentPlugins, pluginId];
172
+ } else if (!shouldEnable && hasPlugin) {
173
+ newPlugins = currentPlugins.filter((id) => id !== pluginId);
174
+ } else {
175
+ return;
176
+ }
177
+
178
+ await updateAgentConfigById(effectiveAgentId, { plugins: newPlugins });
179
+ },
180
+ [effectiveAgentId, plugins, updateAgentConfigById],
181
+ );
152
182
 
153
183
  // Check if a tool is enabled (handles web browsing specially)
154
184
  const isToolEnabled = useCallback(
@@ -167,10 +197,10 @@ const AgentTool = memo<AgentToolProps>(
167
197
  if (showWebBrowsing && identifier === WEB_BROWSING_IDENTIFIER) {
168
198
  await toggleWebBrowsing();
169
199
  } else {
170
- await toggleAgentPlugin(identifier);
200
+ await togglePlugin(identifier);
171
201
  }
172
202
  },
173
- [toggleWebBrowsing, toggleAgentPlugin, showWebBrowsing],
203
+ [toggleWebBrowsing, togglePlugin, showWebBrowsing],
174
204
  );
175
205
 
176
206
  // Set default tab based on installed plugins (only on first load)
@@ -240,7 +270,7 @@ const AgentTool = memo<AgentToolProps>(
240
270
  [isKlavisEnabledInEnv, allKlavisServers],
241
271
  );
242
272
 
243
- // Handle plugin remove via Tag close
273
+ // Handle plugin remove via Tag close - use byId actions
244
274
  const handleRemovePlugin =
245
275
  (
246
276
  pluginId: string | { enabled: boolean; identifier: string; settings: Record<string, any> },
@@ -250,9 +280,10 @@ const AgentTool = memo<AgentToolProps>(
250
280
  e.stopPropagation();
251
281
  const identifier = typeof pluginId === 'string' ? pluginId : pluginId?.identifier;
252
282
  if (showWebBrowsing && identifier === WEB_BROWSING_IDENTIFIER) {
253
- await updateAgentChatConfig({ searchMode: 'off' });
283
+ if (!effectiveAgentId) return;
284
+ await updateAgentChatConfigById(effectiveAgentId, { searchMode: 'off' });
254
285
  } else {
255
- toggleAgentPlugin(identifier, false);
286
+ await togglePlugin(identifier, false);
256
287
  }
257
288
  };
258
289
 
@@ -304,13 +335,13 @@ const AgentTool = memo<AgentToolProps>(
304
335
  label={item.title}
305
336
  onUpdate={async () => {
306
337
  setUpdating(true);
307
- await toggleAgentPlugin(item.identifier);
338
+ await togglePlugin(item.identifier);
308
339
  setUpdating(false);
309
340
  }}
310
341
  />
311
342
  ),
312
343
  })),
313
- [installedPluginList, plugins, toggleAgentPlugin],
344
+ [installedPluginList, plugins, togglePlugin],
314
345
  );
315
346
 
316
347
  // All tab items (市场 tab)
@@ -411,7 +442,7 @@ const AgentTool = memo<AgentToolProps>(
411
442
  label={item.title}
412
443
  onUpdate={async () => {
413
444
  setUpdating(true);
414
- await toggleAgentPlugin(item.identifier);
445
+ await togglePlugin(item.identifier);
415
446
  setUpdating(false);
416
447
  }}
417
448
  />
@@ -435,7 +466,7 @@ const AgentTool = memo<AgentToolProps>(
435
466
  plugins,
436
467
  isToolEnabled,
437
468
  handleToggleTool,
438
- toggleAgentPlugin,
469
+ togglePlugin,
439
470
  t,
440
471
  ]);
441
472