mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
✨ feat: add the lobehub market tools servers (#11315)
* feat: add the lobehub market tools servers * feat: change all marketConnect to lobehubSkill & update the tools meta to show * fix: slove test error * chore: update the package json
This commit is contained in:
@@ -528,6 +528,9 @@
|
||||
"tools.klavis.servers": "servers",
|
||||
"tools.klavis.tools": "tools",
|
||||
"tools.klavis.verifyAuth": "I have completed authentication",
|
||||
"tools.lobehubSkill.authorize": "Authorize",
|
||||
"tools.lobehubSkill.connect": "Connect",
|
||||
"tools.lobehubSkill.error": "Error",
|
||||
"tools.notInstalled": "Not Installed",
|
||||
"tools.notInstalledWarning": "This skill is not currently installed, which may affect agent functionality.",
|
||||
"tools.plugins.enabled": "Enabled: {{num}}",
|
||||
|
||||
@@ -528,6 +528,9 @@
|
||||
"tools.klavis.servers": "个服务器",
|
||||
"tools.klavis.tools": "个工具",
|
||||
"tools.klavis.verifyAuth": "我已完成认证",
|
||||
"tools.lobehubSkill.authorize": "授权",
|
||||
"tools.lobehubSkill.connect": "连接",
|
||||
"tools.lobehubSkill.error": "错误",
|
||||
"tools.notInstalled": "未安装",
|
||||
"tools.notInstalledWarning": "当前技能暂未安装,可能会影响助理使用",
|
||||
"tools.plugins.enabled": "已启用 {{num}}",
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
"@lobehub/desktop-ipc-typings": "workspace:*",
|
||||
"@lobehub/editor": "^3.6.0",
|
||||
"@lobehub/icons": "^4.0.2",
|
||||
"@lobehub/market-sdk": "^0.27.1",
|
||||
"@lobehub/market-sdk": "0.28.0",
|
||||
"@lobehub/tts": "^4.0.2",
|
||||
"@lobehub/ui": "^4.11.6",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from './discover';
|
||||
export * from './editor';
|
||||
export * from './klavis';
|
||||
export * from './layoutTokens';
|
||||
export * from './lobehubSkill';
|
||||
export * from './message';
|
||||
export * from './meta';
|
||||
export * from './plugin';
|
||||
|
||||
55
packages/const/src/lobehubSkill.ts
Normal file
55
packages/const/src/lobehubSkill.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { type IconType, SiLinear } from '@icons-pack/react-simple-icons';
|
||||
|
||||
export interface LobehubSkillProviderType {
|
||||
/**
|
||||
* Whether this provider is visible by default in the UI
|
||||
*/
|
||||
defaultVisible?: boolean;
|
||||
/**
|
||||
* Icon - can be a URL string or a React icon component
|
||||
*/
|
||||
icon: string | IconType;
|
||||
/**
|
||||
* Provider ID (matches Market API, e.g., 'linear', 'microsoft')
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Display label for the provider
|
||||
*/
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Predefined LobeHub Skill Provider list
|
||||
*
|
||||
* Note:
|
||||
* - This list is used for UI display (icons, labels)
|
||||
* - Actual availability depends on Market API response
|
||||
* - Add new providers here when Market adds support
|
||||
*/
|
||||
export const LOBEHUB_SKILL_PROVIDERS: LobehubSkillProviderType[] = [
|
||||
{
|
||||
defaultVisible: true,
|
||||
icon: SiLinear,
|
||||
id: 'linear',
|
||||
label: 'Linear',
|
||||
},
|
||||
{
|
||||
defaultVisible: true,
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/outlook.svg',
|
||||
id: 'microsoft',
|
||||
label: 'Outlook Calendar',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Get provider config by ID
|
||||
*/
|
||||
export const getLobehubSkillProviderById = (id: string) =>
|
||||
LOBEHUB_SKILL_PROVIDERS.find((p) => p.id === id);
|
||||
|
||||
/**
|
||||
* Get all visible providers (for default UI display)
|
||||
*/
|
||||
export const getVisibleLobehubSkillProviders = () =>
|
||||
LOBEHUB_SKILL_PROVIDERS.filter((p) => p.defaultVisible !== false);
|
||||
@@ -8,7 +8,7 @@
|
||||
"@lobechat/python-interpreter": "workspace:*",
|
||||
"@lobechat/web-crawler": "workspace:*",
|
||||
"@lobehub/chat-plugin-sdk": "^1.32.4",
|
||||
"@lobehub/market-sdk": "beta",
|
||||
"@lobehub/market-sdk": "0.28.0",
|
||||
"@lobehub/market-types": "^1.11.4",
|
||||
"model-bank": "workspace:*",
|
||||
"type-fest": "^4.41.0",
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface ChatPluginPayload {
|
||||
/**
|
||||
* Tool source indicates where the tool comes from
|
||||
*/
|
||||
export type ToolSource = 'builtin' | 'plugin' | 'mcp' | 'klavis';
|
||||
export type ToolSource = 'builtin' | 'plugin' | 'mcp' | 'klavis' | 'lobehubSkill';
|
||||
|
||||
export interface ChatToolPayload {
|
||||
apiName: string;
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface GlobalServerConfig {
|
||||
defaultAgent?: PartialDeep<UserDefaultAgent>;
|
||||
enableEmailVerification?: boolean;
|
||||
enableKlavis?: boolean;
|
||||
enableLobehubSkill?: boolean;
|
||||
enableMagicLink?: boolean;
|
||||
enableMarketTrustedClient?: boolean;
|
||||
enableUploadFileToServer?: boolean;
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
import { Checkbox, Flexbox, Icon } from '@lobehub/ui';
|
||||
import { Loader2, SquareArrowOutUpRight, Unplug } from 'lucide-react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { lobehubSkillStoreSelectors } from '@/store/tool/selectors';
|
||||
import { LobehubSkillStatus } from '@/store/tool/slices/lobehubSkillStore/types';
|
||||
|
||||
const POLL_INTERVAL_MS = 1000;
|
||||
const POLL_TIMEOUT_MS = 15_000;
|
||||
|
||||
interface LobehubSkillServerItemProps {
|
||||
/**
|
||||
* Display label for the provider
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Provider ID (e.g., 'linear', 'github')
|
||||
*/
|
||||
provider: string;
|
||||
}
|
||||
|
||||
const LobehubSkillServerItem = memo<LobehubSkillServerItemProps>(({ provider, label }) => {
|
||||
const { t } = useTranslation('setting');
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [isToggling, setIsToggling] = useState(false);
|
||||
const [isWaitingAuth, setIsWaitingAuth] = useState(false);
|
||||
|
||||
const oauthWindowRef = useRef<Window | null>(null);
|
||||
const windowCheckIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const pollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const server = useToolStore(lobehubSkillStoreSelectors.getServerByIdentifier(provider));
|
||||
const checkStatus = useToolStore((s) => s.checkLobehubSkillStatus);
|
||||
const revokeConnect = useToolStore((s) => s.revokeLobehubSkill);
|
||||
const getAuthorizeUrl = useToolStore((s) => s.getLobehubSkillAuthorizeUrl);
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
if (windowCheckIntervalRef.current) {
|
||||
clearInterval(windowCheckIntervalRef.current);
|
||||
windowCheckIntervalRef.current = null;
|
||||
}
|
||||
if (pollIntervalRef.current) {
|
||||
clearInterval(pollIntervalRef.current);
|
||||
pollIntervalRef.current = null;
|
||||
}
|
||||
if (pollTimeoutRef.current) {
|
||||
clearTimeout(pollTimeoutRef.current);
|
||||
pollTimeoutRef.current = null;
|
||||
}
|
||||
oauthWindowRef.current = null;
|
||||
setIsWaitingAuth(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanup();
|
||||
};
|
||||
}, [cleanup]);
|
||||
|
||||
useEffect(() => {
|
||||
if (server?.status === LobehubSkillStatus.CONNECTED && isWaitingAuth) {
|
||||
cleanup();
|
||||
}
|
||||
}, [server?.status, isWaitingAuth, cleanup]);
|
||||
|
||||
const startFallbackPolling = useCallback(() => {
|
||||
if (pollIntervalRef.current) return;
|
||||
|
||||
pollIntervalRef.current = setInterval(async () => {
|
||||
try {
|
||||
await checkStatus(provider);
|
||||
} catch (error) {
|
||||
console.error('[LobehubSkill] Failed to check status:', error);
|
||||
}
|
||||
}, POLL_INTERVAL_MS);
|
||||
|
||||
pollTimeoutRef.current = setTimeout(() => {
|
||||
if (pollIntervalRef.current) {
|
||||
clearInterval(pollIntervalRef.current);
|
||||
pollIntervalRef.current = null;
|
||||
}
|
||||
setIsWaitingAuth(false);
|
||||
}, POLL_TIMEOUT_MS);
|
||||
}, [checkStatus, provider]);
|
||||
|
||||
const startWindowMonitor = useCallback(
|
||||
(oauthWindow: Window) => {
|
||||
windowCheckIntervalRef.current = setInterval(() => {
|
||||
try {
|
||||
if (oauthWindow.closed) {
|
||||
if (windowCheckIntervalRef.current) {
|
||||
clearInterval(windowCheckIntervalRef.current);
|
||||
windowCheckIntervalRef.current = null;
|
||||
}
|
||||
oauthWindowRef.current = null;
|
||||
checkStatus(provider);
|
||||
}
|
||||
} catch {
|
||||
console.log('[LobehubSkill] COOP blocked window.closed access, falling back to polling');
|
||||
if (windowCheckIntervalRef.current) {
|
||||
clearInterval(windowCheckIntervalRef.current);
|
||||
windowCheckIntervalRef.current = null;
|
||||
}
|
||||
startFallbackPolling();
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
[checkStatus, provider, startFallbackPolling],
|
||||
);
|
||||
|
||||
const openOAuthWindow = useCallback(
|
||||
(authorizeUrl: string) => {
|
||||
cleanup();
|
||||
setIsWaitingAuth(true);
|
||||
|
||||
const oauthWindow = window.open(authorizeUrl, '_blank', 'width=600,height=700');
|
||||
if (oauthWindow) {
|
||||
oauthWindowRef.current = oauthWindow;
|
||||
startWindowMonitor(oauthWindow);
|
||||
} else {
|
||||
startFallbackPolling();
|
||||
}
|
||||
},
|
||||
[cleanup, startWindowMonitor, startFallbackPolling],
|
||||
);
|
||||
|
||||
const pluginId = server ? server.identifier : '';
|
||||
const [checked, togglePlugin] = useAgentStore((s) => [
|
||||
agentSelectors.currentAgentPlugins(s).includes(pluginId),
|
||||
s.togglePlugin,
|
||||
]);
|
||||
|
||||
const handleConnect = async () => {
|
||||
// 只有已连接状态才阻止重新连接
|
||||
if (server?.isConnected) return;
|
||||
|
||||
setIsConnecting(true);
|
||||
try {
|
||||
const { authorizeUrl } = await getAuthorizeUrl(provider);
|
||||
openOAuthWindow(authorizeUrl);
|
||||
} catch (error) {
|
||||
console.error('[LobehubSkill] Failed to get authorize URL:', error);
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = async () => {
|
||||
if (!server) return;
|
||||
setIsToggling(true);
|
||||
await togglePlugin(pluginId);
|
||||
setIsToggling(false);
|
||||
};
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
if (!server) return;
|
||||
setIsToggling(true);
|
||||
if (checked) {
|
||||
await togglePlugin(pluginId);
|
||||
}
|
||||
await revokeConnect(server.identifier);
|
||||
setIsToggling(false);
|
||||
};
|
||||
|
||||
const renderRightControl = () => {
|
||||
if (isConnecting) {
|
||||
return (
|
||||
<Flexbox align="center" gap={4} horizontal onClick={(e) => e.stopPropagation()}>
|
||||
<Icon icon={Loader2} spin />
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
if (!server) {
|
||||
return (
|
||||
<Flexbox
|
||||
align="center"
|
||||
gap={4}
|
||||
horizontal
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleConnect();
|
||||
}}
|
||||
style={{ cursor: 'pointer', opacity: 0.65 }}
|
||||
>
|
||||
{t('tools.lobehubSkill.connect', { defaultValue: 'Connect' })}
|
||||
<Icon icon={SquareArrowOutUpRight} size="small" />
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
switch (server.status) {
|
||||
case LobehubSkillStatus.CONNECTED: {
|
||||
if (isToggling) {
|
||||
return <Icon icon={Loader2} spin />;
|
||||
}
|
||||
return (
|
||||
<Flexbox align="center" gap={8} horizontal>
|
||||
<Icon
|
||||
icon={Unplug}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDisconnect();
|
||||
}}
|
||||
size="small"
|
||||
style={{ cursor: 'pointer', opacity: 0.5 }}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
case LobehubSkillStatus.CONNECTING: {
|
||||
if (isWaitingAuth) {
|
||||
return (
|
||||
<Flexbox align="center" gap={4} horizontal onClick={(e) => e.stopPropagation()}>
|
||||
<Icon icon={Loader2} spin />
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Flexbox
|
||||
align="center"
|
||||
gap={4}
|
||||
horizontal
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
const { authorizeUrl } = await getAuthorizeUrl(provider);
|
||||
openOAuthWindow(authorizeUrl);
|
||||
} catch (error) {
|
||||
console.error('[LobehubSkill] Failed to get authorize URL:', error);
|
||||
}
|
||||
}}
|
||||
style={{ cursor: 'pointer', opacity: 0.65 }}
|
||||
>
|
||||
{t('tools.lobehubSkill.authorize', { defaultValue: 'Authorize' })}
|
||||
<Icon icon={SquareArrowOutUpRight} size="small" />
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
case LobehubSkillStatus.NOT_CONNECTED: {
|
||||
return (
|
||||
<Flexbox
|
||||
align="center"
|
||||
gap={4}
|
||||
horizontal
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleConnect();
|
||||
}}
|
||||
style={{ cursor: 'pointer', opacity: 0.65 }}
|
||||
>
|
||||
{t('tools.lobehubSkill.connect', { defaultValue: 'Connect' })}
|
||||
<Icon icon={SquareArrowOutUpRight} size="small" />
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
case LobehubSkillStatus.ERROR: {
|
||||
return (
|
||||
<span style={{ color: 'red', fontSize: 12 }}>
|
||||
{t('tools.lobehubSkill.error', { defaultValue: 'Error' })}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
gap={24}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (server?.status === LobehubSkillStatus.CONNECTED) {
|
||||
handleToggle();
|
||||
}
|
||||
}}
|
||||
style={{ paddingLeft: 8 }}
|
||||
>
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
{label}
|
||||
</Flexbox>
|
||||
{renderRightControl()}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default LobehubSkillServerItem;
|
||||
@@ -1,4 +1,9 @@
|
||||
import { KLAVIS_SERVER_TYPES, type KlavisServerType } from '@lobechat/const';
|
||||
import {
|
||||
KLAVIS_SERVER_TYPES,
|
||||
type KlavisServerType,
|
||||
LOBEHUB_SKILL_PROVIDERS,
|
||||
type LobehubSkillProviderType,
|
||||
} from '@lobechat/const';
|
||||
import { Avatar, Flexbox, Icon, Image, type ItemType } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
@@ -16,11 +21,13 @@ import { useToolStore } from '@/store/tool';
|
||||
import {
|
||||
builtinToolSelectors,
|
||||
klavisStoreSelectors,
|
||||
lobehubSkillStoreSelectors,
|
||||
pluginSelectors,
|
||||
} from '@/store/tool/selectors';
|
||||
|
||||
import { useAgentId } from '../../hooks/useAgentId';
|
||||
import KlavisServerItem from './KlavisServerItem';
|
||||
import LobehubSkillServerItem from './LobehubSkillServerItem';
|
||||
import ToolItem from './ToolItem';
|
||||
|
||||
/**
|
||||
@@ -39,6 +46,21 @@ const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label
|
||||
|
||||
KlavisIcon.displayName = 'KlavisIcon';
|
||||
|
||||
/**
|
||||
* LobeHub Skill Provider 图标组件
|
||||
*/
|
||||
const LobehubSkillIcon = memo<Pick<LobehubSkillProviderType, 'icon' | 'label'>>(
|
||||
({ icon, label }) => {
|
||||
if (typeof icon === 'string') {
|
||||
return <Image alt={label} height={18} src={icon} style={{ flex: 'none' }} width={18} />;
|
||||
}
|
||||
|
||||
return <Icon fill={cssVar.colorText} icon={icon} size={18} />;
|
||||
},
|
||||
);
|
||||
|
||||
LobehubSkillIcon.displayName = 'LobehubSkillIcon';
|
||||
|
||||
export const useControls = ({
|
||||
setModalOpen,
|
||||
setUpdating,
|
||||
@@ -66,10 +88,16 @@ export const useControls = ({
|
||||
const allKlavisServers = useToolStore(klavisStoreSelectors.getServers, isEqual);
|
||||
const isKlavisEnabledInEnv = useServerConfigStore(serverConfigSelectors.enableKlavis);
|
||||
|
||||
const [useFetchPluginStore, useFetchUserKlavisServers] = useToolStore((s) => [
|
||||
s.useFetchPluginStore,
|
||||
s.useFetchUserKlavisServers,
|
||||
]);
|
||||
// LobeHub Skill 相关状态
|
||||
const allLobehubSkillServers = useToolStore(lobehubSkillStoreSelectors.getServers, isEqual);
|
||||
const isLobehubSkillEnabled = useServerConfigStore(serverConfigSelectors.enableLobehubSkill);
|
||||
|
||||
const [useFetchPluginStore, useFetchUserKlavisServers, useFetchLobehubSkillConnections] =
|
||||
useToolStore((s) => [
|
||||
s.useFetchPluginStore,
|
||||
s.useFetchUserKlavisServers,
|
||||
s.useFetchLobehubSkillConnections,
|
||||
]);
|
||||
|
||||
useFetchPluginStore();
|
||||
useFetchInstalledPlugins();
|
||||
@@ -78,6 +106,9 @@ export const useControls = ({
|
||||
// 使用 SWR 加载用户的 Klavis 集成(从数据库)
|
||||
useFetchUserKlavisServers(isKlavisEnabledInEnv);
|
||||
|
||||
// 使用 SWR 加载用户的 LobeHub Skill 连接
|
||||
useFetchLobehubSkillConnections(isLobehubSkillEnabled);
|
||||
|
||||
// 根据 identifier 获取已连接的服务器
|
||||
const getServerByName = (identifier: string) => {
|
||||
return allKlavisServers.find((server) => server.identifier === identifier);
|
||||
@@ -118,7 +149,20 @@ export const useControls = ({
|
||||
[isKlavisEnabledInEnv, allKlavisServers],
|
||||
);
|
||||
|
||||
// 合并 builtin 工具和 Klavis 服务器
|
||||
// LobeHub Skill Provider 列表项
|
||||
const lobehubSkillItems = useMemo(
|
||||
() =>
|
||||
isLobehubSkillEnabled
|
||||
? LOBEHUB_SKILL_PROVIDERS.map((provider) => ({
|
||||
icon: <LobehubSkillIcon icon={provider.icon} label={provider.label} />,
|
||||
key: provider.id, // 使用 provider.id 作为 key,与 pluginId 保持一致
|
||||
label: <LobehubSkillServerItem label={provider.label} provider={provider.id} />,
|
||||
}))
|
||||
: [],
|
||||
[isLobehubSkillEnabled, allLobehubSkillServers],
|
||||
);
|
||||
|
||||
// 合并 builtin 工具、Klavis 服务器和 LobeHub Skill Provider
|
||||
const builtinItems = useMemo(
|
||||
() => [
|
||||
// 原有的 builtin 工具
|
||||
@@ -140,10 +184,12 @@ export const useControls = ({
|
||||
/>
|
||||
),
|
||||
})),
|
||||
// LobeHub Skill Providers
|
||||
...lobehubSkillItems,
|
||||
// Klavis 服务器
|
||||
...klavisServerItems,
|
||||
],
|
||||
[filteredBuiltinList, klavisServerItems, checked, togglePlugin, setUpdating],
|
||||
[filteredBuiltinList, klavisServerItems, lobehubSkillItems, checked, togglePlugin, setUpdating],
|
||||
);
|
||||
|
||||
// 市场 tab 的 items
|
||||
@@ -233,8 +279,17 @@ export const useControls = ({
|
||||
checked.includes(item.key as string),
|
||||
);
|
||||
|
||||
// 合并 builtin 和 Klavis
|
||||
const allBuiltinItems = [...enabledBuiltinItems, ...connectedKlavisItems];
|
||||
// 已连接的 LobeHub Skill Providers
|
||||
const connectedLobehubSkillItems = lobehubSkillItems.filter((item) =>
|
||||
checked.includes(item.key as string),
|
||||
);
|
||||
|
||||
// 合并 builtin、Klavis 和 LobeHub Skill
|
||||
const allBuiltinItems = [
|
||||
...enabledBuiltinItems,
|
||||
...connectedKlavisItems,
|
||||
...connectedLobehubSkillItems,
|
||||
];
|
||||
|
||||
if (allBuiltinItems.length > 0) {
|
||||
installedItems.push({
|
||||
@@ -279,7 +334,16 @@ export const useControls = ({
|
||||
}
|
||||
|
||||
return installedItems;
|
||||
}, [filteredBuiltinList, list, klavisServerItems, checked, togglePlugin, setUpdating, t]);
|
||||
}, [
|
||||
filteredBuiltinList,
|
||||
list,
|
||||
klavisServerItems,
|
||||
lobehubSkillItems,
|
||||
checked,
|
||||
togglePlugin,
|
||||
setUpdating,
|
||||
t,
|
||||
]);
|
||||
|
||||
return { installedPluginItems, marketItems };
|
||||
};
|
||||
|
||||
@@ -38,6 +38,15 @@ const ToolTitle = memo<ToolTitleProps>(({ identifier, apiName, isLoading, isAbor
|
||||
const isBuiltinPlugin = builtinToolIdentifiers.includes(identifier);
|
||||
const pluginTitle = pluginHelpers.getPluginTitle(pluginMeta) ?? t('unknownPlugin');
|
||||
|
||||
// Debug logging for LobeHub Skill title issue
|
||||
console.log('[ToolTitle Debug]', {
|
||||
apiName,
|
||||
identifier,
|
||||
isBuiltinPlugin,
|
||||
pluginMeta,
|
||||
pluginTitle,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
|
||||
@@ -70,6 +70,9 @@ vi.mock('@/store/tool/selectors', () => ({
|
||||
klavisStoreSelectors: {
|
||||
klavisAsLobeTools: () => [],
|
||||
},
|
||||
lobehubSkillStoreSelectors: {
|
||||
lobehubSkillAsLobeTools: () => [],
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../isCanUseFC', () => ({
|
||||
|
||||
@@ -11,7 +11,11 @@ import type { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { getAgentStoreState } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { getToolStoreState } from '@/store/tool';
|
||||
import { klavisStoreSelectors, pluginSelectors } from '@/store/tool/selectors';
|
||||
import {
|
||||
klavisStoreSelectors,
|
||||
lobehubSkillStoreSelectors,
|
||||
pluginSelectors,
|
||||
} from '@/store/tool/selectors';
|
||||
|
||||
import { getSearchConfig } from '../getSearchConfig';
|
||||
import { isCanUseFC } from '../isCanUseFC';
|
||||
@@ -51,11 +55,18 @@ export const createToolsEngine = (config: ToolsEngineConfig = {}): ToolsEngine =
|
||||
.map((tool) => tool.manifest as LobeChatPluginManifest)
|
||||
.filter(Boolean);
|
||||
|
||||
// Get LobeHub Skill tool manifests
|
||||
const lobehubSkillTools = lobehubSkillStoreSelectors.lobehubSkillAsLobeTools(toolStoreState);
|
||||
const lobehubSkillManifests = lobehubSkillTools
|
||||
.map((tool) => tool.manifest as LobeChatPluginManifest)
|
||||
.filter(Boolean);
|
||||
|
||||
// Combine all manifests
|
||||
const allManifests = [
|
||||
...pluginManifests,
|
||||
...builtinManifests,
|
||||
...klavisManifests,
|
||||
...lobehubSkillManifests,
|
||||
...additionalManifests,
|
||||
];
|
||||
|
||||
|
||||
@@ -603,6 +603,9 @@ export default {
|
||||
'tools.klavis.servers': 'servers',
|
||||
'tools.klavis.tools': 'tools',
|
||||
'tools.klavis.verifyAuth': 'I have completed authentication',
|
||||
'tools.lobehubSkill.authorize': 'Authorize',
|
||||
'tools.lobehubSkill.connect': 'Connect',
|
||||
'tools.lobehubSkill.error': 'Error',
|
||||
'tools.notInstalled': 'Not Installed',
|
||||
'tools.notInstalledWarning':
|
||||
'This skill is not currently installed, which may affect agent functionality.',
|
||||
|
||||
@@ -76,6 +76,7 @@ export const getServerGlobalConfig = async () => {
|
||||
},
|
||||
enableEmailVerification: authEnv.AUTH_EMAIL_VERIFICATION,
|
||||
enableKlavis: !!klavisEnv.KLAVIS_API_KEY,
|
||||
enableLobehubSkill: !!(appEnv.MARKET_TRUSTED_CLIENT_SECRET && appEnv.MARKET_TRUSTED_CLIENT_ID),
|
||||
enableMagicLink: authEnv.ENABLE_MAGIC_LINK,
|
||||
enableMarketTrustedClient: !!(
|
||||
appEnv.MARKET_TRUSTED_CLIENT_SECRET && appEnv.MARKET_TRUSTED_CLIENT_ID
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from 'zod';
|
||||
import { type ToolCallContent } from '@/libs/mcp';
|
||||
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
||||
import { marketUserInfo, serverDatabase, telemetry } from '@/libs/trpc/lambda/middleware';
|
||||
import { marketSDK, requireMarketAuth } from '@/libs/trpc/lambda/middleware/marketSDK';
|
||||
import { generateTrustedClientToken, isTrustedClientEnabled } from '@/libs/trusted-client';
|
||||
import { FileS3 } from '@/server/modules/S3';
|
||||
import { DiscoverService } from '@/server/services/discover';
|
||||
@@ -41,6 +42,23 @@ const marketToolProcedure = authedProcedure
|
||||
});
|
||||
});
|
||||
|
||||
// ============================== LobeHub Skill Procedures ==============================
|
||||
/**
|
||||
* LobeHub Skill procedure with SDK and optional auth
|
||||
* Used for routes that may work without auth (like listing providers)
|
||||
*/
|
||||
const lobehubSkillBaseProcedure = authedProcedure
|
||||
.use(serverDatabase)
|
||||
.use(telemetry)
|
||||
.use(marketUserInfo)
|
||||
.use(marketSDK);
|
||||
|
||||
/**
|
||||
* LobeHub Skill procedure with required auth
|
||||
* Used for routes that require user authentication
|
||||
*/
|
||||
const lobehubSkillAuthProcedure = lobehubSkillBaseProcedure.use(requireMarketAuth);
|
||||
|
||||
// ============================== Schema Definitions ==============================
|
||||
|
||||
// Schema for metadata that frontend needs to pass (for cloud MCP reporting)
|
||||
@@ -269,6 +287,249 @@ export const marketRouter = router({
|
||||
}
|
||||
}),
|
||||
|
||||
// ============================== LobeHub Skill ==============================
|
||||
/**
|
||||
* Call a LobeHub Skill tool
|
||||
*/
|
||||
connectCallTool: lobehubSkillAuthProcedure
|
||||
.input(
|
||||
z.object({
|
||||
args: z.record(z.any()).optional(),
|
||||
provider: z.string(),
|
||||
toolName: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { provider, toolName, args } = input;
|
||||
log('connectCallTool: provider=%s, tool=%s', provider, toolName);
|
||||
|
||||
try {
|
||||
const response = await ctx.marketSDK.skills.callTool(provider, {
|
||||
args: args || {},
|
||||
tool: toolName,
|
||||
});
|
||||
|
||||
log('connectCallTool response: %O', response);
|
||||
|
||||
return {
|
||||
data: response.data,
|
||||
success: response.success,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message;
|
||||
log('connectCallTool error: %s', errorMessage);
|
||||
|
||||
if (errorMessage.includes('NOT_CONNECTED')) {
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Provider not connected. Please authorize first.',
|
||||
});
|
||||
}
|
||||
|
||||
if (errorMessage.includes('TOKEN_EXPIRED')) {
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Token expired. Please re-authorize.',
|
||||
});
|
||||
}
|
||||
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `Failed to call tool: ${errorMessage}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get all connections health status
|
||||
*/
|
||||
connectGetAllHealth: lobehubSkillAuthProcedure.query(async ({ ctx }) => {
|
||||
log('connectGetAllHealth');
|
||||
|
||||
try {
|
||||
const response = await ctx.marketSDK.connect.getAllHealth();
|
||||
return {
|
||||
connections: response.connections || [],
|
||||
summary: response.summary,
|
||||
};
|
||||
} catch (error) {
|
||||
log('connectGetAllHealth error: %O', error);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `Failed to get connections health: ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get authorize URL for a provider
|
||||
* This calls the SDK's authorize method which generates a secure authorization URL
|
||||
*/
|
||||
connectGetAuthorizeUrl: lobehubSkillAuthProcedure
|
||||
.input(
|
||||
z.object({
|
||||
provider: z.string(),
|
||||
redirectUri: z.string().optional(),
|
||||
scopes: z.array(z.string()).optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
log('connectGetAuthorizeUrl: provider=%s', input.provider);
|
||||
|
||||
try {
|
||||
const response = await ctx.marketSDK.connect.authorize(input.provider, {
|
||||
redirect_uri: input.redirectUri,
|
||||
scopes: input.scopes,
|
||||
});
|
||||
|
||||
return {
|
||||
authorizeUrl: response.authorize_url,
|
||||
code: response.code,
|
||||
expiresIn: response.expires_in,
|
||||
};
|
||||
} catch (error) {
|
||||
log('connectGetAuthorizeUrl error: %O', error);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `Failed to get authorize URL: ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get connection status for a provider
|
||||
*/
|
||||
connectGetStatus: lobehubSkillAuthProcedure
|
||||
.input(z.object({ provider: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
log('connectGetStatus: provider=%s', input.provider);
|
||||
|
||||
try {
|
||||
const response = await ctx.marketSDK.connect.getStatus(input.provider);
|
||||
return {
|
||||
connected: response.connected,
|
||||
connection: response.connection,
|
||||
icon: (response as any).icon,
|
||||
providerName: (response as any).providerName,
|
||||
};
|
||||
} catch (error) {
|
||||
log('connectGetStatus error: %O', error);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `Failed to get status: ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* List all user connections
|
||||
*/
|
||||
connectListConnections: lobehubSkillAuthProcedure.query(async ({ ctx }) => {
|
||||
log('connectListConnections');
|
||||
|
||||
try {
|
||||
const response = await ctx.marketSDK.connect.listConnections();
|
||||
// Debug logging
|
||||
log('connectListConnections raw response: %O', response);
|
||||
log('connectListConnections connections: %O', response.connections);
|
||||
return {
|
||||
connections: response.connections || [],
|
||||
};
|
||||
} catch (error) {
|
||||
log('connectListConnections error: %O', error);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `Failed to list connections: ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* List available providers (public, no auth required)
|
||||
*/
|
||||
connectListProviders: lobehubSkillBaseProcedure.query(async ({ ctx }) => {
|
||||
log('connectListProviders');
|
||||
|
||||
try {
|
||||
const response = await ctx.marketSDK.skills.listProviders();
|
||||
return {
|
||||
providers: response.providers || [],
|
||||
};
|
||||
} catch (error) {
|
||||
log('connectListProviders error: %O', error);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `Failed to list providers: ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* List tools for a provider
|
||||
*/
|
||||
connectListTools: lobehubSkillBaseProcedure
|
||||
.input(z.object({ provider: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
log('connectListTools: provider=%s', input.provider);
|
||||
|
||||
try {
|
||||
const response = await ctx.marketSDK.skills.listTools(input.provider);
|
||||
return {
|
||||
provider: input.provider,
|
||||
tools: response.tools || [],
|
||||
};
|
||||
} catch (error) {
|
||||
log('connectListTools error: %O', error);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `Failed to list tools: ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Refresh token for a provider
|
||||
*/
|
||||
connectRefresh: lobehubSkillAuthProcedure
|
||||
.input(z.object({ provider: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
log('connectRefresh: provider=%s', input.provider);
|
||||
|
||||
try {
|
||||
const response = await ctx.marketSDK.connect.refresh(input.provider);
|
||||
return {
|
||||
connection: response.connection,
|
||||
refreshed: response.refreshed,
|
||||
};
|
||||
} catch (error) {
|
||||
log('connectRefresh error: %O', error);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `Failed to refresh token: ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Revoke connection for a provider
|
||||
*/
|
||||
connectRevoke: lobehubSkillAuthProcedure
|
||||
.input(z.object({ provider: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
log('connectRevoke: provider=%s', input.provider);
|
||||
|
||||
try {
|
||||
await ctx.marketSDK.connect.revoke(input.provider);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
log('connectRevoke error: %O', error);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `Failed to revoke connection: ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Export a file from sandbox and upload to S3, then create a persistent file record
|
||||
* This combines the previous getExportFileUploadUrl + callCodeInterpreterTool + createFileRecord flow
|
||||
|
||||
@@ -6,7 +6,11 @@ import { type StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import { type ChatStore } from '@/store/chat/store';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { klavisStoreSelectors, pluginSelectors } from '@/store/tool/selectors';
|
||||
import {
|
||||
klavisStoreSelectors,
|
||||
lobehubSkillStoreSelectors,
|
||||
pluginSelectors,
|
||||
} from '@/store/tool/selectors';
|
||||
import { builtinTools } from '@/tools';
|
||||
|
||||
/**
|
||||
@@ -34,7 +38,7 @@ export const pluginInternals: StateCreator<
|
||||
const manifests: Record<string, LobeChatPluginManifest> = {};
|
||||
|
||||
// Track source for each identifier
|
||||
const sourceMap: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis'> = {};
|
||||
const sourceMap: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis' | 'lobehubSkill'> = {};
|
||||
|
||||
// Get all installed plugins
|
||||
const installedPlugins = pluginSelectors.installedPlugins(toolStoreState);
|
||||
@@ -63,6 +67,15 @@ export const pluginInternals: StateCreator<
|
||||
}
|
||||
}
|
||||
|
||||
// Get all LobeHub Skill tools
|
||||
const lobehubSkillTools = lobehubSkillStoreSelectors.lobehubSkillAsLobeTools(toolStoreState);
|
||||
for (const tool of lobehubSkillTools) {
|
||||
if (tool.manifest) {
|
||||
manifests[tool.identifier] = tool.manifest as LobeChatPluginManifest;
|
||||
sourceMap[tool.identifier] = 'lobehubSkill';
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve tool calls and add source field
|
||||
const resolved = toolNameResolver.resolve(toolCalls, manifests);
|
||||
return resolved.map((payload) => ({
|
||||
|
||||
@@ -60,6 +60,14 @@ export interface PluginTypesAction {
|
||||
*/
|
||||
invokeKlavisTypePlugin: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Invoke LobeHub Skill type plugin
|
||||
*/
|
||||
invokeLobehubSkillTypePlugin: (
|
||||
id: string,
|
||||
payload: ChatToolPayload,
|
||||
) => Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Invoke markdown type plugin
|
||||
*/
|
||||
@@ -93,6 +101,11 @@ export const pluginTypes: StateCreator<
|
||||
return await get().invokeKlavisTypePlugin(id, payload);
|
||||
}
|
||||
|
||||
// Check if this is a LobeHub Skill tool by source field
|
||||
if (payload.source === 'lobehubSkill') {
|
||||
return await get().invokeLobehubSkillTypePlugin(id, payload);
|
||||
}
|
||||
|
||||
// Check if this is Cloud Code Interpreter - route to specific handler
|
||||
if (payload.identifier === CloudSandboxIdentifier) {
|
||||
return await get().invokeCloudCodeInterpreterTool(id, payload);
|
||||
@@ -439,6 +452,97 @@ export const pluginTypes: StateCreator<
|
||||
return data.content;
|
||||
},
|
||||
|
||||
invokeLobehubSkillTypePlugin: async (id, payload) => {
|
||||
let data: MCPToolCallResult | undefined;
|
||||
|
||||
// Get message to extract sessionId/topicId
|
||||
const message = dbMessageSelectors.getDbMessageById(id)(get());
|
||||
|
||||
// Get abort controller from operation
|
||||
const operationId = get().messageOperationMap[id];
|
||||
const operation = operationId ? get().operations[operationId] : undefined;
|
||||
const abortController = operation?.abortController;
|
||||
|
||||
log(
|
||||
'[invokeLobehubSkillTypePlugin] messageId=%s, tool=%s, operationId=%s, aborted=%s',
|
||||
id,
|
||||
payload.apiName,
|
||||
operationId,
|
||||
abortController?.signal.aborted,
|
||||
);
|
||||
|
||||
try {
|
||||
// payload.identifier is the provider id (e.g., 'linear', 'microsoft')
|
||||
const provider = payload.identifier;
|
||||
|
||||
// Parse arguments
|
||||
const args = safeParseJSON(payload.arguments) || {};
|
||||
|
||||
// Call LobeHub Skill tool via store action
|
||||
const result = await useToolStore.getState().callLobehubSkillTool({
|
||||
args,
|
||||
provider,
|
||||
toolName: payload.apiName,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'LobeHub Skill tool execution failed');
|
||||
}
|
||||
|
||||
// Convert to MCPToolCallResult format
|
||||
const content = typeof result.data === 'string' ? result.data : JSON.stringify(result.data);
|
||||
data = {
|
||||
content,
|
||||
error: undefined,
|
||||
state: { content: [{ text: content, type: 'text' }] },
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[invokeLobehubSkillTypePlugin] Error:', error);
|
||||
|
||||
// ignore the aborted request error
|
||||
const err = error as Error;
|
||||
if (err.message.includes('aborted')) {
|
||||
log(
|
||||
'[invokeLobehubSkillTypePlugin] Request aborted: messageId=%s, tool=%s',
|
||||
id,
|
||||
payload.apiName,
|
||||
);
|
||||
} else {
|
||||
const result = await messageService.updateMessageError(id, error as any, {
|
||||
agentId: message?.agentId,
|
||||
topicId: message?.topicId,
|
||||
});
|
||||
if (result?.success && result.messages) {
|
||||
get().replaceMessages(result.messages, {
|
||||
context: {
|
||||
agentId: message?.agentId,
|
||||
topicId: message?.topicId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If error occurred, exit
|
||||
if (!data) return;
|
||||
|
||||
const context = operationId ? { operationId } : undefined;
|
||||
|
||||
// Use optimisticUpdateToolMessage to update content and state/error in a single call
|
||||
await get().optimisticUpdateToolMessage(
|
||||
id,
|
||||
{
|
||||
content: data.content,
|
||||
pluginError: data.success ? undefined : data.error,
|
||||
pluginState: data.success ? data.state : undefined,
|
||||
},
|
||||
context,
|
||||
);
|
||||
|
||||
return data.content;
|
||||
},
|
||||
|
||||
invokeMarkdownTypePlugin: async (id, payload) => {
|
||||
const { internal_callPluginApi } = get();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export const serverConfigSelectors = {
|
||||
enableEmailVerification: (s: ServerConfigStore) =>
|
||||
s.serverConfig.enableEmailVerification || false,
|
||||
enableKlavis: (s: ServerConfigStore) => s.serverConfig.enableKlavis || false,
|
||||
enableLobehubSkill: (s: ServerConfigStore) => s.serverConfig.enableLobehubSkill || false,
|
||||
enableMagicLink: (s: ServerConfigStore) => s.serverConfig.enableMagicLink || false,
|
||||
enableMarketTrustedClient: (s: ServerConfigStore) =>
|
||||
s.serverConfig.enableMarketTrustedClient || false,
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { type BuiltinToolState, initialBuiltinToolState } from './slices/builtin/initialState';
|
||||
import { type CustomPluginState, initialCustomPluginState } from './slices/customPlugin/initialState';
|
||||
import {
|
||||
type CustomPluginState,
|
||||
initialCustomPluginState,
|
||||
} from './slices/customPlugin/initialState';
|
||||
import { type KlavisStoreState, initialKlavisStoreState } from './slices/klavisStore/initialState';
|
||||
import {
|
||||
type LobehubSkillStoreState,
|
||||
initialLobehubSkillStoreState,
|
||||
} from './slices/lobehubSkillStore/initialState';
|
||||
import { type MCPStoreState, initialMCPStoreState } from './slices/mcpStore/initialState';
|
||||
import { type PluginStoreState, initialPluginStoreState } from './slices/oldStore/initialState';
|
||||
import { type PluginState, initialPluginState } from './slices/plugin/initialState';
|
||||
@@ -10,7 +17,8 @@ export type ToolStoreState = PluginState &
|
||||
PluginStoreState &
|
||||
BuiltinToolState &
|
||||
MCPStoreState &
|
||||
KlavisStoreState;
|
||||
KlavisStoreState &
|
||||
LobehubSkillStoreState;
|
||||
|
||||
export const initialState: ToolStoreState = {
|
||||
...initialPluginState,
|
||||
@@ -19,4 +27,5 @@ export const initialState: ToolStoreState = {
|
||||
...initialBuiltinToolState,
|
||||
...initialMCPStoreState,
|
||||
...initialKlavisStoreState,
|
||||
...initialLobehubSkillStoreState,
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ export {
|
||||
} from '../slices/builtin/selectors';
|
||||
export { customPluginSelectors } from '../slices/customPlugin/selectors';
|
||||
export { klavisStoreSelectors } from '../slices/klavisStore/selectors';
|
||||
export { lobehubSkillStoreSelectors } from '../slices/lobehubSkillStore/selectors';
|
||||
export { mcpStoreSelectors } from '../slices/mcpStore/selectors';
|
||||
export { pluginStoreSelectors } from '../slices/oldStore/selectors';
|
||||
export { pluginSelectors } from '../slices/plugin/selectors';
|
||||
|
||||
@@ -6,12 +6,14 @@ import { type LobeToolMeta } from '@/types/tool/tool';
|
||||
|
||||
import { type ToolStoreState } from '../initialState';
|
||||
import { builtinToolSelectors } from '../slices/builtin/selectors';
|
||||
import { lobehubSkillStoreSelectors } from '../slices/lobehubSkillStore/selectors';
|
||||
import { pluginSelectors } from '../slices/plugin/selectors';
|
||||
|
||||
const metaList = (s: ToolStoreState): LobeToolMeta[] => {
|
||||
const pluginList = pluginSelectors.installedPluginMetaList(s) as LobeToolMeta[];
|
||||
const lobehubSkillList = lobehubSkillStoreSelectors.metaList(s) as LobeToolMeta[];
|
||||
|
||||
return builtinToolSelectors.metaList(s).concat(pluginList);
|
||||
return builtinToolSelectors.metaList(s).concat(pluginList).concat(lobehubSkillList);
|
||||
};
|
||||
|
||||
const getMetaById =
|
||||
|
||||
361
src/store/tool/slices/lobehubSkillStore/action.ts
Normal file
361
src/store/tool/slices/lobehubSkillStore/action.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
import { getLobehubSkillProviderById } from '@lobechat/const';
|
||||
import { enableMapSet, produce } from 'immer';
|
||||
import useSWR, { type SWRResponse } from 'swr';
|
||||
import { type StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import { toolsClient } from '@/libs/trpc/client';
|
||||
import { setNamespace } from '@/utils/storeDebug';
|
||||
|
||||
import { type ToolStore } from '../../store';
|
||||
import { type LobehubSkillStoreState } from './initialState';
|
||||
import {
|
||||
type CallLobehubSkillToolParams,
|
||||
type CallLobehubSkillToolResult,
|
||||
type LobehubSkillServer,
|
||||
LobehubSkillStatus,
|
||||
type LobehubSkillTool,
|
||||
} from './types';
|
||||
|
||||
enableMapSet();
|
||||
|
||||
const n = setNamespace('lobehubSkillStore');
|
||||
|
||||
/**
|
||||
* LobeHub Skill Store Actions
|
||||
*/
|
||||
export interface LobehubSkillStoreAction {
|
||||
/**
|
||||
* 调用 LobeHub Skill 工具
|
||||
*/
|
||||
callLobehubSkillTool: (params: CallLobehubSkillToolParams) => Promise<CallLobehubSkillToolResult>;
|
||||
|
||||
/**
|
||||
* 获取单个 Provider 的连接状态
|
||||
* @param provider - Provider ID (如 'linear')
|
||||
*/
|
||||
checkLobehubSkillStatus: (provider: string) => Promise<LobehubSkillServer | undefined>;
|
||||
|
||||
/**
|
||||
* 获取 Provider 的授权信息(URL、code、过期时间)
|
||||
* @param provider - Provider ID (如 'linear')
|
||||
* @param options - 可选的 scopes 和 redirectUri
|
||||
* @returns 授权 URL 和相关信息
|
||||
*/
|
||||
getLobehubSkillAuthorizeUrl: (
|
||||
provider: string,
|
||||
options?: { redirectUri?: string; scopes?: string[] },
|
||||
) => Promise<{ authorizeUrl: string; code: string; expiresIn: number }>;
|
||||
|
||||
/**
|
||||
* 内部方法: 更新 Server 状态
|
||||
*/
|
||||
internal_updateLobehubSkillServer: (
|
||||
provider: string,
|
||||
update: Partial<LobehubSkillServer>,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* 刷新 Provider 的 Token (如果支持)
|
||||
* @param provider - Provider ID
|
||||
*/
|
||||
refreshLobehubSkillToken: (provider: string) => Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 刷新 Provider 的工具列表
|
||||
* @param provider - Provider ID
|
||||
*/
|
||||
refreshLobehubSkillTools: (provider: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 断开 Provider 连接
|
||||
* @param provider - Provider ID
|
||||
*/
|
||||
revokeLobehubSkill: (provider: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 使用 SWR 获取用户的所有连接状态
|
||||
* @param enabled - 是否启用获取
|
||||
*/
|
||||
useFetchLobehubSkillConnections: (enabled: boolean) => SWRResponse<LobehubSkillServer[]>;
|
||||
}
|
||||
|
||||
export const createLobehubSkillStoreSlice: StateCreator<
|
||||
ToolStore,
|
||||
[['zustand/devtools', never]],
|
||||
[],
|
||||
LobehubSkillStoreAction
|
||||
> = (set, get) => ({
|
||||
callLobehubSkillTool: async (params) => {
|
||||
const { provider, toolName, args } = params;
|
||||
const toolId = `${provider}:${toolName}`;
|
||||
|
||||
set(
|
||||
produce((draft: LobehubSkillStoreState) => {
|
||||
draft.lobehubSkillExecutingToolIds.add(toolId);
|
||||
}),
|
||||
false,
|
||||
n('callLobehubSkillTool/start'),
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await toolsClient.market.connectCallTool.mutate({
|
||||
args,
|
||||
provider,
|
||||
toolName,
|
||||
});
|
||||
|
||||
set(
|
||||
produce((draft: LobehubSkillStoreState) => {
|
||||
draft.lobehubSkillExecutingToolIds.delete(toolId);
|
||||
}),
|
||||
false,
|
||||
n('callLobehubSkillTool/success'),
|
||||
);
|
||||
|
||||
return { data: response.data, success: true };
|
||||
} catch (error) {
|
||||
console.error('[LobehubSkill] Failed to call tool:', error);
|
||||
|
||||
set(
|
||||
produce((draft: LobehubSkillStoreState) => {
|
||||
draft.lobehubSkillExecutingToolIds.delete(toolId);
|
||||
}),
|
||||
false,
|
||||
n('callLobehubSkillTool/error'),
|
||||
);
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
if (errorMessage.includes('NOT_CONNECTED') || errorMessage.includes('TOKEN_EXPIRED')) {
|
||||
return {
|
||||
error: errorMessage,
|
||||
errorCode: 'NOT_CONNECTED',
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
error: errorMessage,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
checkLobehubSkillStatus: async (provider) => {
|
||||
set(
|
||||
produce((draft: LobehubSkillStoreState) => {
|
||||
draft.lobehubSkillLoadingIds.add(provider);
|
||||
}),
|
||||
false,
|
||||
n('checkLobehubSkillStatus/start'),
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await toolsClient.market.connectGetStatus.query({ provider });
|
||||
// Get provider config from local definition for correct display name
|
||||
const providerConfig = getLobehubSkillProviderById(provider);
|
||||
|
||||
const server: LobehubSkillServer = {
|
||||
cachedAt: Date.now(),
|
||||
icon: response.icon,
|
||||
identifier: provider,
|
||||
isConnected: response.connected,
|
||||
// Use local config label (e.g., "Linear") instead of API's providerName
|
||||
name: providerConfig?.label || provider,
|
||||
providerUsername: response.connection?.providerUsername,
|
||||
scopes: response.connection?.scopes,
|
||||
status: response.connected
|
||||
? LobehubSkillStatus.CONNECTED
|
||||
: LobehubSkillStatus.NOT_CONNECTED,
|
||||
tokenExpiresAt: response.connection?.tokenExpiresAt,
|
||||
};
|
||||
|
||||
set(
|
||||
produce((draft: LobehubSkillStoreState) => {
|
||||
const existingIndex = draft.lobehubSkillServers.findIndex(
|
||||
(s) => s.identifier === provider,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
draft.lobehubSkillServers[existingIndex] = server;
|
||||
} else {
|
||||
draft.lobehubSkillServers.push(server);
|
||||
}
|
||||
draft.lobehubSkillLoadingIds.delete(provider);
|
||||
}),
|
||||
false,
|
||||
n('checkLobehubSkillStatus/success'),
|
||||
);
|
||||
|
||||
if (server.isConnected) {
|
||||
get().refreshLobehubSkillTools(provider);
|
||||
}
|
||||
|
||||
return server;
|
||||
} catch (error) {
|
||||
console.error('[LobehubSkill] Failed to check status:', error);
|
||||
|
||||
set(
|
||||
produce((draft: LobehubSkillStoreState) => {
|
||||
draft.lobehubSkillLoadingIds.delete(provider);
|
||||
}),
|
||||
false,
|
||||
n('checkLobehubSkillStatus/error'),
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
getLobehubSkillAuthorizeUrl: async (provider, options) => {
|
||||
const response = await toolsClient.market.connectGetAuthorizeUrl.query({
|
||||
provider,
|
||||
redirectUri: options?.redirectUri,
|
||||
scopes: options?.scopes,
|
||||
});
|
||||
|
||||
return {
|
||||
authorizeUrl: response.authorizeUrl,
|
||||
code: response.code,
|
||||
expiresIn: response.expiresIn,
|
||||
};
|
||||
},
|
||||
|
||||
internal_updateLobehubSkillServer: (provider, update) => {
|
||||
set(
|
||||
produce((draft: LobehubSkillStoreState) => {
|
||||
const serverIndex = draft.lobehubSkillServers.findIndex((s) => s.identifier === provider);
|
||||
if (serverIndex >= 0) {
|
||||
draft.lobehubSkillServers[serverIndex] = {
|
||||
...draft.lobehubSkillServers[serverIndex],
|
||||
...update,
|
||||
};
|
||||
}
|
||||
}),
|
||||
false,
|
||||
n('internal_updateLobehubSkillServer'),
|
||||
);
|
||||
},
|
||||
|
||||
refreshLobehubSkillToken: async (provider) => {
|
||||
try {
|
||||
const response = await toolsClient.market.connectRefresh.mutate({ provider });
|
||||
|
||||
if (response.refreshed) {
|
||||
get().internal_updateLobehubSkillServer(provider, {
|
||||
status: LobehubSkillStatus.CONNECTED,
|
||||
tokenExpiresAt: response.connection?.tokenExpiresAt,
|
||||
});
|
||||
}
|
||||
|
||||
return response.refreshed;
|
||||
} catch (error) {
|
||||
console.error('[LobehubSkill] Failed to refresh token:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
refreshLobehubSkillTools: async (provider) => {
|
||||
try {
|
||||
const response = await toolsClient.market.connectListTools.query({ provider });
|
||||
|
||||
set(
|
||||
produce((draft: LobehubSkillStoreState) => {
|
||||
const serverIndex = draft.lobehubSkillServers.findIndex((s) => s.identifier === provider);
|
||||
if (serverIndex >= 0) {
|
||||
draft.lobehubSkillServers[serverIndex].tools = response.tools as LobehubSkillTool[];
|
||||
}
|
||||
}),
|
||||
false,
|
||||
n('refreshLobehubSkillTools/success'),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[LobehubSkill] Failed to refresh tools:', error);
|
||||
}
|
||||
},
|
||||
|
||||
revokeLobehubSkill: async (provider) => {
|
||||
set(
|
||||
produce((draft: LobehubSkillStoreState) => {
|
||||
draft.lobehubSkillLoadingIds.add(provider);
|
||||
}),
|
||||
false,
|
||||
n('revokeLobehubSkill/start'),
|
||||
);
|
||||
|
||||
try {
|
||||
await toolsClient.market.connectRevoke.mutate({ provider });
|
||||
|
||||
set(
|
||||
produce((draft: LobehubSkillStoreState) => {
|
||||
draft.lobehubSkillServers = draft.lobehubSkillServers.filter(
|
||||
(s) => s.identifier !== provider,
|
||||
);
|
||||
draft.lobehubSkillLoadingIds.delete(provider);
|
||||
}),
|
||||
false,
|
||||
n('revokeLobehubSkill/success'),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[LobehubSkill] Failed to revoke:', error);
|
||||
|
||||
set(
|
||||
produce((draft: LobehubSkillStoreState) => {
|
||||
draft.lobehubSkillLoadingIds.delete(provider);
|
||||
}),
|
||||
false,
|
||||
n('revokeLobehubSkill/error'),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
useFetchLobehubSkillConnections: (enabled) =>
|
||||
useSWR<LobehubSkillServer[]>(
|
||||
enabled ? 'fetchLobehubSkillConnections' : null,
|
||||
async () => {
|
||||
const response = await toolsClient.market.connectListConnections.query();
|
||||
|
||||
// Debug logging
|
||||
console.log('[useFetchLobehubSkillConnections] raw response:', response);
|
||||
|
||||
return response.connections.map((conn: any) => {
|
||||
// Debug logging for each connection
|
||||
console.log('[useFetchLobehubSkillConnections] connection:', conn);
|
||||
// Get provider config from local definition for correct display name
|
||||
const providerConfig = getLobehubSkillProviderById(conn.providerId);
|
||||
return {
|
||||
cachedAt: Date.now(),
|
||||
icon: conn.icon,
|
||||
identifier: conn.providerId,
|
||||
isConnected: true,
|
||||
// Use local config label (e.g., "Linear") instead of API's providerName (which is user's name on that service)
|
||||
name: providerConfig?.label || conn.providerId,
|
||||
providerUsername: conn.providerUsername,
|
||||
scopes: conn.scopes,
|
||||
status: LobehubSkillStatus.CONNECTED,
|
||||
tokenExpiresAt: conn.tokenExpiresAt,
|
||||
};
|
||||
});
|
||||
},
|
||||
{
|
||||
fallbackData: [],
|
||||
onSuccess: (data) => {
|
||||
if (data.length > 0) {
|
||||
set(
|
||||
produce((draft: LobehubSkillStoreState) => {
|
||||
const existingIds = new Set(draft.lobehubSkillServers.map((s) => s.identifier));
|
||||
const newServers = data.filter((s) => !existingIds.has(s.identifier));
|
||||
draft.lobehubSkillServers = [...draft.lobehubSkillServers, ...newServers];
|
||||
}),
|
||||
false,
|
||||
n('useFetchLobehubSkillConnections'),
|
||||
);
|
||||
|
||||
for (const server of data) {
|
||||
get().refreshLobehubSkillTools(server.identifier);
|
||||
}
|
||||
}
|
||||
},
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
),
|
||||
});
|
||||
4
src/store/tool/slices/lobehubSkillStore/index.ts
Normal file
4
src/store/tool/slices/lobehubSkillStore/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './action';
|
||||
export * from './initialState';
|
||||
export * from './selectors';
|
||||
export * from './types';
|
||||
24
src/store/tool/slices/lobehubSkillStore/initialState.ts
Normal file
24
src/store/tool/slices/lobehubSkillStore/initialState.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { type LobehubSkillServer } from './types';
|
||||
|
||||
/**
|
||||
* LobeHub Skill Store 状态接口
|
||||
*
|
||||
* NOTE: 所有连接状态和工具数据都从 Market API 实时获取,不存储到本地数据库
|
||||
*/
|
||||
export interface LobehubSkillStoreState {
|
||||
/** 正在执行的工具调用 ID 集合 */
|
||||
lobehubSkillExecutingToolIds: Set<string>;
|
||||
/** 正在加载的 Provider ID 集合 */
|
||||
lobehubSkillLoadingIds: Set<string>;
|
||||
/** 已连接的 LobeHub Skill Server 列表 */
|
||||
lobehubSkillServers: LobehubSkillServer[];
|
||||
}
|
||||
|
||||
/**
|
||||
* LobeHub Skill Store 初始状态
|
||||
*/
|
||||
export const initialLobehubSkillStoreState: LobehubSkillStoreState = {
|
||||
lobehubSkillExecutingToolIds: new Set(),
|
||||
lobehubSkillLoadingIds: new Set(),
|
||||
lobehubSkillServers: [],
|
||||
};
|
||||
145
src/store/tool/slices/lobehubSkillStore/selectors.ts
Normal file
145
src/store/tool/slices/lobehubSkillStore/selectors.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { type ToolStoreState } from '../../initialState';
|
||||
import { type LobehubSkillServer, LobehubSkillStatus } from './types';
|
||||
|
||||
/**
|
||||
* LobeHub Skill Store Selectors
|
||||
*/
|
||||
export const lobehubSkillStoreSelectors = {
|
||||
/**
|
||||
* 获取所有 LobeHub Skill 服务器的 identifier 集合
|
||||
*/
|
||||
getAllServerIdentifiers: (s: ToolStoreState): Set<string> => {
|
||||
const servers = s.lobehubSkillServers || [];
|
||||
return new Set(servers.map((server) => server.identifier));
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取所有可用的工具(来自所有已连接的服务器)
|
||||
*/
|
||||
getAllTools: (s: ToolStoreState) => {
|
||||
const connectedServers = lobehubSkillStoreSelectors.getConnectedServers(s);
|
||||
return connectedServers.flatMap((server) =>
|
||||
(server.tools || []).map((tool) => ({
|
||||
...tool,
|
||||
provider: server.identifier,
|
||||
})),
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取所有已连接的服务器
|
||||
*/
|
||||
getConnectedServers: (s: ToolStoreState): LobehubSkillServer[] =>
|
||||
(s.lobehubSkillServers || []).filter(
|
||||
(server) => server.status === LobehubSkillStatus.CONNECTED,
|
||||
),
|
||||
|
||||
/**
|
||||
* 根据 identifier 获取服务器
|
||||
* @param identifier - Provider 标识符 (e.g., 'linear')
|
||||
*/
|
||||
getServerByIdentifier: (identifier: string) => (s: ToolStoreState) =>
|
||||
s.lobehubSkillServers?.find((server) => server.identifier === identifier),
|
||||
|
||||
/**
|
||||
* 获取所有 LobeHub Skill 服务器
|
||||
*/
|
||||
getServers: (s: ToolStoreState): LobehubSkillServer[] => s.lobehubSkillServers || [],
|
||||
|
||||
/**
|
||||
* 检查给定的 identifier 是否是 LobeHub Skill 服务器
|
||||
* @param identifier - Provider 标识符 (e.g., 'linear')
|
||||
*/
|
||||
isLobehubSkillServer:
|
||||
(identifier: string) =>
|
||||
(s: ToolStoreState): boolean => {
|
||||
const servers = s.lobehubSkillServers || [];
|
||||
return servers.some((server) => server.identifier === identifier);
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查服务器是否正在加载
|
||||
* @param identifier - Provider 标识符 (e.g., 'linear')
|
||||
*/
|
||||
isServerLoading: (identifier: string) => (s: ToolStoreState) =>
|
||||
s.lobehubSkillLoadingIds?.has(identifier) || false,
|
||||
|
||||
/**
|
||||
* 检查工具是否正在执行
|
||||
*/
|
||||
isToolExecuting: (provider: string, toolName: string) => (s: ToolStoreState) => {
|
||||
const toolId = `${provider}:${toolName}`;
|
||||
return s.lobehubSkillExecutingToolIds?.has(toolId) || false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all LobeHub Skill tools as LobeTool format for agent use
|
||||
* Converts LobeHub Skill tools into the format expected by ToolNameResolver
|
||||
*/
|
||||
lobehubSkillAsLobeTools: (s: ToolStoreState) => {
|
||||
const servers = s.lobehubSkillServers || [];
|
||||
const tools: any[] = [];
|
||||
|
||||
for (const server of servers) {
|
||||
if (!server.tools || server.status !== LobehubSkillStatus.CONNECTED) continue;
|
||||
|
||||
const apis = server.tools.map((tool) => ({
|
||||
description: tool.description || '',
|
||||
name: tool.name,
|
||||
parameters: tool.inputSchema || {},
|
||||
}));
|
||||
|
||||
if (apis.length > 0) {
|
||||
tools.push({
|
||||
identifier: server.identifier,
|
||||
manifest: {
|
||||
api: apis,
|
||||
author: 'LobeHub Market',
|
||||
homepage: 'https://lobehub.com/market',
|
||||
identifier: server.identifier,
|
||||
meta: {
|
||||
avatar: server.icon || '🔗',
|
||||
description: `LobeHub Skill: ${server.name}`,
|
||||
tags: ['lobehub-skill', server.identifier],
|
||||
title: server.name,
|
||||
},
|
||||
type: 'builtin',
|
||||
version: '1.0.0',
|
||||
},
|
||||
type: 'plugin',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get metadata list for all connected LobeHub Skill servers
|
||||
* Used by toolSelectors.metaList for unified tool metadata resolution
|
||||
*/
|
||||
metaList: (s: ToolStoreState) => {
|
||||
const servers = s.lobehubSkillServers || [];
|
||||
const result = servers
|
||||
.filter((server) => server.status === LobehubSkillStatus.CONNECTED)
|
||||
.map((server) => {
|
||||
// Debug logging
|
||||
console.log('[lobehubSkillStoreSelectors.metaList] server:', {
|
||||
icon: server.icon,
|
||||
identifier: server.identifier,
|
||||
name: server.name,
|
||||
status: server.status,
|
||||
});
|
||||
return {
|
||||
identifier: server.identifier,
|
||||
meta: {
|
||||
avatar: server.icon || '🔗',
|
||||
description: `LobeHub Skill: ${server.name}`,
|
||||
title: server.name,
|
||||
},
|
||||
};
|
||||
});
|
||||
console.log('[lobehubSkillStoreSelectors.metaList] result:', result);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
100
src/store/tool/slices/lobehubSkillStore/types.ts
Normal file
100
src/store/tool/slices/lobehubSkillStore/types.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* LobeHub Skill Server 连接状态
|
||||
*/
|
||||
export enum LobehubSkillStatus {
|
||||
/** 已连接,可以使用 */
|
||||
CONNECTED = 'connected',
|
||||
/** 连接中 */
|
||||
CONNECTING = 'connecting',
|
||||
/** 连接失败或 Token 过期 */
|
||||
ERROR = 'error',
|
||||
/** 未连接 */
|
||||
NOT_CONNECTED = 'not_connected',
|
||||
}
|
||||
|
||||
/**
|
||||
* LobeHub Skill Tool 定义 (来自 Market API)
|
||||
*/
|
||||
export interface LobehubSkillTool {
|
||||
/** 工具描述 */
|
||||
description?: string;
|
||||
/** 工具输入的 JSON Schema */
|
||||
inputSchema: {
|
||||
additionalProperties?: boolean;
|
||||
properties?: Record<string, any>;
|
||||
required?: string[];
|
||||
type: string;
|
||||
};
|
||||
/** 工具名称 */
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* LobeHub Skill Provider 定义 (来自 Market API)
|
||||
*/
|
||||
export interface LobehubSkillProvider {
|
||||
/** Provider 图标 URL */
|
||||
icon?: string;
|
||||
/** Provider ID (如 'linear', 'github') */
|
||||
id: string;
|
||||
/** 显示名称 */
|
||||
name: string;
|
||||
/** 是否支持刷新 Token */
|
||||
refreshSupported?: boolean;
|
||||
/** Provider 类型 */
|
||||
type?: 'mcp' | 'rest';
|
||||
}
|
||||
|
||||
/**
|
||||
* LobeHub Skill Server 实例 (用户已连接的 provider)
|
||||
*/
|
||||
export interface LobehubSkillServer {
|
||||
/** 缓存时间戳 */
|
||||
cachedAt?: number;
|
||||
/** 错误信息 */
|
||||
errorMessage?: string;
|
||||
/** Provider 图标 URL */
|
||||
icon?: string;
|
||||
/** Provider ID (如 'linear') */
|
||||
identifier: string;
|
||||
/** 是否已认证 */
|
||||
isConnected: boolean;
|
||||
/** Provider 显示名称 */
|
||||
name: string;
|
||||
/** Provider 用户名 (如 GitHub username) */
|
||||
providerUsername?: string;
|
||||
/** 授权的 scopes */
|
||||
scopes?: string[];
|
||||
/** 连接状态 */
|
||||
status: LobehubSkillStatus;
|
||||
/** Token 过期时间 */
|
||||
tokenExpiresAt?: string;
|
||||
/** 工具列表 (已连接后可用) */
|
||||
tools?: LobehubSkillTool[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 LobeHub Skill 工具的参数
|
||||
*/
|
||||
export interface CallLobehubSkillToolParams {
|
||||
/** 工具参数 */
|
||||
args?: Record<string, unknown>;
|
||||
/** Provider ID (如 'linear') */
|
||||
provider: string;
|
||||
/** 工具名称 */
|
||||
toolName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 LobeHub Skill 工具的结果
|
||||
*/
|
||||
export interface CallLobehubSkillToolResult {
|
||||
/** 返回数据 */
|
||||
data?: any;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 错误代码 */
|
||||
errorCode?: string;
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
}
|
||||
@@ -7,9 +7,13 @@ import { type ToolStoreState, initialState } from './initialState';
|
||||
import { type BuiltinToolAction, createBuiltinToolSlice } from './slices/builtin';
|
||||
import { type CustomPluginAction, createCustomPluginSlice } from './slices/customPlugin';
|
||||
import { type KlavisStoreAction, createKlavisStoreSlice } from './slices/klavisStore';
|
||||
import {
|
||||
type LobehubSkillStoreAction,
|
||||
createLobehubSkillStoreSlice,
|
||||
} from './slices/lobehubSkillStore';
|
||||
import { type PluginMCPStoreAction, createMCPPluginStoreSlice } from './slices/mcpStore';
|
||||
import { type PluginAction, createPluginSlice } from './slices/plugin';
|
||||
import { type PluginStoreAction, createPluginStoreSlice } from './slices/oldStore';
|
||||
import { type PluginAction, createPluginSlice } from './slices/plugin';
|
||||
|
||||
// =============== Aggregate createStoreFn ============ //
|
||||
|
||||
@@ -19,7 +23,8 @@ export type ToolStore = ToolStoreState &
|
||||
PluginStoreAction &
|
||||
BuiltinToolAction &
|
||||
PluginMCPStoreAction &
|
||||
KlavisStoreAction;
|
||||
KlavisStoreAction &
|
||||
LobehubSkillStoreAction;
|
||||
|
||||
const createStore: StateCreator<ToolStore, [['zustand/devtools', never]]> = (...parameters) => ({
|
||||
...initialState,
|
||||
@@ -29,6 +34,7 @@ const createStore: StateCreator<ToolStore, [['zustand/devtools', never]]> = (...
|
||||
...createBuiltinToolSlice(...parameters),
|
||||
...createMCPPluginStoreSlice(...parameters),
|
||||
...createKlavisStoreSlice(...parameters),
|
||||
...createLobehubSkillStoreSlice(...parameters),
|
||||
});
|
||||
|
||||
// =============== Implement useStore ============ //
|
||||
|
||||
Reference in New Issue
Block a user