mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
✨ feat: support runtime config (#12902)
* feat: support runtime config * fix: cloud sandbox default tool ids
This commit is contained in:
@@ -241,6 +241,14 @@
|
||||
"rag.userQuery.actions.regenerate": "Regenerate Query",
|
||||
"regenerate": "Regenerate",
|
||||
"roleAndArchive": "Agent Profile & History",
|
||||
"runtimeEnv.mode.cloud": "Cloud Sandbox",
|
||||
"runtimeEnv.mode.cloudDesc": "Run in a secure cloud sandbox",
|
||||
"runtimeEnv.mode.local": "Local",
|
||||
"runtimeEnv.mode.localDesc": "Access local files and commands",
|
||||
"runtimeEnv.mode.none": "None",
|
||||
"runtimeEnv.mode.noneDesc": "No runtime environment",
|
||||
"runtimeEnv.selectMode": "Select Runtime Environment",
|
||||
"runtimeEnv.title": "Runtime Environment",
|
||||
"search.grounding.imageSearchQueries": "Image Search Keywords",
|
||||
"search.grounding.imageTitle": "Found {{count}} images",
|
||||
"search.grounding.searchQueries": "Search Keywords",
|
||||
@@ -391,6 +399,7 @@
|
||||
"tokenTag.overload": "Exceeded Limit",
|
||||
"tokenTag.remained": "Remaining",
|
||||
"tokenTag.used": "Used",
|
||||
"tool.intervention.approvalMode": "Approval Mode",
|
||||
"tool.intervention.approve": "Approve",
|
||||
"tool.intervention.approveAndRemember": "Approve and Remember",
|
||||
"tool.intervention.approveOnce": "Approve This Time Only",
|
||||
|
||||
@@ -241,6 +241,14 @@
|
||||
"rag.userQuery.actions.regenerate": "重新生成 Query",
|
||||
"regenerate": "重新生成",
|
||||
"roleAndArchive": "助理档案与记录",
|
||||
"runtimeEnv.mode.cloud": "云端沙箱",
|
||||
"runtimeEnv.mode.cloudDesc": "在安全的云端沙箱中运行",
|
||||
"runtimeEnv.mode.local": "本地",
|
||||
"runtimeEnv.mode.localDesc": "访问本地文件和命令",
|
||||
"runtimeEnv.mode.none": "不启用",
|
||||
"runtimeEnv.mode.noneDesc": "不启用运行环境",
|
||||
"runtimeEnv.selectMode": "选择运行环境",
|
||||
"runtimeEnv.title": "运行环境",
|
||||
"search.grounding.imageSearchQueries": "图片搜索关键词",
|
||||
"search.grounding.imageTitle": "找到 {{count}} 张图片",
|
||||
"search.grounding.searchQueries": "搜索关键词",
|
||||
@@ -391,6 +399,7 @@
|
||||
"tokenTag.overload": "超出限制",
|
||||
"tokenTag.remained": "剩余",
|
||||
"tokenTag.used": "已使用",
|
||||
"tool.intervention.approvalMode": "审批模式",
|
||||
"tool.intervention.approve": "批准",
|
||||
"tool.intervention.approveAndRemember": "批准并记住",
|
||||
"tool.intervention.approveOnce": "仅本次批准",
|
||||
|
||||
@@ -29,6 +29,8 @@ export const defaultToolIds = [
|
||||
WebBrowsingManifest.identifier,
|
||||
KnowledgeBaseManifest.identifier,
|
||||
MemoryManifest.identifier,
|
||||
LocalSystemManifest.identifier,
|
||||
CloudSandboxManifest.identifier,
|
||||
];
|
||||
|
||||
export const builtinTools: LobeBuiltinTool[] = [
|
||||
@@ -55,7 +57,7 @@ export const builtinTools: LobeBuiltinTool[] = [
|
||||
},
|
||||
{
|
||||
discoverable: isDesktop,
|
||||
hidden: !isDesktop,
|
||||
hidden: true,
|
||||
identifier: LocalSystemManifest.identifier,
|
||||
manifest: LocalSystemManifest,
|
||||
type: 'builtin',
|
||||
@@ -73,6 +75,7 @@ export const builtinTools: LobeBuiltinTool[] = [
|
||||
type: 'builtin',
|
||||
},
|
||||
{
|
||||
hidden: true,
|
||||
identifier: CloudSandboxManifest.identifier,
|
||||
manifest: CloudSandboxManifest,
|
||||
type: 'builtin',
|
||||
|
||||
@@ -8,15 +8,25 @@
|
||||
export type AgentMode = 'auto' | 'plan' | 'ask' | 'implement';
|
||||
|
||||
/**
|
||||
* Local System configuration (desktop only)
|
||||
* Runtime environment mode
|
||||
* - local: Access local files and commands (desktop only)
|
||||
* - cloud: Run in cloud sandbox
|
||||
* - none: No runtime environment
|
||||
*/
|
||||
export interface LocalSystemConfig {
|
||||
export type RuntimeEnvMode = 'cloud' | 'local' | 'none';
|
||||
|
||||
export type RuntimePlatform = 'desktop' | 'web';
|
||||
|
||||
/**
|
||||
* Runtime environment configuration
|
||||
*/
|
||||
export interface RuntimeEnvConfig {
|
||||
/**
|
||||
* Local System working directory (desktop only)
|
||||
* Runtime environment mode per platform
|
||||
*/
|
||||
runtimeMode?: Partial<Record<RuntimePlatform, RuntimeEnvMode>>;
|
||||
/**
|
||||
* Working directory (desktop only)
|
||||
*/
|
||||
workingDirectory?: string;
|
||||
|
||||
// Future extensions:
|
||||
// allowedPaths?: string[];
|
||||
// deniedCommands?: string[];
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
import { type SearchMode } from '../search';
|
||||
import { type UserMemoryEffort } from '../user/settings/memory';
|
||||
import { type LocalSystemConfig } from './agentConfig';
|
||||
import { type RuntimeEnvConfig } from './agentConfig';
|
||||
|
||||
export interface WorkingModel {
|
||||
model: string;
|
||||
@@ -95,12 +95,12 @@ export interface LobeAgentChatConfig extends AgentMemoryChatConfig {
|
||||
*/
|
||||
imageResolution2?: '512px' | '1K' | '2K' | '4K';
|
||||
inputTemplate?: string;
|
||||
/**
|
||||
* Local System configuration (desktop only)
|
||||
*/
|
||||
localSystem?: LocalSystemConfig;
|
||||
reasoningBudgetToken?: number;
|
||||
reasoningEffort?: 'low' | 'medium' | 'high';
|
||||
/**
|
||||
* Runtime environment configuration (desktop only)
|
||||
*/
|
||||
runtimeEnv?: RuntimeEnvConfig;
|
||||
|
||||
searchFCModel?: WorkingModel;
|
||||
searchMode?: SearchMode;
|
||||
@@ -130,9 +130,12 @@ export interface LobeAgentChatConfig extends AgentMemoryChatConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Zod schema for LocalSystemConfig
|
||||
* Zod schema for RuntimeEnvConfig
|
||||
*/
|
||||
export const LocalSystemConfigSchema = z.object({
|
||||
const runtimeEnvModeEnum = z.enum(['local', 'cloud', 'none']);
|
||||
|
||||
export const RuntimeEnvConfigSchema = z.object({
|
||||
runtimeMode: z.record(z.string(), runtimeEnvModeEnum).optional(),
|
||||
workingDirectory: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -173,7 +176,7 @@ export const AgentChatConfigSchema = z
|
||||
imageAspectRatio2: z.string().optional(),
|
||||
imageResolution: z.enum(['1K', '2K', '4K']).optional(),
|
||||
imageResolution2: z.enum(['512px', '1K', '2K', '4K']).optional(),
|
||||
localSystem: LocalSystemConfigSchema.optional(),
|
||||
runtimeEnv: RuntimeEnvConfigSchema.optional(),
|
||||
reasoningBudgetToken: z.number().optional(),
|
||||
reasoningEffort: z.enum(['low', 'medium', 'high']).optional(),
|
||||
searchFCModel: z
|
||||
|
||||
@@ -18,6 +18,7 @@ import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
import { type ActionToolbarProps } from '../ActionBar';
|
||||
import ActionBar from '../ActionBar';
|
||||
import InputEditor from '../InputEditor';
|
||||
import RuntimeConfig from '../RuntimeConfig';
|
||||
import SendArea from '../SendArea';
|
||||
import TypoBar from '../TypoBar';
|
||||
import ContextContainer from './ContextContainer';
|
||||
@@ -59,11 +60,13 @@ interface DesktopChatInputProps extends ActionToolbarProps {
|
||||
leftContent?: ReactNode;
|
||||
sendAreaPrefix?: ReactNode;
|
||||
showFootnote?: boolean;
|
||||
showRuntimeConfig?: boolean;
|
||||
}
|
||||
|
||||
const DesktopChatInput = memo<DesktopChatInputProps>(
|
||||
({
|
||||
showFootnote,
|
||||
showRuntimeConfig = true,
|
||||
inputContainerProps,
|
||||
extentHeaderContent,
|
||||
actionBarStyle,
|
||||
@@ -151,6 +154,7 @@ const DesktopChatInput = memo<DesktopChatInputProps>(
|
||||
>
|
||||
<InputEditor />
|
||||
</ChatInput>
|
||||
{showRuntimeConfig && <RuntimeConfig />}
|
||||
{showFootnote && !expand && (
|
||||
<Center style={{ pointerEvents: 'none', zIndex: 100 }}>
|
||||
<Text className={styles.footnote} type={'secondary'}>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { type MenuProps } from '@lobehub/ui';
|
||||
import { Button, Center, DropdownMenu, Icon } from '@lobehub/ui';
|
||||
import { Button, Center, DropdownMenu, Icon, Tooltip } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { Check, ChevronDown, Hand, ListChecks, Zap } from 'lucide-react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { toolInterventionSelectors } from '@/store/user/selectors';
|
||||
|
||||
import { type ApprovalMode } from './index';
|
||||
import { type ApprovalMode } from '@/store/user/slices/settings/selectors';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
icon: css`
|
||||
@@ -39,6 +38,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
|
||||
const ModeSelector = memo(() => {
|
||||
const { t } = useTranslation('chat');
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const approvalMode = useUserStore(toolInterventionSelectors.approvalMode);
|
||||
const updateHumanIntervention = useUserStore((s) => s.updateHumanIntervention);
|
||||
|
||||
@@ -112,18 +112,33 @@ const ModeSelector = memo(() => {
|
||||
[approvalMode, modeLabels, handleModeChange, styles, t],
|
||||
);
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
className={styles.modeButton}
|
||||
color={'default'}
|
||||
icon={ChevronDown}
|
||||
iconPlacement="end"
|
||||
size="small"
|
||||
variant={'text'}
|
||||
>
|
||||
{modeLabels[approvalMode]}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu items={menuItems} placement="bottomLeft">
|
||||
<Button
|
||||
className={styles.modeButton}
|
||||
color={'default'}
|
||||
icon={ChevronDown}
|
||||
iconPlacement="end"
|
||||
size="small"
|
||||
variant={'text'}
|
||||
>
|
||||
{modeLabels[approvalMode]}
|
||||
</Button>
|
||||
<DropdownMenu
|
||||
items={menuItems}
|
||||
open={dropdownOpen}
|
||||
placement="bottomLeft"
|
||||
onOpenChange={setDropdownOpen}
|
||||
>
|
||||
<div>
|
||||
{dropdownOpen ? (
|
||||
button
|
||||
) : (
|
||||
<Tooltip title={t('tool.intervention.approvalMode')}>{button}</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
);
|
||||
});
|
||||
@@ -27,7 +27,7 @@ const WorkingDirectoryContent = memo<WorkingDirectoryContentProps>(({ agentId, o
|
||||
const activeTopicId = useChatStore((s) => s.activeTopicId);
|
||||
|
||||
// Actions
|
||||
const updateAgentLocalSystemConfig = useAgentStore((s) => s.updateAgentLocalSystemConfigById);
|
||||
const updateAgentRuntimeEnvConfig = useAgentStore((s) => s.updateAgentRuntimeEnvConfigById);
|
||||
const updateTopicMetadata = useChatStore((s) => s.updateTopicMetadata);
|
||||
|
||||
// Local state for editing
|
||||
@@ -58,7 +58,7 @@ const WorkingDirectoryContent = memo<WorkingDirectoryContentProps>(({ agentId, o
|
||||
setLoading(true);
|
||||
try {
|
||||
// Save agent working directory
|
||||
await updateAgentLocalSystemConfig(agentId, {
|
||||
await updateAgentRuntimeEnvConfig(agentId, {
|
||||
workingDirectory: agentDir || undefined,
|
||||
});
|
||||
|
||||
@@ -85,7 +85,7 @@ const WorkingDirectoryContent = memo<WorkingDirectoryContentProps>(({ agentId, o
|
||||
useTopicOverride,
|
||||
topicDir,
|
||||
topicWorkingDirectory,
|
||||
updateAgentLocalSystemConfig,
|
||||
updateAgentRuntimeEnvConfig,
|
||||
updateTopicMetadata,
|
||||
onClose,
|
||||
]);
|
||||
276
src/features/ChatInput/RuntimeConfig/index.tsx
Normal file
276
src/features/ChatInput/RuntimeConfig/index.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
import { isDesktop } from '@lobechat/const';
|
||||
import { type RuntimeEnvMode } from '@lobechat/types';
|
||||
import { Flexbox, Icon, Popover, Skeleton, Tooltip } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
CloudIcon,
|
||||
FolderIcon,
|
||||
LaptopIcon,
|
||||
MonitorOffIcon,
|
||||
SquircleDashed,
|
||||
} from 'lucide-react';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentByIdSelectors, chatConfigByIdSelectors } from '@/store/agent/selectors';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { topicSelectors } from '@/store/chat/selectors';
|
||||
|
||||
import { useAgentId } from '../hooks/useAgentId';
|
||||
import { useUpdateAgentConfig } from '../hooks/useUpdateAgentConfig';
|
||||
import ApprovalMode from './ApprovalMode';
|
||||
import WorkingDirectory from './WorkingDirectory';
|
||||
|
||||
const MODE_ICONS: Record<RuntimeEnvMode, typeof LaptopIcon> = {
|
||||
cloud: CloudIcon,
|
||||
local: LaptopIcon,
|
||||
none: MonitorOffIcon,
|
||||
};
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
bar: css`
|
||||
padding-block: 0;
|
||||
padding-inline: 4px;
|
||||
`,
|
||||
button: css`
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
|
||||
height: 28px;
|
||||
padding-inline: 8px;
|
||||
border-radius: 6px;
|
||||
|
||||
font-size: 12px;
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: ${cssVar.colorText};
|
||||
background: ${cssVar.colorFillSecondary};
|
||||
}
|
||||
`,
|
||||
modeDesc: css`
|
||||
font-size: 12px;
|
||||
color: ${cssVar.colorTextTertiary};
|
||||
`,
|
||||
modeOption: css`
|
||||
cursor: pointer;
|
||||
|
||||
width: 100%;
|
||||
padding-block: 8px;
|
||||
padding-inline: 8px;
|
||||
border-radius: ${cssVar.borderRadius};
|
||||
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: ${cssVar.colorFillTertiary};
|
||||
}
|
||||
`,
|
||||
modeOptionActive: css`
|
||||
background: ${cssVar.colorFillTertiary};
|
||||
`,
|
||||
modeOptionDesc: css`
|
||||
font-size: 12px;
|
||||
color: ${cssVar.colorTextDescription};
|
||||
`,
|
||||
modeOptionIcon: css`
|
||||
border: 1px solid ${cssVar.colorFillTertiary};
|
||||
border-radius: ${cssVar.borderRadius};
|
||||
background: ${cssVar.colorBgElevated};
|
||||
`,
|
||||
modeOptionTitle: css`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${cssVar.colorText};
|
||||
`,
|
||||
}));
|
||||
|
||||
const RuntimeConfig = memo(() => {
|
||||
const { t } = useTranslation('chat');
|
||||
const { t: tPlugin } = useTranslation('plugin');
|
||||
const agentId = useAgentId();
|
||||
const { updateAgentChatConfig } = useUpdateAgentConfig();
|
||||
const [dirPopoverOpen, setDirPopoverOpen] = useState(false);
|
||||
const [modePopoverOpen, setModePopoverOpen] = useState(false);
|
||||
|
||||
const [isLoading, runtimeMode] = useAgentStore((s) => [
|
||||
agentByIdSelectors.isAgentConfigLoadingById(agentId)(s),
|
||||
chatConfigByIdSelectors.getRuntimeModeById(agentId)(s),
|
||||
]);
|
||||
|
||||
// Get working directory
|
||||
const topicWorkingDirectory = useChatStore(topicSelectors.currentTopicWorkingDirectory);
|
||||
const agentWorkingDirectory = useAgentStore((s) =>
|
||||
agentId ? agentByIdSelectors.getAgentWorkingDirectoryById(agentId)(s) : undefined,
|
||||
);
|
||||
const effectiveWorkingDirectory = topicWorkingDirectory || agentWorkingDirectory;
|
||||
|
||||
const switchMode = useCallback(
|
||||
async (mode: RuntimeEnvMode) => {
|
||||
if (mode === runtimeMode) return;
|
||||
|
||||
const platform = isDesktop ? 'desktop' : 'web';
|
||||
|
||||
await updateAgentChatConfig({
|
||||
runtimeEnv: { runtimeMode: { [platform]: mode } },
|
||||
});
|
||||
},
|
||||
[runtimeMode, updateAgentChatConfig],
|
||||
);
|
||||
|
||||
// Skeleton placeholder to prevent layout jump during loading
|
||||
if (!agentId || isLoading) {
|
||||
return (
|
||||
<Flexbox horizontal align={'center'} className={styles.bar} gap={4}>
|
||||
<Skeleton.Button active size="small" style={{ height: 22, minWidth: 64, width: 64 }} />
|
||||
<Skeleton.Button active size="small" style={{ height: 22, minWidth: 100, width: 100 }} />
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
const ModeIcon = MODE_ICONS[runtimeMode];
|
||||
const modeLabel = t(`runtimeEnv.mode.${runtimeMode}`);
|
||||
|
||||
const displayName = effectiveWorkingDirectory
|
||||
? effectiveWorkingDirectory.split('/').findLast(Boolean) || effectiveWorkingDirectory
|
||||
: tPlugin('localSystem.workingDirectory.notSet');
|
||||
|
||||
const modes: { desc: string; icon: typeof LaptopIcon; label: string; mode: RuntimeEnvMode }[] = [
|
||||
// Local mode is desktop-only
|
||||
...(isDesktop
|
||||
? [
|
||||
{
|
||||
desc: t('runtimeEnv.mode.localDesc'),
|
||||
icon: LaptopIcon,
|
||||
label: t('runtimeEnv.mode.local'),
|
||||
mode: 'local' as RuntimeEnvMode,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
desc: t('runtimeEnv.mode.cloudDesc'),
|
||||
icon: CloudIcon,
|
||||
label: t('runtimeEnv.mode.cloud'),
|
||||
mode: 'cloud',
|
||||
},
|
||||
{
|
||||
desc: t('runtimeEnv.mode.noneDesc'),
|
||||
icon: MonitorOffIcon,
|
||||
label: t('runtimeEnv.mode.none'),
|
||||
mode: 'none',
|
||||
},
|
||||
];
|
||||
|
||||
const modeContent = (
|
||||
<Flexbox gap={4} style={{ minWidth: 280 }}>
|
||||
{modes.map(({ mode, icon, label, desc }) => (
|
||||
<Flexbox
|
||||
horizontal
|
||||
align={'flex-start'}
|
||||
className={cx(styles.modeOption, runtimeMode === mode && styles.modeOptionActive)}
|
||||
gap={12}
|
||||
key={mode}
|
||||
onClick={() => switchMode(mode)}
|
||||
>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={styles.modeOptionIcon}
|
||||
flex={'none'}
|
||||
height={32}
|
||||
justify={'center'}
|
||||
width={32}
|
||||
>
|
||||
<Icon icon={icon} />
|
||||
</Flexbox>
|
||||
<Flexbox flex={1}>
|
||||
<div className={styles.modeOptionTitle}>{label}</div>
|
||||
<div className={styles.modeOptionDesc}>{desc}</div>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
))}
|
||||
</Flexbox>
|
||||
);
|
||||
|
||||
const modeButton = (
|
||||
<div className={styles.button}>
|
||||
<Icon icon={ModeIcon} size={14} />
|
||||
<span>{modeLabel}</span>
|
||||
<Icon icon={ChevronDownIcon} size={12} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const dirButton = (
|
||||
<div className={styles.button}>
|
||||
<Icon icon={effectiveWorkingDirectory ? FolderIcon : SquircleDashed} size={14} />
|
||||
<span>{displayName}</span>
|
||||
<Icon icon={ChevronDownIcon} size={12} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const rightContent = () => {
|
||||
if (runtimeMode === 'local') {
|
||||
return (
|
||||
<Popover
|
||||
content={<WorkingDirectory agentId={agentId} onClose={() => setDirPopoverOpen(false)} />}
|
||||
open={dirPopoverOpen}
|
||||
placement="bottomRight"
|
||||
trigger="click"
|
||||
onOpenChange={setDirPopoverOpen}
|
||||
>
|
||||
<div>
|
||||
{dirPopoverOpen ? (
|
||||
dirButton
|
||||
) : (
|
||||
<Tooltip
|
||||
title={effectiveWorkingDirectory || tPlugin('localSystem.workingDirectory.notSet')}
|
||||
>
|
||||
{dirButton}
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Flexbox horizontal align={'center'} className={styles.bar} justify={'space-between'}>
|
||||
{/* Left: Runtime env + working directory */}
|
||||
<Flexbox horizontal align={'center'} gap={4}>
|
||||
<Popover
|
||||
content={modeContent}
|
||||
open={modePopoverOpen}
|
||||
placement="top"
|
||||
styles={{ content: { padding: 4 } }}
|
||||
trigger="click"
|
||||
onOpenChange={setModePopoverOpen}
|
||||
>
|
||||
<div>
|
||||
{modePopoverOpen ? (
|
||||
modeButton
|
||||
) : (
|
||||
<Tooltip title={t('runtimeEnv.selectMode')}>{modeButton}</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
{rightContent()}
|
||||
</Flexbox>
|
||||
|
||||
{/* Right: Permission control */}
|
||||
<ApprovalMode />
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
RuntimeConfig.displayName = 'RuntimeConfig';
|
||||
|
||||
export default RuntimeConfig;
|
||||
@@ -11,7 +11,6 @@ import { useConversationStore } from '../../../../../store';
|
||||
import Arguments from '../Arguments';
|
||||
import ApprovalActions from './ApprovalActions';
|
||||
import KeyValueEditor from './KeyValueEditor';
|
||||
import ModeSelector from './ModeSelector';
|
||||
|
||||
interface FallbackInterventionProps {
|
||||
apiName: string;
|
||||
@@ -77,8 +76,7 @@ const FallbackIntervention = memo<FallbackInterventionProps>(
|
||||
}
|
||||
/>
|
||||
|
||||
<Flexbox horizontal justify={'space-between'}>
|
||||
<ModeSelector />
|
||||
<Flexbox horizontal justify={'flex-end'}>
|
||||
<ApprovalActions
|
||||
apiName={apiName}
|
||||
approvalMode={approvalMode}
|
||||
|
||||
@@ -11,10 +11,9 @@ import Arguments from '../Arguments';
|
||||
import ApprovalActions from './ApprovalActions';
|
||||
import Fallback from './Fallback';
|
||||
import KeyValueEditor from './KeyValueEditor';
|
||||
import ModeSelector from './ModeSelector';
|
||||
import SecurityBlacklistWarning from './SecurityBlacklistWarning';
|
||||
|
||||
export type ApprovalMode = 'auto-run' | 'allow-list' | 'manual';
|
||||
export type { ApprovalMode } from '@/store/user/slices/settings/selectors';
|
||||
|
||||
interface InterventionProps {
|
||||
apiName: string;
|
||||
@@ -110,8 +109,7 @@ const Intervention = memo<InterventionProps>(
|
||||
registerBeforeApprove={registerBeforeApprove}
|
||||
onArgsChange={handleArgsChange}
|
||||
/>
|
||||
<Flexbox horizontal justify={'space-between'}>
|
||||
<ModeSelector />
|
||||
<Flexbox horizontal justify={'flex-end'}>
|
||||
<ApprovalActions
|
||||
apiName={apiName}
|
||||
approvalMode={approvalMode}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { memo, Suspense } from 'react';
|
||||
|
||||
import AbortResponse from './AbortResponse';
|
||||
import Intervention from './Intervention';
|
||||
import ModeSelector from './Intervention/ModeSelector';
|
||||
import LoadingPlaceholder from './LoadingPlaceholder';
|
||||
import RejectedResponse from './RejectedResponse';
|
||||
import ToolRender from './Render';
|
||||
@@ -124,11 +123,6 @@ const Render = memo<RenderProps>(
|
||||
type: type as any,
|
||||
}}
|
||||
/>
|
||||
{!disableEditing && (
|
||||
<div>
|
||||
<ModeSelector />
|
||||
</div>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/**
|
||||
* Tools Engineering - Unified tools processing using ToolsEngine
|
||||
*/
|
||||
import { CloudSandboxManifest } from '@lobechat/builtin-tool-cloud-sandbox';
|
||||
import { KnowledgeBaseManifest } from '@lobechat/builtin-tool-knowledge-base';
|
||||
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
|
||||
import { MemoryManifest } from '@lobechat/builtin-tool-memory';
|
||||
import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing';
|
||||
import { defaultToolIds } from '@lobechat/builtin-tools';
|
||||
@@ -102,7 +104,10 @@ export const createAgentToolsEngine = (workingModel: WorkingModel) => {
|
||||
return undefined; // fall through to rules
|
||||
},
|
||||
rules: {
|
||||
[CloudSandboxManifest.identifier]:
|
||||
agentChatConfigSelectors.isCloudSandboxEnabled(agentState),
|
||||
[KnowledgeBaseManifest.identifier]: agentSelectors.hasEnabledKnowledgeBases(agentState),
|
||||
[LocalSystemManifest.identifier]: agentChatConfigSelectors.isLocalSystemEnabled(agentState),
|
||||
[MemoryManifest.identifier]: agentChatConfigSelectors.isMemoryToolEnabled(agentState),
|
||||
[WebBrowsingManifest.identifier]: searchConfig.useApplicationBuiltinSearchTool,
|
||||
},
|
||||
|
||||
@@ -273,6 +273,14 @@ export default {
|
||||
'memory.title': 'Memory',
|
||||
'search.grounding.imageSearchQueries': 'Image Search Keywords',
|
||||
'search.grounding.imageTitle': 'Found {{count}} images',
|
||||
'runtimeEnv.mode.cloud': 'Cloud Sandbox',
|
||||
'runtimeEnv.mode.cloudDesc': 'Run in a secure cloud sandbox',
|
||||
'runtimeEnv.mode.local': 'Local',
|
||||
'runtimeEnv.mode.localDesc': 'Access local files and commands',
|
||||
'runtimeEnv.mode.none': 'Off',
|
||||
'runtimeEnv.mode.noneDesc': 'Disable runtime environment',
|
||||
'runtimeEnv.selectMode': 'Select Runtime Environment',
|
||||
'runtimeEnv.title': 'Runtime Environment',
|
||||
'search.grounding.searchQueries': 'Search Keywords',
|
||||
'search.grounding.title': 'Found {{count}} results',
|
||||
'search.mode.auto.desc': 'Search the web automatically when needed.',
|
||||
@@ -425,6 +433,7 @@ export default {
|
||||
'tokenTag.overload': 'Exceeded Limit',
|
||||
'tokenTag.remained': 'Remaining',
|
||||
'tokenTag.used': 'Used',
|
||||
'tool.intervention.approvalMode': 'Approval Mode',
|
||||
'tool.intervention.approve': 'Approve',
|
||||
'tool.intervention.approveAndRemember': 'Approve and Remember',
|
||||
'tool.intervention.approveOnce': 'Approve This Time Only',
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
|
||||
import { Flexbox, Icon, Popover, Tooltip } from '@lobehub/ui';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { LaptopIcon, SquircleDashed } from 'lucide-react';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentByIdSelectors } from '@/store/agent/selectors';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { topicSelectors } from '@/store/chat/selectors';
|
||||
|
||||
import WorkingDirectoryContent from './WorkingDirectoryContent';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => {
|
||||
return {
|
||||
base: css`
|
||||
border-radius: 6px;
|
||||
color: ${cssVar.colorTextTertiary};
|
||||
background-color: ${cssVar.colorFillTertiary};
|
||||
|
||||
:hover {
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
background-color: ${cssVar.colorFillSecondary};
|
||||
}
|
||||
`,
|
||||
filled: css`
|
||||
font-family: ${cssVar.fontFamilyCode};
|
||||
color: ${cssVar.colorText} !important;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const WorkingDirectory = memo(() => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const agentId = useAgentStore((s) => s.activeAgentId);
|
||||
|
||||
// Check if local-system plugin is enabled for current agent
|
||||
const plugins = useAgentStore((s) =>
|
||||
agentId ? agentByIdSelectors.getAgentPluginsById(agentId)(s) : [],
|
||||
);
|
||||
const isLocalSystemEnabled = useMemo(
|
||||
() => plugins.includes(LocalSystemManifest.identifier),
|
||||
[plugins],
|
||||
);
|
||||
|
||||
// Get working directory from Topic (higher priority) or Agent (fallback)
|
||||
const topicWorkingDirectory = useChatStore(topicSelectors.currentTopicWorkingDirectory);
|
||||
const agentWorkingDirectory = useAgentStore((s) =>
|
||||
agentId ? agentByIdSelectors.getAgentWorkingDirectoryById(agentId)(s) : undefined,
|
||||
);
|
||||
|
||||
const effectiveWorkingDirectory = topicWorkingDirectory || agentWorkingDirectory;
|
||||
|
||||
// Only show when local-system is enabled and agent exists
|
||||
if (!agentId || !isLocalSystemEnabled) return null;
|
||||
|
||||
// Get last folder name for display
|
||||
const hasWorkingDirectory = !!effectiveWorkingDirectory;
|
||||
|
||||
const displayName = effectiveWorkingDirectory
|
||||
? effectiveWorkingDirectory.split('/').findLast(Boolean) || effectiveWorkingDirectory
|
||||
: t('localSystem.workingDirectory.notSet');
|
||||
|
||||
const content = hasWorkingDirectory ? (
|
||||
<Flexbox
|
||||
horizontal
|
||||
align="center"
|
||||
className={cx(styles.base, styles.filled)}
|
||||
gap={6}
|
||||
style={{ cursor: 'pointer', height: 32, padding: '0 12px' }}
|
||||
>
|
||||
<Icon icon={LaptopIcon} size={18} />
|
||||
<span>{displayName}</span>
|
||||
</Flexbox>
|
||||
) : (
|
||||
<Flexbox
|
||||
horizontal
|
||||
align="center"
|
||||
className={styles.base}
|
||||
gap={6}
|
||||
style={{ cursor: 'pointer', height: 32, padding: '0 12px' }}
|
||||
>
|
||||
<Icon icon={SquircleDashed} size={16} />
|
||||
<span>{t('localSystem.workingDirectory.notSet')}</span>
|
||||
</Flexbox>
|
||||
);
|
||||
return (
|
||||
<Popover
|
||||
content={<WorkingDirectoryContent agentId={agentId} onClose={() => setOpen(false)} />}
|
||||
open={open}
|
||||
placement="bottomRight"
|
||||
trigger="click"
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<div>
|
||||
{open ? (
|
||||
content
|
||||
) : (
|
||||
<Tooltip title={effectiveWorkingDirectory || t('localSystem.workingDirectory.notSet')}>
|
||||
{content}
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
WorkingDirectory.displayName = 'WorkingDirectory';
|
||||
|
||||
export default WorkingDirectory;
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { isDesktop } from '@lobechat/const';
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
@@ -11,7 +10,6 @@ import HeaderActions from './HeaderActions';
|
||||
import NotebookButton from './NotebookButton';
|
||||
import ShareButton from './ShareButton';
|
||||
import Tags from './Tags';
|
||||
import WorkingDirectory from './WorkingDirectory';
|
||||
|
||||
const Header = memo(() => {
|
||||
return (
|
||||
@@ -23,7 +21,6 @@ const Header = memo(() => {
|
||||
}
|
||||
right={
|
||||
<Flexbox horizontal align={'center'} style={{ backgroundColor: cssVar.colorBgContainer }}>
|
||||
{isDesktop && <WorkingDirectory />}
|
||||
<NotebookButton />
|
||||
<ShareButton />
|
||||
<HeaderActions />
|
||||
|
||||
@@ -112,6 +112,7 @@ const InputArea = () => {
|
||||
dropdownPlacement="bottomLeft"
|
||||
extraActionItems={extraActionItems}
|
||||
inputContainerProps={inputContainerProps}
|
||||
showRuntimeConfig={false}
|
||||
/>
|
||||
</ChatInputProvider>
|
||||
</DragUploadZone>
|
||||
|
||||
@@ -16,7 +16,13 @@ const Home: FC = () => {
|
||||
return (
|
||||
<>
|
||||
{isHomeRoute && <PageTitle title="" />}
|
||||
<NavHeader right={<WideScreenButton />} />
|
||||
<NavHeader
|
||||
right={
|
||||
<Flexbox horizontal align="center">
|
||||
<WideScreenButton />
|
||||
</Flexbox>
|
||||
}
|
||||
/>
|
||||
<Flexbox height={'100%'} style={{ overflowY: 'auto', paddingBottom: '16vh' }} width={'100%'}>
|
||||
<WideScreenContainer>
|
||||
<HomeContent />
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* - Gets model capabilities from provided function
|
||||
* - No dependency on frontend stores (useToolStore, useAgentStore, etc.)
|
||||
*/
|
||||
import { CloudSandboxManifest } from '@lobechat/builtin-tool-cloud-sandbox';
|
||||
import { KnowledgeBaseManifest } from '@lobechat/builtin-tool-knowledge-base';
|
||||
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
|
||||
import { MemoryManifest } from '@lobechat/builtin-tool-memory';
|
||||
@@ -100,11 +101,19 @@ export const createServerAgentToolsEngine = (
|
||||
const searchMode = agentConfig.chatConfig?.searchMode ?? 'auto';
|
||||
const isSearchEnabled = searchMode !== 'off';
|
||||
|
||||
// Determine runtime mode based on platform
|
||||
const isDesktopClient = !!deviceContext?.gatewayConfigured;
|
||||
const platform = isDesktopClient ? 'desktop' : 'web';
|
||||
const runtimeMode =
|
||||
agentConfig.chatConfig?.runtimeEnv?.runtimeMode?.[platform] ??
|
||||
(isDesktopClient ? 'local' : 'none');
|
||||
|
||||
log(
|
||||
'Creating agent tools engine for model=%s, provider=%s, searchMode=%s, additionalManifests=%d, deviceGateway=%s',
|
||||
'Creating agent tools engine for model=%s, provider=%s, searchMode=%s, runtimeMode=%s, additionalManifests=%d, deviceGateway=%s',
|
||||
model,
|
||||
provider,
|
||||
searchMode,
|
||||
runtimeMode,
|
||||
additionalManifests?.length ?? 0,
|
||||
!!deviceContext?.gatewayConfigured,
|
||||
);
|
||||
@@ -116,9 +125,12 @@ export const createServerAgentToolsEngine = (
|
||||
defaultToolIds,
|
||||
enableChecker: createEnableChecker({
|
||||
rules: {
|
||||
[CloudSandboxManifest.identifier]: runtimeMode === 'cloud',
|
||||
[KnowledgeBaseManifest.identifier]: hasEnabledKnowledgeBases,
|
||||
[LocalSystemManifest.identifier]:
|
||||
!!deviceContext?.gatewayConfigured && !!deviceContext?.deviceOnline,
|
||||
runtimeMode === 'local' &&
|
||||
!!deviceContext?.gatewayConfigured &&
|
||||
!!deviceContext?.deviceOnline,
|
||||
[MemoryManifest.identifier]: globalMemoryEnabled,
|
||||
[RemoteDeviceManifest.identifier]: !!deviceContext?.gatewayConfigured,
|
||||
[WebBrowsingManifest.identifier]: isSearchEnabled,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type LobeToolManifest, type PluginEnableChecker } from '@lobechat/context-engine';
|
||||
import { type LobeTool } from '@lobechat/types';
|
||||
import { type LobeTool, type RuntimeEnvConfig } from '@lobechat/types';
|
||||
|
||||
/**
|
||||
* Installed plugin with manifest
|
||||
@@ -36,8 +36,9 @@ export interface ServerCreateAgentToolsEngineParams {
|
||||
additionalManifests?: LobeToolManifest[];
|
||||
/** Agent configuration containing plugins array */
|
||||
agentConfig: {
|
||||
/** Optional agent chat config with searchMode */
|
||||
/** Optional agent chat config */
|
||||
chatConfig?: {
|
||||
runtimeEnv?: RuntimeEnvConfig;
|
||||
searchMode?: 'off' | 'on' | 'auto';
|
||||
};
|
||||
/** Plugin IDs enabled for this agent */
|
||||
|
||||
@@ -311,7 +311,7 @@ export class AgentRuntimeService {
|
||||
userMemory,
|
||||
userTimezone,
|
||||
webhookDelivery,
|
||||
workingDirectory: agentConfig?.chatConfig?.localSystem?.workingDirectory,
|
||||
workingDirectory: agentConfig?.chatConfig?.runtimeEnv?.workingDirectory,
|
||||
...appContext,
|
||||
},
|
||||
maxSteps,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DEFAULT_PROVIDER } from '@lobechat/business-const';
|
||||
import { DEFAULT_MODEL, DEFAUTT_AGENT_TTS_CONFIG } from '@lobechat/const';
|
||||
import { type AgentBuilderContext } from '@lobechat/context-engine';
|
||||
import { type AgentMode, type LobeAgentTTSConfig, type LocalSystemConfig } from '@lobechat/types';
|
||||
import { type AgentMode, type LobeAgentTTSConfig, type RuntimeEnvConfig } from '@lobechat/types';
|
||||
|
||||
import { type AgentStoreState } from '../initialState';
|
||||
import { agentSelectors } from './selectors';
|
||||
@@ -74,13 +74,13 @@ const getAgentEnableModeById =
|
||||
};
|
||||
|
||||
/**
|
||||
* Get local system config by agentId
|
||||
* Now reads from chatConfig.localSystem
|
||||
* Get runtime env config by agentId
|
||||
* Now reads from chatConfig.runtimeEnv
|
||||
*/
|
||||
const getAgentLocalSystemConfigById =
|
||||
const getAgentRuntimeEnvConfigById =
|
||||
(agentId: string) =>
|
||||
(s: AgentStoreState): LocalSystemConfig | undefined =>
|
||||
agentSelectors.getAgentConfigById(agentId)(s)?.chatConfig?.localSystem;
|
||||
(s: AgentStoreState): RuntimeEnvConfig | undefined =>
|
||||
agentSelectors.getAgentConfigById(agentId)(s)?.chatConfig?.runtimeEnv;
|
||||
|
||||
/**
|
||||
* Get working directory by agentId
|
||||
@@ -88,7 +88,7 @@ const getAgentLocalSystemConfigById =
|
||||
const getAgentWorkingDirectoryById =
|
||||
(agentId: string) =>
|
||||
(s: AgentStoreState): string | undefined =>
|
||||
getAgentLocalSystemConfigById(agentId)(s)?.workingDirectory;
|
||||
getAgentRuntimeEnvConfigById(agentId)(s)?.workingDirectory;
|
||||
|
||||
/**
|
||||
* Get agent builder context by agentId
|
||||
@@ -128,7 +128,7 @@ export const agentByIdSelectors = {
|
||||
getAgentEnableModeById,
|
||||
getAgentFilesById,
|
||||
getAgentKnowledgeBasesById,
|
||||
getAgentLocalSystemConfigById,
|
||||
getAgentRuntimeEnvConfigById,
|
||||
getAgentModeById,
|
||||
getAgentModelById,
|
||||
getAgentModelProviderById,
|
||||
|
||||
@@ -13,6 +13,8 @@ vi.mock('@lobechat/model-runtime', () => ({
|
||||
isThinkingWithToolClaudeModel: vi.fn((model) => model === 'claude-3-7-sonnet'),
|
||||
}));
|
||||
|
||||
// isDesktop defaults to false in test environment (no __ELECTRON__)
|
||||
|
||||
const createState = (overrides: Partial<AgentStoreState> = {}): AgentStoreState => ({
|
||||
...initialAgentSliceState,
|
||||
...initialBuiltinAgentSliceState,
|
||||
@@ -427,4 +429,127 @@ describe('chatConfigByIdSelectors', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRuntimeEnvConfigById', () => {
|
||||
it('should return runtimeEnv config for specified agent', () => {
|
||||
const state = createState({
|
||||
agentMap: {
|
||||
'agent-1': {
|
||||
chatConfig: {
|
||||
runtimeEnv: { runtimeMode: { web: 'cloud' }, workingDirectory: '/home' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(chatConfigByIdSelectors.getRuntimeEnvConfigById('agent-1')(state)).toEqual({
|
||||
runtimeMode: { web: 'cloud' },
|
||||
workingDirectory: '/home',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when runtimeEnv is not set', () => {
|
||||
const state = createState({
|
||||
agentMap: { 'agent-1': {} },
|
||||
});
|
||||
|
||||
expect(chatConfigByIdSelectors.getRuntimeEnvConfigById('agent-1')(state)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// In test environment, isDesktop is false (no __ELECTRON__), so CURRENT_PLATFORM = 'web'
|
||||
describe('getRuntimeModeById (web platform)', () => {
|
||||
it('should return web runtime mode when set', () => {
|
||||
const state = createState({
|
||||
agentMap: {
|
||||
'agent-1': {
|
||||
chatConfig: { runtimeEnv: { runtimeMode: { web: 'cloud' } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(chatConfigByIdSelectors.getRuntimeModeById('agent-1')(state)).toBe('cloud');
|
||||
});
|
||||
|
||||
it('should default to "none" on web when not set', () => {
|
||||
const state = createState({
|
||||
agentMap: { 'agent-1': {} },
|
||||
});
|
||||
|
||||
expect(chatConfigByIdSelectors.getRuntimeModeById('agent-1')(state)).toBe('none');
|
||||
});
|
||||
|
||||
it('should default to "none" for non-existent agent', () => {
|
||||
const state = createState({ agentMap: {} });
|
||||
|
||||
expect(chatConfigByIdSelectors.getRuntimeModeById('non-existent')(state)).toBe('none');
|
||||
});
|
||||
|
||||
it('should ignore desktop runtime mode on web', () => {
|
||||
const state = createState({
|
||||
agentMap: {
|
||||
'agent-1': {
|
||||
chatConfig: { runtimeEnv: { runtimeMode: { desktop: 'local' } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// desktop is set but web is not, should fall back to web default
|
||||
expect(chatConfigByIdSelectors.getRuntimeModeById('agent-1')(state)).toBe('none');
|
||||
});
|
||||
|
||||
it('should read correct platform when both are set', () => {
|
||||
const state = createState({
|
||||
agentMap: {
|
||||
'agent-1': {
|
||||
chatConfig: {
|
||||
runtimeEnv: { runtimeMode: { desktop: 'local', web: 'cloud' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(chatConfigByIdSelectors.getRuntimeModeById('agent-1')(state)).toBe('cloud');
|
||||
});
|
||||
|
||||
it('should work with different agents independently', () => {
|
||||
const state = createState({
|
||||
agentMap: {
|
||||
'agent-1': {
|
||||
chatConfig: { runtimeEnv: { runtimeMode: { web: 'cloud' } } },
|
||||
},
|
||||
'agent-2': {
|
||||
chatConfig: { runtimeEnv: { runtimeMode: { web: 'none' } } },
|
||||
},
|
||||
'agent-3': { chatConfig: {} },
|
||||
},
|
||||
});
|
||||
|
||||
expect(chatConfigByIdSelectors.getRuntimeModeById('agent-1')(state)).toBe('cloud');
|
||||
expect(chatConfigByIdSelectors.getRuntimeModeById('agent-2')(state)).toBe('none');
|
||||
expect(chatConfigByIdSelectors.getRuntimeModeById('agent-3')(state)).toBe('none');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLocalSystemEnabledById', () => {
|
||||
it('should return false on web even with desktop set to local', () => {
|
||||
const state = createState({
|
||||
agentMap: {
|
||||
'agent-1': {
|
||||
chatConfig: { runtimeEnv: { runtimeMode: { desktop: 'local' } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(chatConfigByIdSelectors.isLocalSystemEnabledById('agent-1')(state)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when not set (web defaults to none)', () => {
|
||||
const state = createState({
|
||||
agentMap: { 'agent-1': {} },
|
||||
});
|
||||
|
||||
expect(chatConfigByIdSelectors.isLocalSystemEnabledById('agent-1')(state)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { DEFAULT_AGENT_CHAT_CONFIG, DEFAULT_AGENT_SEARCH_FC_MODEL } from '@lobechat/const';
|
||||
import { type LobeAgentChatConfig } from '@lobechat/types';
|
||||
import {
|
||||
DEFAULT_AGENT_CHAT_CONFIG,
|
||||
DEFAULT_AGENT_SEARCH_FC_MODEL,
|
||||
isDesktop,
|
||||
} from '@lobechat/const';
|
||||
import { type LobeAgentChatConfig, type RuntimeEnvMode } from '@lobechat/types';
|
||||
|
||||
import { type AgentStoreState } from '@/store/agent/initialState';
|
||||
|
||||
@@ -48,15 +52,37 @@ const isMemoryToolEnabledById = (agentId: string) => (s: AgentStoreState) =>
|
||||
const getMemoryToolEffortById = (agentId: string) => (s: AgentStoreState) =>
|
||||
getChatConfigById(agentId)(s).memory?.effort ?? 'medium';
|
||||
|
||||
const getRuntimeEnvConfigById = (agentId: string) => (s: AgentStoreState) =>
|
||||
getChatConfigById(agentId)(s).runtimeEnv;
|
||||
|
||||
const isLocalSystemEnabledById = (agentId: string) => (s: AgentStoreState) =>
|
||||
getRuntimeModeById(agentId)(s) === 'local';
|
||||
|
||||
/**
|
||||
* Get runtime environment mode by agent ID.
|
||||
* Reads from `runtimeMode[platform]`, defaults to 'local' on desktop, 'none' on web.
|
||||
*/
|
||||
const getRuntimeModeById =
|
||||
(agentId: string) =>
|
||||
(s: AgentStoreState): RuntimeEnvMode => {
|
||||
const runtimeEnv = getChatConfigById(agentId)(s).runtimeEnv;
|
||||
const platform = isDesktop ? 'desktop' : 'web';
|
||||
|
||||
return runtimeEnv?.runtimeMode?.[platform] ?? (isDesktop ? 'local' : 'none');
|
||||
};
|
||||
|
||||
export const chatConfigByIdSelectors = {
|
||||
getChatConfigById,
|
||||
getEnableHistoryCountById,
|
||||
getHistoryCountById,
|
||||
getRuntimeEnvConfigById,
|
||||
getMemoryToolConfigById,
|
||||
getMemoryToolEffortById,
|
||||
getRuntimeModeById,
|
||||
getSearchFCModelById,
|
||||
getSearchModeById,
|
||||
getUseModelBuiltinSearchById,
|
||||
isEnableSearchById,
|
||||
isLocalSystemEnabledById,
|
||||
isMemoryToolEnabledById,
|
||||
};
|
||||
|
||||
@@ -31,6 +31,12 @@ const historyCount = (s: AgentStoreState): number =>
|
||||
const isMemoryToolEnabled = (s: AgentStoreState) =>
|
||||
chatConfigByIdSelectors.isMemoryToolEnabledById(s.activeAgentId || '')(s);
|
||||
|
||||
const isLocalSystemEnabled = (s: AgentStoreState) =>
|
||||
chatConfigByIdSelectors.isLocalSystemEnabledById(s.activeAgentId || '')(s);
|
||||
|
||||
const isCloudSandboxEnabled = (s: AgentStoreState) =>
|
||||
chatConfigByIdSelectors.getRuntimeModeById(s.activeAgentId || '')(s) === 'cloud';
|
||||
|
||||
const enableHistoryDivider =
|
||||
(historyLength: number, currentIndex: number) => (s: AgentStoreState) => {
|
||||
const config = currentChatConfig(s);
|
||||
@@ -49,6 +55,8 @@ export const agentChatConfigSelectors = {
|
||||
enableHistoryDivider,
|
||||
historyCount,
|
||||
isAgentEnableSearch,
|
||||
isCloudSandboxEnabled,
|
||||
isLocalSystemEnabled,
|
||||
isMemoryToolEnabled,
|
||||
searchFCModel,
|
||||
useModelBuiltinSearch,
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
type KnowledgeItem,
|
||||
type LobeAgentConfig,
|
||||
type LobeAgentTTSConfig,
|
||||
type LocalSystemConfig,
|
||||
type MetaData,
|
||||
type RuntimeEnvConfig,
|
||||
} from '@lobechat/types';
|
||||
import { KnowledgeType } from '@lobechat/types';
|
||||
import { VoiceList } from '@lobehub/tts';
|
||||
@@ -248,17 +248,17 @@ const currentAgentMode = (s: AgentStoreState): AgentMode | undefined => {
|
||||
const isAgentModeEnabled = (s: AgentStoreState): boolean => currentAgentMode(s) !== undefined;
|
||||
|
||||
/**
|
||||
* Get current agent's local system config
|
||||
* Now reads from chatConfig.localSystem
|
||||
* Get current agent's runtime env config
|
||||
* Now reads from chatConfig.runtimeEnv
|
||||
*/
|
||||
const currentAgentLocalSystemConfig = (s: AgentStoreState): LocalSystemConfig | undefined =>
|
||||
currentAgentConfig(s)?.chatConfig?.localSystem;
|
||||
const currentAgentRuntimeEnvConfig = (s: AgentStoreState): RuntimeEnvConfig | undefined =>
|
||||
currentAgentConfig(s)?.chatConfig?.runtimeEnv;
|
||||
|
||||
/**
|
||||
* Get current agent's working directory
|
||||
*/
|
||||
const currentAgentWorkingDirectory = (s: AgentStoreState): string | undefined =>
|
||||
currentAgentLocalSystemConfig(s)?.workingDirectory;
|
||||
currentAgentRuntimeEnvConfig(s)?.workingDirectory;
|
||||
|
||||
const isCurrentAgentExternal = (s: AgentStoreState): boolean => !currentAgentData(s)?.virtual;
|
||||
|
||||
@@ -269,7 +269,7 @@ export const agentSelectors = {
|
||||
currentAgentDescription,
|
||||
currentAgentFiles,
|
||||
currentAgentKnowledgeBases,
|
||||
currentAgentLocalSystemConfig,
|
||||
currentAgentRuntimeEnvConfig,
|
||||
currentAgentMeta,
|
||||
currentAgentMode,
|
||||
currentAgentModel,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { userProfileSelectors } from '@/store/user/selectors';
|
||||
import {
|
||||
type LobeAgentChatConfig,
|
||||
type LobeAgentConfig,
|
||||
type LocalSystemConfig,
|
||||
type RuntimeEnvConfig,
|
||||
} from '@/types/agent';
|
||||
import { type MetaData } from '@/types/meta';
|
||||
import { merge } from '@/utils/merge';
|
||||
@@ -191,13 +191,13 @@ export class AgentSliceActionImpl {
|
||||
await this.#get().optimisticUpdateAgentConfig(agentId, config, controller.signal);
|
||||
};
|
||||
|
||||
updateAgentLocalSystemConfigById = async (
|
||||
updateAgentRuntimeEnvConfigById = async (
|
||||
agentId: string,
|
||||
config: Partial<LocalSystemConfig>,
|
||||
config: Partial<RuntimeEnvConfig>,
|
||||
): Promise<void> => {
|
||||
if (!agentId) return;
|
||||
|
||||
await this.#get().updateAgentChatConfigById(agentId, { localSystem: config });
|
||||
await this.#get().updateAgentChatConfigById(agentId, { runtimeEnv: config });
|
||||
};
|
||||
|
||||
updateAgentMeta = async (meta: Partial<MetaData>): Promise<void> => {
|
||||
|
||||
@@ -2,4 +2,4 @@ export { userGeneralSettingsSelectors } from './general';
|
||||
export { keyVaultsConfigSelectors } from './keyVaults';
|
||||
export { settingsSelectors } from './settings';
|
||||
export { systemAgentSelectors } from './systemAgent';
|
||||
export { toolInterventionSelectors } from './toolIntervention';
|
||||
export { type ApprovalMode, toolInterventionSelectors } from './toolIntervention';
|
||||
|
||||
@@ -5,11 +5,11 @@ import { currentSettings } from './settings';
|
||||
/**
|
||||
* User-selectable approval modes (excludes 'headless' which is for backend async tasks only)
|
||||
*/
|
||||
type UserApprovalMode = 'auto-run' | 'allow-list' | 'manual';
|
||||
export type ApprovalMode = 'auto-run' | 'allow-list' | 'manual';
|
||||
|
||||
const humanInterventionConfig = (s: UserStore) => currentSettings(s).tool?.humanIntervention || {};
|
||||
|
||||
const interventionApprovalMode = (s: UserStore): UserApprovalMode => {
|
||||
const interventionApprovalMode = (s: UserStore): ApprovalMode => {
|
||||
const mode = currentSettings(s).tool?.humanIntervention?.approvalMode;
|
||||
// Filter out 'headless' mode as it's not user-selectable (fallback to auto-run as similar behavior)
|
||||
if (mode === 'headless') return 'auto-run';
|
||||
|
||||
Reference in New Issue
Block a user