diff --git a/src/langbot/libs/official_account_api/api.py b/src/langbot/libs/official_account_api/api.py index 671f49a4..b474205d 100644 --- a/src/langbot/libs/official_account_api/api.py +++ b/src/langbot/libs/official_account_api/api.py @@ -23,12 +23,21 @@ xml_template = """ class OAClient: - def __init__(self, token: str, EncodingAESKey: str, AppID: str, Appsecret: str, logger: None, unified_mode: bool = False): + def __init__( + self, + token: str, + EncodingAESKey: str, + AppID: str, + Appsecret: str, + logger: None, + unified_mode: bool = False, + api_base_url: str = 'https://api.weixin.qq.com', + ): self.token = token self.aes = EncodingAESKey self.appid = AppID self.appsecret = Appsecret - self.base_url = 'https://api.weixin.qq.com' + self.base_url = api_base_url self.access_token = '' self.unified_mode = unified_mode self.app = Quart(__name__) @@ -208,12 +217,13 @@ class OAClientForLongerResponse: LoadingMessage: str, logger: None, unified_mode: bool = False, + api_base_url: str = 'https://api.weixin.qq.com', ): self.token = token self.aes = EncodingAESKey self.appid = AppID self.appsecret = Appsecret - self.base_url = 'https://api.weixin.qq.com' + self.base_url = api_base_url self.access_token = '' self.unified_mode = unified_mode self.app = Quart(__name__) diff --git a/src/langbot/libs/wecom_api/api.py b/src/langbot/libs/wecom_api/api.py index 1b8591fd..948bac95 100644 --- a/src/langbot/libs/wecom_api/api.py +++ b/src/langbot/libs/wecom_api/api.py @@ -22,13 +22,14 @@ class WecomClient: contacts_secret: str, logger: None, unified_mode: bool = False, + api_base_url: str = 'https://qyapi.weixin.qq.com/cgi-bin', ): self.corpid = corpid self.secret = secret self.access_token_for_contacts = '' self.token = token self.aes = EncodingAESKey - self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin' + self.base_url = api_base_url self.access_token = '' self.secret_for_contacts = contacts_secret self.logger = logger @@ -56,7 +57,7 @@ class WecomClient: return bool(self.access_token_for_contacts and self.access_token_for_contacts.strip()) async def get_access_token(self, secret): - url = f'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={self.corpid}&corpsecret={secret}' + url = f'{self.base_url}/gettoken?corpid={self.corpid}&corpsecret={secret}' async with httpx.AsyncClient() as client: response = await client.get(url) data = response.json() diff --git a/src/langbot/libs/wecom_customer_service_api/api.py b/src/langbot/libs/wecom_customer_service_api/api.py index 3c4e0fab..e1b94879 100644 --- a/src/langbot/libs/wecom_customer_service_api/api.py +++ b/src/langbot/libs/wecom_customer_service_api/api.py @@ -13,13 +13,22 @@ import aiofiles class WecomCSClient: - def __init__(self, corpid: str, secret: str, token: str, EncodingAESKey: str, logger: None, unified_mode: bool = False): + def __init__( + self, + corpid: str, + secret: str, + token: str, + EncodingAESKey: str, + logger: None, + unified_mode: bool = False, + api_base_url: str = 'https://qyapi.weixin.qq.com/cgi-bin', + ): self.corpid = corpid self.secret = secret self.access_token_for_contacts = '' self.token = token self.aes = EncodingAESKey - self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin' + self.base_url = api_base_url self.access_token = '' self.logger = logger self.unified_mode = unified_mode @@ -66,7 +75,7 @@ class WecomCSClient: return bool(self.access_token_for_contacts and self.access_token_for_contacts.strip()) async def get_access_token(self, secret): - url = f'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={self.corpid}&corpsecret={secret}' + url = f'{self.base_url}/gettoken?corpid={self.corpid}&corpsecret={secret}' async with httpx.AsyncClient() as client: response = await client.get(url) data = response.json() @@ -172,7 +181,7 @@ class WecomCSClient: if not await self.check_access_token(): self.access_token = await self.get_access_token(self.secret) - url = f'https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg?access_token={self.access_token}' + url = f'{self.base_url}/kf/send_msg?access_token={self.access_token}' payload = { 'touser': external_userid, diff --git a/src/langbot/pkg/platform/sources/officialaccount.py b/src/langbot/pkg/platform/sources/officialaccount.py index e0c48a23..288991d6 100644 --- a/src/langbot/pkg/platform/sources/officialaccount.py +++ b/src/langbot/pkg/platform/sources/officialaccount.py @@ -76,6 +76,7 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd AppID=config['AppID'], logger=logger, unified_mode=True, + api_base_url=config.get('api_base_url', 'https://api.weixin.qq.com'), ) elif config['Mode'] == 'passive': bot = OAClientForLongerResponse( @@ -86,6 +87,7 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd LoadingMessage=config.get('LoadingMessage', ''), logger=logger, unified_mode=True, + api_base_url=config.get('api_base_url', 'https://api.weixin.qq.com'), ) else: raise KeyError('请设置微信公众号通信模式') diff --git a/src/langbot/pkg/platform/sources/officialaccount.yaml b/src/langbot/pkg/platform/sources/officialaccount.yaml index 53345413..fda0a912 100644 --- a/src/langbot/pkg/platform/sources/officialaccount.yaml +++ b/src/langbot/pkg/platform/sources/officialaccount.yaml @@ -53,6 +53,16 @@ spec: type: string required: true default: "AI正在思考中,请发送任意内容获取回复。" + - name: api_base_url + label: + en_US: API Base URL + zh_Hans: API 基础 URL + description: + en_US: API Base URL, used for accessing the Official Account API. If you are deploying in an internal network environment and accessing the Official Account API through a reverse proxy, please fill in this item according to the documentation. + zh_Hans: 可选,若您部署在内网环境并通过反向代理访问微信公众号 API,可根据文档修改此项 + type: string + required: false + default: "https://api.weixin.qq.com" execution: python: path: ./officialaccount.py diff --git a/src/langbot/pkg/platform/sources/wecom.py b/src/langbot/pkg/platform/sources/wecom.py index c42d0c9c..c6625040 100644 --- a/src/langbot/pkg/platform/sources/wecom.py +++ b/src/langbot/pkg/platform/sources/wecom.py @@ -170,6 +170,7 @@ class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): contacts_secret=config['contacts_secret'], logger=logger, unified_mode=True, + api_base_url=config.get('api_base_url', 'https://qyapi.weixin.qq.com/cgi-bin'), ) super().__init__( diff --git a/src/langbot/pkg/platform/sources/wecom.yaml b/src/langbot/pkg/platform/sources/wecom.yaml index 98fad4ce..1d547c29 100644 --- a/src/langbot/pkg/platform/sources/wecom.yaml +++ b/src/langbot/pkg/platform/sources/wecom.yaml @@ -46,6 +46,16 @@ spec: type: string required: true default: "" + - name: api_base_url + label: + en_US: API Base URL + zh_Hans: API 基础 URL + description: + en_US: API Base URL, used for accessing the WeCom API. If you are deploying in an internal network environment and accessing the WeCom Customer Service API through a reverse proxy, please fill in this item according to the documentation. + zh_Hans: 可选,若您部署在内网环境并通过反向代理访问企业微信 API,可根据文档填写此项 + type: string + required: false + default: "https://qyapi.weixin.qq.com/cgi-bin" execution: python: path: ./wecom.py diff --git a/src/langbot/pkg/platform/sources/wecomcs.py b/src/langbot/pkg/platform/sources/wecomcs.py index bfe6811b..536429cc 100644 --- a/src/langbot/pkg/platform/sources/wecomcs.py +++ b/src/langbot/pkg/platform/sources/wecomcs.py @@ -141,6 +141,7 @@ class WecomCSAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): EncodingAESKey=config['EncodingAESKey'], logger=logger, unified_mode=True, + api_base_url=config.get('api_base_url', 'https://qyapi.weixin.qq.com/cgi-bin'), ) super().__init__( diff --git a/src/langbot/pkg/platform/sources/wecomcs.yaml b/src/langbot/pkg/platform/sources/wecomcs.yaml index 7d4f398d..a1be068e 100644 --- a/src/langbot/pkg/platform/sources/wecomcs.yaml +++ b/src/langbot/pkg/platform/sources/wecomcs.yaml @@ -39,6 +39,16 @@ spec: type: string required: true default: "" + - name: api_base_url + label: + en_US: API Base URL + zh_Hans: API 基础 URL + description: + en_US: API Base URL, used for accessing the WeCom API. If you are deploying in an internal network environment and accessing the WeCom Customer Service API through a reverse proxy, please fill in this item according to the documentation. + zh_Hans: 可选,若您部署在内网环境并通过反向代理访问企业微信 API,可根据文档修改此项 + type: string + required: false + default: "https://qyapi.weixin.qq.com/cgi-bin" execution: python: path: ./wecomcs.py diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx index bef21443..5e030021 100644 --- a/web/src/app/home/bots/components/bot-form/BotForm.tsx +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -19,6 +19,7 @@ import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; +import { Copy, Check } from 'lucide-react'; import { Dialog, @@ -116,6 +117,7 @@ export default function BotForm({ const [, setIsLoading] = useState(false); const [webhookUrl, setWebhookUrl] = useState(''); const webhookInputRef = React.useRef(null); + const [copied, setCopied] = useState(false); // Watch adapter and adapter_config for filtering const currentAdapter = form.watch('adapter'); @@ -153,7 +155,6 @@ export default function BotForm({ const inputElement = webhookInputRef.current; if (!inputElement) { console.error('[Copy] Input element not found'); - toast.error(t('common.copyFailed')); return; } @@ -178,7 +179,8 @@ export default function BotForm({ console.log('[Copy] Clipboard API success'); inputElement.blur(); // 取消选中 inputElement.readOnly = true; - toast.success(t('bots.webhookUrlCopied')); + setCopied(true); + setTimeout(() => setCopied(false), 2000); }) .catch((err) => { console.error( @@ -191,9 +193,8 @@ export default function BotForm({ inputElement.blur(); inputElement.readOnly = true; if (successful) { - toast.success(t('bots.webhookUrlCopied')); - } else { - toast.error(t('common.copyFailed')); + setCopied(true); + setTimeout(() => setCopied(false), 2000); } }); } else { @@ -207,15 +208,13 @@ export default function BotForm({ inputElement.blur(); inputElement.readOnly = true; if (successful) { - toast.success(t('bots.webhookUrlCopied')); - } else { - toast.error(t('common.copyFailed')); + setCopied(true); + setTimeout(() => setCopied(false), 2000); } } } catch (err) { console.error('[Copy] Copy failed:', err); inputElement.readOnly = true; - toast.error(t('common.copyFailed')); } }; @@ -548,6 +547,11 @@ export default function BotForm({ size="sm" onClick={copyToClipboard} > + {copied ? ( + + ) : ( + + )} {t('common.copy')} diff --git a/web/src/app/home/bots/components/bot-log/view/BotLogCard.tsx b/web/src/app/home/bots/components/bot-log/view/BotLogCard.tsx index f916af2f..7cc380ec 100644 --- a/web/src/app/home/bots/components/bot-log/view/BotLogCard.tsx +++ b/web/src/app/home/bots/components/bot-log/view/BotLogCard.tsx @@ -1,15 +1,17 @@ 'use client'; +import { useState } from 'react'; import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse'; import styles from './botLog.module.css'; import { httpClient } from '@/app/infra/http/HttpClient'; import { PhotoProvider } from 'react-photo-view'; import { useTranslation } from 'react-i18next'; -import { toast } from 'sonner'; +import { Check } from 'lucide-react'; export function BotLogCard({ botLog }: { botLog: BotLog }) { const { t } = useTranslation(); const baseURL = httpClient.getBaseUrl(); + const [copied, setCopied] = useState(false); function formatTime(timestamp: number) { const now = new Date(); @@ -75,42 +77,47 @@ export function BotLogCard({ botLog }: { botLog: BotLog }) { {botLog.message_session_id && (
{ navigator.clipboard .writeText(botLog.message_session_id) .then(() => { - toast.success(t('common.copySuccess')); + setCopied(true); + setTimeout(() => setCopied(false), 2000); }); }} title={t('common.clickToCopy')} > - - + ) : ( + - - - + > + + + + + )} {getSubChatId(botLog.message_session_id)} diff --git a/web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx b/web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx index b39f9144..ef653715 100644 --- a/web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx +++ b/web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; -import { Copy, Trash2, Plus } from 'lucide-react'; +import { Copy, Check, Trash2, Plus } from 'lucide-react'; import { useRouter, usePathname, useSearchParams } from 'next/navigation'; import { Dialog, @@ -87,6 +87,7 @@ export default function ApiIntegrationDialog({ const [newWebhookDescription, setNewWebhookDescription] = useState(''); const [newWebhookEnabled, setNewWebhookEnabled] = useState(true); const [deleteWebhookId, setDeleteWebhookId] = useState(null); + const [copiedKey, setCopiedKey] = useState(null); // Sync URL with dialog state useEffect(() => { @@ -182,7 +183,8 @@ export default function ApiIntegrationDialog({ const handleCopyKey = (key: string) => { navigator.clipboard.writeText(key); - toast.success(t('common.apiKeyCopied')); + setCopiedKey(key); + setTimeout(() => setCopiedKey(null), 2000); }; const maskApiKey = (key: string) => { @@ -352,7 +354,11 @@ export default function ApiIntegrationDialog({ onClick={() => handleCopyKey(key.key)} title={t('common.copyApiKey')} > - + {copiedKey === key.key ? ( + + ) : ( + + )}
@@ -640,7 +650,11 @@ export default function ApiIntegrationDialog({ variant="outline" size="icon" > - + {copiedKey === createdKey?.key ? ( + + ) : ( + + )} diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index 3444fa7f..822364d6 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -26,6 +26,7 @@ import { ChevronLeft, Code, Copy, + Check, Bug, } from 'lucide-react'; import { @@ -118,6 +119,8 @@ export default function PluginConfigPage() { plugin_debug_key: string; } | null>(null); const [debugPopoverOpen, setDebugPopoverOpen] = useState(false); + const [copiedDebugUrl, setCopiedDebugUrl] = useState(false); + const [copiedDebugKey, setCopiedDebugKey] = useState(false); useEffect(() => { const fetchPluginSystemStatus = async () => { @@ -398,9 +401,15 @@ export default function PluginConfigPage() { } }; - const handleCopyDebugInfo = (text: string) => { + const handleCopyDebugInfo = (text: string, type: 'url' | 'key') => { navigator.clipboard.writeText(text); - toast.success(t('plugins.copiedToClipboard')); + if (type === 'url') { + setCopiedDebugUrl(true); + setTimeout(() => setCopiedDebugUrl(false), 2000); + } else { + setCopiedDebugKey(true); + setTimeout(() => setCopiedDebugKey(false), 2000); + } }; const renderPluginDisabledState = () => ( @@ -536,10 +545,14 @@ export default function PluginConfigPage() { size="icon" className="h-8 w-8 shrink-0" onClick={() => - handleCopyDebugInfo(debugInfo?.debug_url || '') + handleCopyDebugInfo(debugInfo?.debug_url || '', 'url') } > - + {copiedDebugUrl ? ( + + ) : ( + + )} @@ -564,11 +577,16 @@ export default function PluginConfigPage() { onClick={() => handleCopyDebugInfo( debugInfo?.plugin_debug_key || '', + 'key', ) } disabled={!debugInfo?.plugin_debug_key} > - + {copiedDebugKey ? ( + + ) : ( + + )} {!debugInfo?.plugin_debug_key && (