@lobehub/lobehub 2.0.0-next.312 → 2.0.0-next.313

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 (75) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/apps/desktop/src/main/controllers/AuthCtr.ts +75 -7
  3. package/changelog/v1.json +9 -0
  4. package/docs/usage/providers/internlm.mdx +2 -2
  5. package/docs/usage/providers/internlm.zh-CN.mdx +3 -3
  6. package/locales/en-US/error.json +10 -1
  7. package/locales/en-US/subscription.json +1 -1
  8. package/locales/zh-CN/desktop-onboarding.json +5 -0
  9. package/locales/zh-CN/error.json +10 -1
  10. package/locales/zh-CN/subscription.json +1 -1
  11. package/package.json +1 -1
  12. package/packages/agent-runtime/src/agents/GeneralChatAgent.ts +14 -2
  13. package/packages/agent-runtime/src/agents/__tests__/GeneralChatAgent.test.ts +275 -1
  14. package/packages/builtin-tool-cloud-sandbox/package.json +1 -0
  15. package/packages/builtin-tool-cloud-sandbox/src/ExecutionRuntime/index.ts +105 -134
  16. package/packages/builtin-tool-cloud-sandbox/src/executor/index.ts +254 -0
  17. package/packages/builtin-tool-cloud-sandbox/src/index.ts +1 -0
  18. package/packages/builtin-tool-cloud-sandbox/src/types/api.ts +22 -0
  19. package/packages/builtin-tool-cloud-sandbox/src/types/index.ts +4 -0
  20. package/packages/builtin-tool-cloud-sandbox/src/types/params.ts +85 -0
  21. package/packages/builtin-tool-cloud-sandbox/src/types/service.ts +48 -0
  22. package/packages/builtin-tool-cloud-sandbox/src/{types.ts → types/state.ts} +0 -23
  23. package/packages/builtin-tool-memory/src/manifest.ts +5 -5
  24. package/packages/editor-runtime/src/__tests__/EditorRuntime.real.test.ts +1 -1
  25. package/packages/editor-runtime/src/__tests__/EditorRuntime.test.ts +1 -1
  26. package/packages/electron-client-ipc/src/events/index.ts +5 -1
  27. package/packages/electron-client-ipc/src/events/remoteServer.ts +23 -0
  28. package/packages/memory-user-memory/src/schemas/index.ts +0 -1
  29. package/packages/model-bank/src/modelProviders/internlm.ts +1 -1
  30. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +5 -15
  31. package/packages/model-runtime/src/providers/internlm/index.test.ts +15 -15
  32. package/packages/model-runtime/src/providers/internlm/index.ts +1 -1
  33. package/packages/types/src/tool/intervention.ts +4 -2
  34. package/packages/types/src/user/preference.ts +1 -0
  35. package/src/app/[variants]/(desktop)/desktop-onboarding/features/LoginStep.tsx +84 -26
  36. package/src/app/[variants]/(main)/_layout/DesktopAutoOidcOnFirstOpen.tsx +4 -0
  37. package/src/business/server/user.ts +4 -0
  38. package/src/features/Conversation/Messages/Task/Actions/index.tsx +0 -2
  39. package/src/features/Conversation/Messages/Task/index.tsx +1 -1
  40. package/src/features/Conversation/Messages/Tasks/shared/ProcessingState.tsx +0 -2
  41. package/src/features/NavPanel/components/NavPanelDraggable.tsx +0 -14
  42. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +4 -3
  43. package/src/features/SharePopover/index.tsx +5 -3
  44. package/src/hooks/useAppOrigin.ts +16 -0
  45. package/src/layout/GlobalProvider/useUserStateRedirect.ts +37 -24
  46. package/src/libs/trusted-client/index.ts +2 -5
  47. package/src/locales/default/desktop-onboarding.ts +5 -0
  48. package/src/locales/default/error.ts +11 -0
  49. package/src/locales/default/subscription.ts +1 -1
  50. package/src/server/modules/AgentRuntime/RuntimeExecutors.ts +2 -0
  51. package/src/server/routers/lambda/user.ts +24 -10
  52. package/src/server/services/agentRuntime/AgentRuntimeService.test.ts +3 -0
  53. package/src/server/services/agentRuntime/AgentRuntimeService.ts +8 -5
  54. package/src/server/services/agentRuntime/types.ts +7 -0
  55. package/src/server/services/aiAgent/__tests__/execGroupSubAgentTask.test.ts +3 -0
  56. package/src/server/services/aiAgent/index.ts +10 -4
  57. package/src/server/services/market/index.ts +7 -0
  58. package/src/server/services/sandbox/index.ts +120 -0
  59. package/src/server/services/toolExecution/builtin.ts +12 -18
  60. package/src/server/services/toolExecution/index.ts +1 -1
  61. package/src/server/services/toolExecution/serverRuntimes/cloudSandbox.ts +31 -0
  62. package/src/server/services/toolExecution/serverRuntimes/index.ts +55 -0
  63. package/src/server/services/toolExecution/serverRuntimes/types.ts +14 -0
  64. package/src/server/services/toolExecution/serverRuntimes/webBrowsing.ts +20 -0
  65. package/src/server/services/toolExecution/types.ts +2 -0
  66. package/src/services/{codeInterpreter.ts → cloudSandbox.ts} +3 -3
  67. package/src/services/electron/remoteServer.ts +8 -0
  68. package/src/store/chat/agents/GroupOrchestration/__tests__/batch-exec-async-tasks.test.ts +626 -0
  69. package/src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.ts +294 -0
  70. package/src/store/chat/slices/plugin/action.test.ts +0 -48
  71. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +0 -131
  72. package/src/store/tool/slices/builtin/executors/index.ts +2 -0
  73. package/src/store/user/slices/settings/selectors/toolIntervention.test.ts +143 -0
  74. package/src/store/user/slices/settings/selectors/toolIntervention.ts +11 -2
  75. package/packages/memory-user-memory/src/schemas/jsonSchemas.ts +0 -37
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { memo, useEffect } from 'react';
4
4
 
