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:
Shinji-Li
2026-01-07 22:13:19 +08:00
committed by GitHub
parent 70b34d5f3c
commit a4003a383b
28 changed files with 1510 additions and 21 deletions

View File

@@ -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}}",

View File

@@ -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}}",

View File

@@ -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",

View File

@@ -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';

View 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);

View File

@@ -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",

View File

@@ -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;

View File

@@ -51,6 +51,7 @@ export interface GlobalServerConfig {
defaultAgent?: PartialDeep<UserDefaultAgent>;
enableEmailVerification?: boolean;
enableKlavis?: boolean;
enableLobehubSkill?: boolean;
enableMagicLink?: boolean;
enableMarketTrustedClient?: boolean;
enableUploadFileToServer?: boolean;

View File

@@ -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;

View File

@@ -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 };
};

View File

@@ -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(

View File

@@ -70,6 +70,9 @@ vi.mock('@/store/tool/selectors', () => ({
klavisStoreSelectors: {
klavisAsLobeTools: () => [],
},
lobehubSkillStoreSelectors: {
lobehubSkillAsLobeTools: () => [],
},
}));
vi.mock('../isCanUseFC', () => ({

View File

@@ -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,
];

View File

@@ -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.',

View File

@@ -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

View File

@@ -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

View File

@@ -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) => ({

View File

@@ -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();

View File

@@ -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,

View File

@@ -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,
};

View File

@@ -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';

View File

@@ -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 =

View 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,
},
),
});

View File

@@ -0,0 +1,4 @@
export * from './action';
export * from './initialState';
export * from './selectors';
export * from './types';

View 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: [],
};

View 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;
},
};

View 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;
}

View File

@@ -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 ============ //