Compare commits

...

12 Commits

Author SHA1 Message Date
ONLY-yours d10a8461e4 🌐 i18n: add googleDataProtection locale keys to setting namespace
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 14:10:18 +08:00
ONLY-yours f0dc739a96 🔀 chore: merge canary into fix/google-dev-test-oauth
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:59:19 +08:00
ONLY-yours bae5c45733 Merge remote-tracking branch 'origin/canary' into fix/google-dev-test-oauth
# Conflicts:
#	src/routes/(main)/agent/features/Conversation/ConversationArea.tsx
2026-04-28 15:44:32 +08:00
ONLY-yours e9f9f55b35 fix: add the klavis gmail drive & docs all callback 2026-03-31 15:06:23 +08:00
ONLY-yours 1892a3e57b Merge remote-tracking branch 'origin/canary' into fix/google-oauth-check-new 2026-03-31 13:17:54 +08:00
ONLY-yours cb5f90b569 Revert "feat: change the klavis docs & sheets into lobehub skill"
This reverts commit d98e5d9d6d.
2026-03-31 12:27:01 +08:00
ONLY-yours d98e5d9d6d feat: change the klavis docs & sheets into lobehub skill 2026-02-26 14:30:07 +08:00
ONLY-yours a76d8c4e57 fix: slove the skillStore not open problem 2026-02-25 09:22:02 +08:00
ONLY-yours 54a0edb7a2 feat: close the klavis gmail & use lobebhub gmail 2026-02-24 14:48:03 +08:00
ONLY-yours 95cf32ef56 fix: update ts 2026-02-24 12:25:10 +08:00
ONLY-yours 76ce969959 fix: add the block when use google oauth tools, not use deepseek & grok
- Add googleDataProtection const with restricted providers/models check
- Add useGoogleDataProtection hook for checking Google tool usage conflicts
- Add useAgentGoogleProtectionHooks for conversation-level protection
- Add i18n translations for protection warning messages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-24 11:33:22 +08:00
ONLY-yours 503cc8b300 fix: google drive & modify gmail only have gmail send
- Comment out Google Drive from KLAVIS_SERVER_TYPES
- Add tool filtering for Gmail to only allow gmail_send_email

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-24 11:24:59 +08:00
11 changed files with 331 additions and 7 deletions
+9
View File
@@ -284,6 +284,15 @@
"header.groupDesc": "管理群组与对话偏好",
"header.session": "会话设置",
"header.sessionDesc": "助理档案与会话偏好",
"googleDataProtection.cannotConnectGoogle.content": "您正在使用非 Google 模型。连接 Google 工具(Gmail、Drive、Docs)可能会将您的数据暴露给第三方服务商。请先切换到 Google 模型后再启用这些工具。",
"googleDataProtection.cannotConnectGoogle.title": "无法连接 Google 工具",
"googleDataProtection.cannotSendMessage.content": "您已启用 Google 工具({{tools}}),但正在使用非 Google 模型。发送消息可能会将您的 Google 数据暴露给第三方服务商。请禁用 Google 工具或切换到 Google 模型。",
"googleDataProtection.cannotSendMessage.title": "无法发送消息",
"googleDataProtection.cannotSendWithHistory.content": "您的对话历史中包含 Google 工具的使用记录({{tools}}),但您当前正在使用非 Google 模型。发送消息可能会将您的 Google 数据暴露给第三方服务商。请切换到 Google 模型。",
"googleDataProtection.cannotSendWithHistory.title": "无法发送消息",
"googleDataProtection.cannotSwitchModel.content": "您已启用 Google 工具({{tools}})。切换到非 Google 模型可能会将您的 Google 数据暴露给第三方服务商。请先禁用 Google 工具后再切换模型。",
"googleDataProtection.cannotSwitchModel.title": "无法切换模型",
"googleDataProtection.understood": "我已了解",
"header.sessionWithName": "会话设置 · {{name}}",
"header.title": "设置",
"heterogeneousStatus.account.label": "账号",
+2 -2
View File
@@ -33,9 +33,9 @@ export const KLAVIS_SERVER_TYPES: KlavisServerType[] = [
description: 'Gmail is a free email service provided by Google',
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/gmail.svg',
identifier: 'gmail',
label: 'Gmail',
readme:
'Bring the power of Gmail directly into your AI assistant. Read, compose, and send emails, search your inbox, manage labels, and organize your communications—all through natural conversation.',
label: 'Gmail',
serverName: Klavis.McpServerName.Gmail,
},
{
@@ -102,9 +102,9 @@ export const KLAVIS_SERVER_TYPES: KlavisServerType[] = [
description: 'Google Drive is a cloud storage service',
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/googledrive.svg',
identifier: 'google-drive',
label: 'Google Drive',
readme:
'Connect to Google Drive to access, organize, and manage your files. Search documents, upload files, share content, and navigate your cloud storage efficiently through AI assistance.',
label: 'Google Drive',
serverName: Klavis.McpServerName.GoogleDrive,
},
{
+38
View File
@@ -0,0 +1,38 @@
export const GOOGLE_RESTRICTED_PROVIDER_IDS = ['xai', 'deepseek'] as const;
export type GoogleRestrictedProvider = (typeof GOOGLE_RESTRICTED_PROVIDER_IDS)[number];
// Model prefixes that are restricted from using Google tools
export const GOOGLE_RESTRICTED_MODEL_PREFIXES = ['grok', 'deepseek'] as const;
export const GOOGLE_TOOL_IDENTIFIERS = [
'gmail',
'google-calendar',
'google-drive',
'google-sheets',
'google-docs',
] as const;
export type GoogleToolIdentifier = (typeof GOOGLE_TOOL_IDENTIFIERS)[number];
/**
* Check if a provider/model combination is restricted from using Google tools
* Checks both provider ID and model ID (model name may contain restricted prefixes)
*/
export const isGoogleRestrictedProvider = (providerId: string, modelId?: string): boolean => {
// Check provider ID
if (GOOGLE_RESTRICTED_PROVIDER_IDS.includes(providerId as GoogleRestrictedProvider)) {
return true;
}
// Check model ID (model name may start with grok or deepseek)
if (modelId) {
const lowerModelId = modelId.toLowerCase();
if (GOOGLE_RESTRICTED_MODEL_PREFIXES.some((prefix) => lowerModelId.startsWith(prefix))) {
return true;
}
}
return false;
};
export const isGoogleTool = (identifier: string): boolean =>
GOOGLE_TOOL_IDENTIFIERS.includes(identifier as GoogleToolIdentifier);
@@ -3,6 +3,7 @@ import { Loader2, SquareArrowOutUpRight } from 'lucide-react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useGoogleDataProtection } from '@/hooks/useGoogleDataProtection';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import { useToolStore } from '@/store/tool';
@@ -40,6 +41,8 @@ const KlavisServerItem = memo<KlavisServerItemProps>(
const [isToggling, setIsToggling] = useState(false);
const [isWaitingAuth, setIsWaitingAuth] = useState(false);
const { checkGoogleToolConnect } = useGoogleDataProtection();
const oauthWindowRef = useRef<Window | null>(null);
const windowCheckIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
@@ -200,6 +203,12 @@ const KlavisServerItem = memo<KlavisServerItemProps>(
return;
}
// Check if this is a Google tool and we're using a restricted provider
const isBlocked = await checkGoogleToolConnect(identifier);
if (isBlocked) {
return;
}
setIsConnecting(true);
try {
const newServer = await createKlavisServer({
@@ -230,6 +239,15 @@ const KlavisServerItem = memo<KlavisServerItemProps>(
const handleToggle = async () => {
if (!server) return;
// If enabling (not currently checked), check for Google tool restrictions
if (!checked) {
const isBlocked = await checkGoogleToolConnect(identifier);
if (isBlocked) {
return;
}
}
setIsToggling(true);
await togglePlugin(pluginId);
setIsToggling(false);
@@ -1,5 +1,6 @@
import { useCallback } from 'react';
import { useGoogleDataProtection } from '@/hooks/useGoogleDataProtection';
import { useAgentStore } from '@/store/agent';
interface UsePanelHandlersProps {
@@ -12,9 +13,16 @@ export const usePanelHandlers = ({
onOpenChange,
}: UsePanelHandlersProps) => {
const updateAgentConfig = useAgentStore((s) => s.updateAgentConfig);
const { checkModelSwitch } = useGoogleDataProtection();
const handleModelChange = useCallback(
(modelId: string, providerId: string) => {
async (modelId: string, providerId: string) => {
// Check if switching to a restricted provider with Google tools enabled
const isBlocked = await checkModelSwitch(providerId, modelId);
if (isBlocked) {
return;
}
// Defer store update so the panel close animation completes
// before React re-renders with new data (prevents detail panel flash).
setTimeout(() => {
@@ -26,7 +34,7 @@ export const usePanelHandlers = ({
}
}, 150);
},
[onModelChangeProp, updateAgentConfig],
[onModelChangeProp, updateAgentConfig, checkModelSwitch],
);
const handleClose = useCallback(() => {
@@ -0,0 +1,35 @@
'use client';
import type { UIChatMessage } from '@lobechat/types';
import { useMemo } from 'react';
import { type ConversationHooks } from '@/features/Conversation';
import { useGoogleDataProtection } from './useGoogleDataProtection';
interface UseAgentGoogleProtectionHooksParams {
messages?: UIChatMessage[];
}
export function useAgentGoogleProtectionHooks(
params?: UseAgentGoogleProtectionHooksParams,
): ConversationHooks {
const { checkMessageHistoryForGoogleTools, checkMessageSend } = useGoogleDataProtection();
return useMemo(
(): ConversationHooks => ({
onBeforeSendMessage: async () => {
// Check 1: Are Google tools currently enabled with restricted provider?
const canSendWithEnabledTools = await checkMessageSend();
if (!canSendWithEnabledTools) return false;
// Check 2: Does message history contain Google tool usage with restricted provider?
const canSendWithHistory = await checkMessageHistoryForGoogleTools(params?.messages);
if (!canSendWithHistory) return false;
return true;
},
}),
[checkMessageSend, checkMessageHistoryForGoogleTools, params?.messages],
);
}
+197
View File
@@ -0,0 +1,197 @@
'use client';
import type { UIChatMessage } from '@lobechat/types';
import { App } from 'antd';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
GOOGLE_TOOL_IDENTIFIERS,
isGoogleRestrictedProvider,
isGoogleTool,
} from '@/const/googleDataProtection';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import { useToolStore } from '@/store/tool';
import { klavisStoreSelectors } from '@/store/tool/selectors';
/**
* Check if messages contain any Google tool usage
*/
export const findGoogleToolsInMessages = (messages: UIChatMessage[] | undefined): string[] => {
if (!messages || messages.length === 0) return [];
const googleToolIds = new Set<string>();
for (const message of messages) {
// Check plugin field
if (message.plugin?.identifier && isGoogleTool(message.plugin.identifier)) {
googleToolIds.add(message.plugin.identifier);
}
// Check tools field
if (message.tools) {
for (const tool of message.tools) {
if (isGoogleTool(tool.identifier)) {
googleToolIds.add(tool.identifier);
}
}
}
// Check children (grouped tool messages - AssistantContentBlock has tools field)
if (message.children) {
for (const child of message.children) {
if (child.tools) {
for (const tool of child.tools) {
if (isGoogleTool(tool.identifier)) {
googleToolIds.add(tool.identifier);
}
}
}
}
}
}
return Array.from(googleToolIds);
};
export interface GoogleDataProtectionState {
enabledGoogleToolIds: string[];
hasConflict: boolean;
hasEnabledGoogleTools: boolean;
isUsingRestrictedProvider: boolean;
}
export const useGoogleDataProtection = () => {
const { t } = useTranslation('setting');
const { modal } = App.useApp();
const plugins = useAgentStore(agentSelectors.currentAgentPlugins);
const currentProvider = useAgentStore(agentSelectors.currentAgentModelProvider);
const currentModel = useAgentStore(agentSelectors.currentAgentModel);
const connectedServers = useToolStore(klavisStoreSelectors.getConnectedServers);
const state = useMemo((): GoogleDataProtectionState => {
const connectedGoogleServerIds = connectedServers
.filter((server) => isGoogleTool(server.identifier))
.map((server) => server.identifier);
const enabledGoogleToolIds = plugins.filter((pluginId) =>
connectedGoogleServerIds.includes(pluginId),
);
const hasEnabledGoogleTools = enabledGoogleToolIds.length > 0;
const isUsingRestrictedProvider = isGoogleRestrictedProvider(currentProvider, currentModel);
const hasConflict = hasEnabledGoogleTools && isUsingRestrictedProvider;
return {
enabledGoogleToolIds,
hasConflict,
hasEnabledGoogleTools,
isUsingRestrictedProvider,
};
}, [plugins, currentProvider, currentModel, connectedServers]);
const checkGoogleToolConnect = useCallback(
async (toolIdentifier: string): Promise<boolean> => {
if (!isGoogleTool(toolIdentifier)) return false;
if (!isGoogleRestrictedProvider(currentProvider, currentModel)) return false;
modal.warning({
centered: true,
content: t('googleDataProtection.cannotConnectGoogle.content'),
okText: t('googleDataProtection.understood'),
title: t('googleDataProtection.cannotConnectGoogle.title'),
});
return true;
},
[currentProvider, currentModel, modal, t],
);
const checkModelSwitch = useCallback(
async (newProviderId: string, newModelId?: string): Promise<boolean> => {
if (!isGoogleRestrictedProvider(newProviderId, newModelId)) return false;
if (!state.hasEnabledGoogleTools) return false;
const toolNames = state.enabledGoogleToolIds
.map((id) => {
const toolInfo = GOOGLE_TOOL_IDENTIFIERS.find((t) => t === id);
return toolInfo || id;
})
.join(', ');
modal.warning({
centered: true,
content: t('googleDataProtection.cannotSwitchModel.content', { tools: toolNames }),
okText: t('googleDataProtection.understood'),
title: t('googleDataProtection.cannotSwitchModel.title'),
});
return true;
},
[state.hasEnabledGoogleTools, state.enabledGoogleToolIds, modal, t],
);
const checkMessageSend = useCallback(async (): Promise<boolean> => {
if (!isGoogleRestrictedProvider(currentProvider, currentModel)) return true;
if (!state.hasEnabledGoogleTools) return true;
const toolNames = state.enabledGoogleToolIds
.map((id) => {
const toolInfo = GOOGLE_TOOL_IDENTIFIERS.find((t) => t === id);
return toolInfo || id;
})
.join(', ');
modal.warning({
centered: true,
content: t('googleDataProtection.cannotSendMessage.content', { tools: toolNames }),
okText: t('googleDataProtection.understood'),
title: t('googleDataProtection.cannotSendMessage.title'),
});
return false;
}, [
currentProvider,
currentModel,
state.hasEnabledGoogleTools,
state.enabledGoogleToolIds,
modal,
t,
]);
/**
* Scenario 4: Check if message history contains Google tool usage
* Block sending if using restricted provider with Google tools in history
* Returns false to block, true to allow
*/
const checkMessageHistoryForGoogleTools = useCallback(
async (messages: UIChatMessage[] | undefined): Promise<boolean> => {
if (!isGoogleRestrictedProvider(currentProvider, currentModel)) return true;
const googleToolsInHistory = findGoogleToolsInMessages(messages);
if (googleToolsInHistory.length === 0) return true;
const toolNames = googleToolsInHistory.join(', ');
modal.warning({
centered: true,
content: t('googleDataProtection.cannotSendWithHistory.content', { tools: toolNames }),
okText: t('googleDataProtection.understood'),
title: t('googleDataProtection.cannotSendWithHistory.title'),
});
return false;
},
[currentProvider, currentModel, modal, t],
);
return {
checkGoogleToolConnect,
checkMessageHistoryForGoogleTools,
checkMessageSend,
checkModelSwitch,
state,
};
};
+15 -1
View File
@@ -206,7 +206,21 @@ export default {
'analytics.telemetry.title': 'Send Anonymous Usage Data',
'analytics.title': 'Analytics',
// Heterogeneous agent CLI status (shown on agent profile page in integration mode)
'googleDataProtection.cannotConnectGoogle.content':
'You are using a non-Google model. Connecting Google tools (Gmail, Drive, Docs) may expose your data to third-party providers. Please switch to a Google model before enabling these tools.',
'googleDataProtection.cannotConnectGoogle.title': 'Cannot Connect Google Tool',
'googleDataProtection.cannotSendMessage.content':
'You have Google tools enabled ({{tools}}) but are using a non-Google model. Sending messages may expose your Google data to third-party providers. Please disable Google tools or switch to a Google model.',
'googleDataProtection.cannotSendMessage.title': 'Cannot Send Message',
'googleDataProtection.cannotSendWithHistory.content':
'Your conversation history contains Google tool usage ({{tools}}), but you are now using a non-Google model. Sending messages may expose your Google data to third-party providers. Please switch to a Google model.',
'googleDataProtection.cannotSendWithHistory.title': 'Cannot Send Message',
'googleDataProtection.cannotSwitchModel.content':
'You have Google tools enabled ({{tools}}). Switching to a non-Google model may expose your Google data to third-party providers. Please disable Google tools before switching models.',
'googleDataProtection.cannotSwitchModel.title': 'Cannot Switch Model',
'googleDataProtection.understood': 'Understood',
// Heterogeneous agent CLI status (shown on agent profile page in integration mode)
'heterogeneousStatus.account.label': 'Account',
'heterogeneousStatus.auth.api': 'API',
'heterogeneousStatus.auth.label': 'Auth Method',
@@ -10,6 +10,7 @@ import AgentHome from '@/features/AgentHome';
import ChatMiniMap from '@/features/ChatMiniMap';
import { ChatList, ConversationProvider } from '@/features/Conversation';
import ZenModeToast from '@/features/ZenModeToast';
import { useAgentGoogleProtectionHooks } from '@/hooks/useAgentGoogleProtectionHooks';
import { useGatewayReconnect } from '@/hooks/useGatewayReconnect';
import { useOperationState } from '@/hooks/useOperationState';
import { useAgentStore } from '@/store/agent';
@@ -54,6 +55,9 @@ const Conversation = memo(() => {
// Get actionsBar config with branching support from ChatStore
const actionsBarConfig = useActionsBarConfig();
// Get Google data protection hooks
const googleProtectionHooks = useAgentGoogleProtectionHooks({ messages });
// Heterogeneous agents (Claude Code, etc.) use a simplified input — their
// toolchain/memory/model are managed by the external runtime, so LobeHub's
// model/tools/memory/KB/MCP/runtime-mode pickers don't apply.
@@ -76,6 +80,7 @@ const Conversation = memo(() => {
actionsBar={actionsBarConfig}
context={context}
hasInitMessages={!!messages}
hooks={googleProtectionHooks}
messages={messages}
operationState={operationState}
onMessagesChange={(messages, ctx) => {
+1 -1
View File
@@ -46,7 +46,7 @@ export const klavisRouter = router({
// Get the tool list for this server
const toolsResponse = await ctx.klavisClient.mcpServer.getTools(serverName as any);
const tools = toolsResponse.tools || [];
const tools = (toolsResponse.tools || []) as { name: string }[];
// Save to database using the provided identifier (format: lowercase, spaces replaced with hyphens)
const manifest: ToolManifest = {
+1 -1
View File
@@ -73,7 +73,7 @@ export const klavisRouter = router({
const response = await klavisClient.mcpServer.getTools(input.serverName as any);
return {
tools: response.tools,
tools: (response.tools || []) as { name: string }[],
};
}),