5
+ import { getDesktopOnboardingCompleted } from '@/app/[variants]/(desktop)/desktop-onboarding/storage';
5
6
  import { useElectronStore } from '@/store/electron';
6
7
  import {
7
8
  getDesktopAutoOidcFirstOpenHandled,
@@ -28,6 +29,9 @@ const DesktopAutoOidcOnFirstOpen = memo(() => {
28
29
  useEffect(() => {
29
30
  if (!isInitRemoteServerConfig) return;
30
31
 
32
+ // Don't auto-trigger during onboarding flow.
33
+ if (!getDesktopOnboardingCompleted()) return;
34
+
31
35
  // If already connected or not in cloud mode, don't auto-trigger.
32
36
  if (dataSyncConfig.active) return;
33
37
  if (dataSyncConfig.storageMode !== 'cloud') return;
@@ -13,6 +13,10 @@ export async function getIsInWaitList(userId: string): Promise<boolean> {
13
13
  return false;
14
14
  }
15
15
 
16
+ export async function getIsInviteCodeRequired(userId: string): Promise<boolean> {
17
+ return false;
18
+ }
19
+
16
20
  export async function initNewUserForBusiness(
17
21
  userId: string,
18
22
  createdAt: Date | null | undefined,
@@ -128,8 +128,6 @@ export const AssistantActionsBar = memo<AssistantActionsBarProps>(
128
128
  defaultActions.edit,
129
129
  defaultActions.copy,
130
130
  collapseAction,
131
- defaultActions.divider,
132
-
133
131
  defaultActions.divider,
134
132
  defaultActions.share,
135
133
  defaultActions.divider,
@@ -43,7 +43,7 @@ const TaskMessage = memo<TaskMessageProps>(({ id, index, disableEditing, isLates
43
43
  const editing = useConversationStore(messageStateSelectors.isMessageEditing(id));
44
44
  const generating = useConversationStore(messageStateSelectors.isMessageGenerating(id));
45
45
  const creating = useConversationStore(messageStateSelectors.isMessageCreating(id));
46
- const newScreen = useNewScreen({ creating, isLatestItem });
46
+ const newScreen = useNewScreen({ creating: generating || creating, isLatestItem });
47
47
 
48
48
  const errorContent = useErrorContent(error);
49
49
 
@@ -28,9 +28,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
28
28
  display: flex;
29
29
  gap: 8px;
30
30
  align-items: center;
31
-
32
31
  padding-block: 8px;
33
- padding-inline: 16px;
34
32
  `,
35
33
  footer: css`
36
34
  padding-block-start: 8px;
@@ -13,7 +13,6 @@ import { systemStatusSelectors } from '@/store/global/selectors';
13
13
  import { isMacOS } from '@/utils/platform';
14
14
 
15
15
  import { useNavPanelSizeChangeHandler } from '../hooks/useNavPanel';
16
- import { BACK_BUTTON_ID } from './BackButton';
17
16
 
18
17
  const motionVariants = {
19
18
  animate: { opacity: 1, x: 0 },
@@ -78,12 +77,6 @@ const draggableStyles = createStaticStyles(({ css, cssVar }) => ({
78
77
  width 0.2s ${cssVar.motionEaseOut};
79
78
  }
80
79
 
81
- #${BACK_BUTTON_ID} {
82
- width: 0 !important;
83
- opacity: 0;
84
- transition: all 0.2s ${cssVar.motionEaseOut};
85
- }
86
-
87
80
  &:hover {
88
81
  #${TOGGLE_BUTTON_ID} {
89
82
  width: 32px !important;
@@ -94,13 +87,6 @@ const draggableStyles = createStaticStyles(({ css, cssVar }) => ({
94
87
  width: 14px !important;
95
88
  opacity: 1;
96
89
  }
97
-
98
- &:hover {
99
- #${BACK_BUTTON_ID} {
100
- width: 24px !important;
101
- opacity: 1;
102
- }
103
- }
104
90
  }
105
91
  `,
106
92
  }));
@@ -17,6 +17,7 @@ import { shallow } from 'zustand/shallow';
17
17
  import RepoIcon from '@/components/LibIcon';
18
18
  import { clearTreeFolderCache } from '@/features/ResourceManager/components/LibraryHierarchy';
19
19
  import { PAGE_FILE_TYPE } from '@/features/ResourceManager/constants';
20
+ import { useAppOrigin } from '@/hooks/useAppOrigin';
20
21
  import { documentService } from '@/services/document';
21
22
  import { useFileStore } from '@/store/file';
22
23
  import { useKnowledgeBaseStore } from '@/store/library';
@@ -53,6 +54,7 @@ export const useFileItemDropdown = ({
53
54
  }: UseFileItemDropdownParams): UseFileItemDropdownReturn => {
54
55
  const { t } = useTranslation(['components', 'common', 'knowledgeBase']);
55
56
  const { message, modal } = App.useApp();
57
+ const appOrigin = useAppOrigin();
56
58
 
57
59
  const { deleteResource, refreshFileList } = useFileStore(
58
60
  (s) => ({
@@ -184,11 +186,10 @@ export const useFileItemDropdown = ({
184
186
  // For pages, use the route path instead of the storage URL
185
187
  let urlToCopy = url;
186
188
  if (isPage) {
187
- const baseUrl = window.location.origin;
188
189
  if (libraryId) {
189
- urlToCopy = `${baseUrl}/resource/library/${libraryId}?file=${id}`;
190
+ urlToCopy = `${appOrigin}/resource/library/${libraryId}?file=${id}`;
190
191
  } else {
191
- urlToCopy = `${baseUrl}/resource?file=${id}`;
192
+ urlToCopy = `${appOrigin}/resource?file=${id}`;
192
193
  }
193
194
  }
194
195
 
@@ -2,11 +2,12 @@
2
2
 
3
3
  import { Button, Flexbox, Popover, copyToClipboard, usePopoverContext } from '@lobehub/ui';
4
4
  import { App, Divider, Select, Skeleton, Typography } from 'antd';
5
- import { CopyIcon, ExternalLinkIcon, LinkIcon, LockIcon } from 'lucide-react';
5
+ import { ExternalLinkIcon, LinkIcon, LockIcon } from 'lucide-react';
6
6
  import { type ReactNode, memo, useCallback, useEffect, useRef, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import useSWR from 'swr';
9
9
 
10
+ import { useAppOrigin } from '@/hooks/useAppOrigin';
10
11
  import { useIsMobile } from '@/hooks/useIsMobile';
11
12
  import { topicService } from '@/services/topic';
12
13
  import { useChatStore } from '@/store/chat';
@@ -26,6 +27,7 @@ const SharePopoverContent = memo<SharePopoverContentProps>(({ onOpenModal }) =>
26
27
  const [updating, setUpdating] = useState(false);
27
28
  const { close } = usePopoverContext();
28
29
  const containerRef = useRef<HTMLDivElement>(null);
30
+ const appOrigin = useAppOrigin();
29
31
 
30
32
  const activeTopicId = useChatStore((s) => s.activeTopicId);
31
33
 
@@ -46,7 +48,7 @@ const SharePopoverContent = memo<SharePopoverContentProps>(({ onOpenModal }) =>
46
48
  }
47
49
  }, [isLoading, shareInfo, activeTopicId, mutate]);
48
50
 
49
- const shareUrl = shareInfo?.id ? `${window.location.origin}/share/t/${shareInfo.id}` : '';
51
+ const shareUrl = shareInfo?.id ? `${appOrigin}/share/t/${shareInfo.id}` : '';
50
52
  const currentVisibility = (shareInfo?.visibility as Visibility) || 'private';
51
53
 
52
54
  const updateVisibility = useCallback(
@@ -178,7 +180,7 @@ const SharePopoverContent = memo<SharePopoverContentProps>(({ onOpenModal }) =>
178
180
  >
179
181
  {t('shareModal.popover.moreOptions')}
180
182
  </Button>
181
- <Button icon={CopyIcon} onClick={handleCopyLink} size="small" type="primary">
183
+ <Button icon={LinkIcon} onClick={handleCopyLink} size="small" type="primary">
182
184
  {t('shareModal.copyLink')}
183
185
  </Button>
184
186
  </Flexbox>
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ import { isDesktop } from '@lobechat/const';
4
+
5
+ import { useElectronStore } from '@/store/electron';
6
+ import { electronSyncSelectors } from '@/store/electron/selectors';
7
+
8
+ /**
9
+ * Returns the correct app origin URL for sharing/linking.
10
+ * - Web: uses window.location.origin
11
+ * - Desktop: uses remoteServerUrl from electron store
12
+ */
13
+ export const useAppOrigin = () => {
14
+ const remoteServerUrl = useElectronStore(electronSyncSelectors.remoteServerUrl);
15
+ return isDesktop ? remoteServerUrl : window.location.origin;
16
+ };
@@ -20,43 +20,51 @@ export const useDesktopUserStateRedirect = () => {
20
20
  const dataSyncConfig = useElectronStore((s) => s.dataSyncConfig);
21
21
  const logout = useUserStore((s) => s.logout);
22
22
 
23
- const handleDesktopWaitlist = useCallback(async () => {
24
- const waitlistBaseUrl = dataSyncConfig.remoteServerUrl || OFFICIAL_URL;
25
- let waitlistUrl = waitlistBaseUrl;
26
- try {
27
- waitlistUrl = new URL('/waitlist', waitlistBaseUrl).toString();
28
- } catch {
29
- // Ignore: keep fallback URL for external open attempt.
30
- }
23
+ const openExternalAndLogout = useCallback(
24
+ async (path: string) => {
25
+ const baseUrl = dataSyncConfig.remoteServerUrl || OFFICIAL_URL;
26
+ let targetUrl = baseUrl;
27
+ try {
28
+ targetUrl = new URL(path, baseUrl).toString();
29
+ } catch {
30
+ // Ignore: keep fallback URL for external open attempt.
31
+ }
31
32
 
32
- try {
33
- const { electronSystemService } = await import('@/services/electron/system');
34
- await electronSystemService.openExternalLink(waitlistUrl);
35
- } catch {
36
- // Ignore: fallback to logout flow even if IPC is unavailable.
37
- }
33
+ try {
34
+ const { electronSystemService } = await import('@/services/electron/system');
35
+ await electronSystemService.openExternalLink(targetUrl);
36
+ } catch {
37
+ // Ignore: fallback to logout flow even if IPC is unavailable.
38
+ }
38
39
 
39
- try {
40
- const { remoteServerService } = await import('@/services/electron/remoteServer');
41
- await remoteServerService.clearRemoteServerConfig();
42
- } catch {
43
- // Ignore: fallback to logout flow even if IPC is unavailable.
44
- }
40
+ try {
41
+ const { remoteServerService } = await import('@/services/electron/remoteServer');
42
+ await remoteServerService.clearRemoteServerConfig();
43
+ } catch {
44
+ // Ignore: fallback to logout flow even if IPC is unavailable.
45
+ }
45
46
 
46
- await logout();
47
- }, [dataSyncConfig.remoteServerUrl, logout]);
47
+ await logout();
48
+ },
49
+ [dataSyncConfig.remoteServerUrl, logout],
50
+ );
48
51
 
49
52
  return useCallback(
50
53
  (state: UserInitializationState) => {
51
54
  if (state.isInWaitList === true) {
52
- void handleDesktopWaitlist();
55
+ void openExternalAndLogout('/waitlist');
56
+ return;
57
+ }
58
+
59
+ if (state.isInviteCodeRequired === true) {
60
+ void openExternalAndLogout('/invite-code');
53
61
  return;
54
62
  }
55
63
 
56
64
  if (!getDesktopOnboardingCompleted()) return;
57
65
  // Desktop onboarding is handled by desktop-only flow.
58
66
  },
59
- [handleDesktopWaitlist],
67
+ [openExternalAndLogout],
60
68
  );
61
69
  };
62
70
 
@@ -68,6 +76,11 @@ export const useWebUserStateRedirect = () =>
68
76
  return;
69
77
  }
70
78
 
79
+ if (state.isInviteCodeRequired === true) {
80
+ redirectIfNotOn(pathname, '/invite-code');
81
+ return;
82
+ }
83
+
71
84
  if (!onboardingSelectors.needsOnboarding(state)) return;
72
85
 
73
86
  redirectIfNotOn(pathname, '/onboarding');
@@ -31,14 +31,11 @@ export const generateTrustedClientToken = (userInfo: TrustedClientUserInfo): str
31
31
  return undefined;
32
32
  }
33
33
 
34
- if (!userInfo.email) {
35
- return undefined;
36
- }
37
-
38
34
  try {
39
35
  const payload = buildTrustedClientPayload({
40
36
  clientId: MARKET_TRUSTED_CLIENT_ID,
41
- email: userInfo.email,
37
+ // TODO: remove '' when sdk update
38
+ email: userInfo.email || '',
42
39
  name: userInfo.name,
43
40
  userId: userInfo.userId,
44
41
  });
@@ -73,6 +73,7 @@ export default {
73
73
  'screen4.title2': 'Your choice helps us improve',
74
74
  'screen4.title3': 'You can change this anytime in settings',
75
75
 
76
+ 'screen5.actions.cancel': 'Cancel',
76
77
  'screen5.actions.connectToServer': 'Connect to Server',
77
78
  'screen5.actions.connecting': 'Connecting...',
78
79
  'screen5.actions.signInCloud': 'Sign in to LobeHub Cloud',
@@ -80,6 +81,10 @@ export default {
80
81
  'screen5.actions.signingIn': 'Signing in...',
81
82
  'screen5.actions.signingOut': 'Signing out...',
82
83
  'screen5.actions.tryAgain': 'Try Again',
84
+ 'screen5.auth.phase.browserOpened': 'Browser opened, please sign in...',
85
+ 'screen5.auth.phase.verifying': 'Verifying credentials...',
86
+ 'screen5.auth.phase.waitingForAuth': 'Waiting for authorization...',
87
+ 'screen5.auth.remaining': 'Remaining: {{time}}s',
83
88
  'screen5.badge': 'Sign in',
84
89
  'screen5.description':
85
90
  'Sign in to sync Agents, Groups, settings, and Context across all devices.',
@@ -16,6 +16,12 @@ export default {
16
16
  'import.incompatible.description':
17
17
  'This file was exported from a higher version. Please try upgrading to the latest version and then re-importing.',
18
18
  'import.incompatible.title': 'Current application does not support importing this file',
19
+ 'inviteCode.currentEmail': 'Current account: {{email}}',
20
+ 'inviteCode.desc':
21
+ 'An invite code is required to access LobeHub. Please enter a valid invite code to continue.',
22
+ 'inviteCode.friends': 'Friends',
23
+ 'inviteCode.getCodeHint': 'Get an invite code from:',
24
+ 'inviteCode.title': 'Invite Code Required',
19
25
  'loginRequired.desc': 'You will be redirected to the login page shortly',
20
26
  'loginRequired.title': 'Please log in to use this feature',
21
27
  'notFound.backHome': 'Back to Home',
@@ -238,5 +244,10 @@ export default {
238
244
  'upload.title': 'File upload failed. Please check your network connection or try again later',
239
245
  'upload.unknownError': 'Error reason: {{reason}}',
240
246
  'upload.uploadFailed': 'File upload failed.',
247
+ 'waitlist.currentEmail': 'Current account: {{email}}',
248
+ 'waitlist.desc':
249
+ 'Your account is not on the whitelist. Please contact the administrator to request access.',
250
+ 'waitlist.switchAccount': 'Switch Account',
251
+ 'waitlist.title': 'Access Restricted',
241
252
  ...businessErrorsLocales,
242
253
  };
@@ -328,7 +328,7 @@ export default {
328
328
  'referral.rules.backfill.expiredTip':
329
329
  'Backfill period has expired. Cannot backfill after 3 days of registration',
330
330
  'referral.rules.backfill.link': 'Backfill Invite Code',
331
- 'referral.rules.backfill.placeholder': 'Enter invite code',
331
+ 'referral.rules.backfill.placeholder': 'Enter invite code or link',
332
332
  'referral.rules.backfill.submit': 'Confirm Binding',
333
333
  'referral.rules.backfill.success': 'Invite code bound successfully',
334
334
  'referral.rules.backfill.title': 'Backfill Invite Code',
@@ -36,6 +36,7 @@ export interface RuntimeExecutorContext {
36
36
  stepIndex: number;
37
37
  streamManager: IStreamEventManager;
38
38
  toolExecutionService: ToolExecutionService;
39
+ topicId?: string;
39
40
  userId?: string;
40
41
  }
41
42
 
@@ -475,6 +476,7 @@ export const createRuntimeExecutors = (
475
476
  const executionResult = await toolExecutionService.executeTool(chatToolPayload, {
476
477
  serverDB: ctx.serverDB,
477
478
  toolManifestMap: state.toolManifestMap,
479
+ topicId: ctx.topicId,
478
480
  userId: ctx.userId,
479
481
  });
480
482
 
@@ -16,7 +16,12 @@ import { after } from 'next/server';
16
16
  import { v4 as uuidv4 } from 'uuid';
17
17
  import { z } from 'zod';
18
18
 
19
- import { getIsInWaitList, getReferralStatus, getSubscriptionPlan } from '@/business/server/user';
19
+ import {
20
+ getIsInviteCodeRequired,
21
+ getIsInWaitList,
22
+ getReferralStatus,
23
+ getSubscriptionPlan,
24
+ } from '@/business/server/user';
20
25
  import { MessageModel } from '@/database/models/message';
21
26
  import { SessionModel } from '@/database/models/session';
22
27
  import { UserModel, UserNotFoundError } from '@/database/models/user';
@@ -129,15 +134,23 @@ export const userRouter = router({
129
134
  };
130
135
 
131
136
  // Run user state fetch and count queries in parallel
132
- const [state, messageCount, hasExtraSession, referralStatus, subscriptionPlan, isInWaitList] =
133
- await Promise.all([
134
- getOrCreateUserState(),
135
- ctx.messageModel.countUpTo(5),
136
- ctx.sessionModel.hasMoreThanN(1),
137
- getReferralStatus(ctx.userId),
138
- getSubscriptionPlan(ctx.userId),
139
- getIsInWaitList(ctx.userId),
140
- ]);
137
+ const [
138
+ state,
139
+ messageCount,
140
+ hasExtraSession,
141
+ referralStatus,
142
+ subscriptionPlan,
143
+ isInWaitList,
144
+ isInviteCodeRequired,
145
+ ] = await Promise.all([
146
+ getOrCreateUserState(),
147
+ ctx.messageModel.countUpTo(5),
148
+ ctx.sessionModel.hasMoreThanN(1),
149
+ getReferralStatus(ctx.userId),
150
+ getSubscriptionPlan(ctx.userId),
151
+ getIsInWaitList(ctx.userId),
152
+ getIsInviteCodeRequired(ctx.userId),
153
+ ]);
141
154
 
142
155
  const hasMoreThan4Messages = messageCount > 4;
143
156
  const hasAnyMessages = messageCount > 0;
@@ -168,6 +181,7 @@ export const userRouter = router({
168
181
  referralStatus,
169
182
  subscriptionPlan,
170
183
  isInWaitList,
184
+ isInviteCodeRequired,
171
185
  isFreePlan: !subscriptionPlan || subscriptionPlan === Plans.Free,
172
186
  } satisfies UserInitializationState;
173
187
  /* eslint-enable sort-keys-fix/sort-keys-fix */
@@ -40,6 +40,9 @@ vi.mock('@/server/modules/ModelRuntime', () => ({
40
40
 
41
41
  // Mock search service to avoid server-side env access
42
42
  vi.mock('@/server/services/search', () => ({
43
+ SearchService: vi.fn().mockImplementation(() => ({
44
+ search: vi.fn(),
45
+ })),
43
46
  searchService: {
44
47
  search: vi.fn(),
45
48
  },
@@ -233,6 +233,7 @@ export class AgentRuntimeService {
233
233
  toolManifestMap,
234
234
  toolSourceMap,
235
235
  stepCallbacks,
236
+ userInterventionConfig,
236
237
  } = params;
237
238
 
238
239
  try {
@@ -261,6 +262,8 @@ export class AgentRuntimeService {
261
262
  toolManifestMap,
262
263
  toolSourceMap,
263
264
  tools,
265
+ // User intervention config for headless mode in async tasks
266
+ userInterventionConfig,
264
267
  } as Partial<AgentState>;
265
268
 
266
269
  // Use coordinator to create operation, automatically sends initialization event
@@ -329,10 +332,7 @@ export class AgentRuntimeService {
329
332
  });
330
333
 
331
334
  // Get operation state and metadata
332
- const [agentState, operationMetadata] = await Promise.all([
333
- this.coordinator.loadAgentState(operationId),
334
- this.coordinator.getOperationMetadata(operationId),
335
- ]);
335
+ const agentState = await this.coordinator.loadAgentState(operationId);
336
336
 
337
337
  if (!agentState) {
338
338
  throw new Error(`Agent state not found for operation ${operationId}`);
@@ -353,8 +353,10 @@ export class AgentRuntimeService {
353
353
  }
354
354
 
355
355
  // Create Agent and Runtime instances
356
+ // Use agentState.metadata which contains the full app context (topicId, agentId, etc.)
357
+ // operationMetadata only contains basic fields (agentConfig, modelRuntimeConfig, userId)
356
358
  const { runtime } = await this.createAgentRuntime({
357
- metadata: operationMetadata,
359
+ metadata: agentState?.metadata,
358
360
  operationId,
359
361
  stepIndex,
360
362
  });
@@ -850,6 +852,7 @@ export class AgentRuntimeService {
850
852
  stepIndex,
851
853
  streamManager: this.streamManager,
852
854
  toolExecutionService: this.toolExecutionService,
855
+ topicId: metadata?.topicId,
853
856
  userId: metadata?.userId,
854
857
  };
855
858
 
@@ -1,5 +1,6 @@
1
1
  import { type AgentRuntimeContext, type AgentState } from '@lobechat/agent-runtime';
2
2
  import { type LobeToolManifest } from '@lobechat/context-engine';
3
+ import type { UserInterventionConfig } from '@lobechat/types';
3
4
 
4
5
  // ==================== Step Lifecycle Callbacks ====================
5
6
 
@@ -90,6 +91,12 @@ export interface OperationCreationParams {
90
91
  toolSourceMap?: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis' | 'lobehubSkill'>;
91
92
  tools?: any[];
92
93
  userId?: string;
94
+ /**
95
+ * User intervention configuration
96
+ * Controls how tools requiring approval are handled
97
+ * Use { approvalMode: 'headless' } for async tasks that should never wait for human approval
98
+ */
99
+ userInterventionConfig?: UserInterventionConfig;
93
100
  }
94
101
 
95
102
  export interface OperationCreationResult {
@@ -185,6 +185,9 @@ describe('AiAgentService.execSubAgentTask', () => {
185
185
  onAfterStep: expect.any(Function),
186
186
  onComplete: expect.any(Function),
187
187
  }),
188
+ userInterventionConfig: {
189
+ approvalMode: 'headless',
190
+ },
188
191
  });
189
192
  });
190
193
 
@@ -8,6 +8,7 @@ import type {
8
8
  ExecGroupAgentResult,
9
9
  ExecSubAgentTaskParams,
10
10
  ExecSubAgentTaskResult,
11
+ UserInterventionConfig,
11
12
  } from '@lobechat/types';
12
13
  import { ThreadStatus, ThreadType } from '@lobechat/types';
13
14
  import { nanoid } from '@lobechat/utils';
@@ -66,6 +67,11 @@ interface InternalExecAgentParams extends ExecAgentParams {
66
67
  stepCallbacks?: StepLifecycleCallbacks;
67
68
  /** Topic creation trigger source ('cron' | 'chat' | 'api') */
68
69
  trigger?: string;
70
+ /**
71
+ * User intervention configuration
72
+ * Use { approvalMode: 'headless' } for async tasks that should never wait for human approval
73
+ */
74
+ userInterventionConfig?: UserInterventionConfig;
69
75
  }
70
76
 
71
77
  /**
@@ -125,6 +131,7 @@ export class AiAgentService {
125
131
  stepCallbacks,
126
132
  trigger,
127
133
  cronJobId,
134
+ userInterventionConfig,
128
135
  } = params;
129
136
 
130
137
  // Validate that either agentId or slug is provided
@@ -231,10 +238,6 @@ export class AiAgentService {
231
238
 
232
239
  const tools = toolsResult.tools;
233
240
 
234
- // Log detailed tools generation result
235
- if (toolsResult.filteredTools && toolsResult.filteredTools.length > 0) {
236
- log('execAgent: filtered tools: %O', toolsResult.filteredTools);
237
- }
238
241
  log('execAgent: enabled tool ids: %O', toolsResult.enabledToolIds);
239
242
 
240
243
  // Get manifest map and convert from Map to Record
@@ -396,6 +399,7 @@ export class AiAgentService {
396
399
  toolSourceMap,
397
400
  tools,
398
401
  userId: this.userId,
402
+ userInterventionConfig,
399
403
  });
400
404
 
401
405
  log('execAgent: created operation %s (autoStarted: %s)', operationId, result.autoStarted);
@@ -572,12 +576,14 @@ export class AiAgentService {
572
576
 
573
577
  // 4. Delegate to execAgent with threadId in appContext and callbacks
574
578
  // The instruction will be created as user message in the Thread
579
+ // Use headless mode to skip human approval in async task execution
575
580
  const result = await this.execAgent({
576
581
  agentId,
577
582
  appContext: { groupId, threadId: thread.id, topicId },
578
583
  autoStart: true,
579
584
  prompt: instruction,
580
585
  stepCallbacks,
586
+ userInterventionConfig: { approvalMode: 'headless' },
581
587
  });
582
588
 
583
589
  log(
@@ -368,6 +368,13 @@ export class MarketService {
368
368
  return this.market.user.getUserInfo(username, options);
369
369
  }
370
370
 
371
+ /**
372
+ * Register user on market and optionally follow another user
373
+ */
374
+ async registerUser(params: { followUserId?: string; registerUserId: string }): Promise<void> {
375
+ await this.market.user.register(params);
376
+ }
377
+
371
378
  // ============================== Skills Methods ==============================
372
379
 
373
380
  /**