Merge branch 'master' into feat/maas-support

This commit is contained in:
Junyan Qin (Chin)
2026-01-01 13:07:45 +08:00
committed by GitHub
13 changed files with 153 additions and 56 deletions
+13 -3
View File
@@ -23,12 +23,21 @@ xml_template = """
class OAClient: 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.token = token
self.aes = EncodingAESKey self.aes = EncodingAESKey
self.appid = AppID self.appid = AppID
self.appsecret = Appsecret self.appsecret = Appsecret
self.base_url = 'https://api.weixin.qq.com' self.base_url = api_base_url
self.access_token = '' self.access_token = ''
self.unified_mode = unified_mode self.unified_mode = unified_mode
self.app = Quart(__name__) self.app = Quart(__name__)
@@ -208,12 +217,13 @@ class OAClientForLongerResponse:
LoadingMessage: str, LoadingMessage: str,
logger: None, logger: None,
unified_mode: bool = False, unified_mode: bool = False,
api_base_url: str = 'https://api.weixin.qq.com',
): ):
self.token = token self.token = token
self.aes = EncodingAESKey self.aes = EncodingAESKey
self.appid = AppID self.appid = AppID
self.appsecret = Appsecret self.appsecret = Appsecret
self.base_url = 'https://api.weixin.qq.com' self.base_url = api_base_url
self.access_token = '' self.access_token = ''
self.unified_mode = unified_mode self.unified_mode = unified_mode
self.app = Quart(__name__) self.app = Quart(__name__)
+3 -2
View File
@@ -22,13 +22,14 @@ class WecomClient:
contacts_secret: str, contacts_secret: str,
logger: None, logger: None,
unified_mode: bool = False, unified_mode: bool = False,
api_base_url: str = 'https://qyapi.weixin.qq.com/cgi-bin',
): ):
self.corpid = corpid self.corpid = corpid
self.secret = secret self.secret = secret
self.access_token_for_contacts = '' self.access_token_for_contacts = ''
self.token = token self.token = token
self.aes = EncodingAESKey self.aes = EncodingAESKey
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin' self.base_url = api_base_url
self.access_token = '' self.access_token = ''
self.secret_for_contacts = contacts_secret self.secret_for_contacts = contacts_secret
self.logger = logger self.logger = logger
@@ -56,7 +57,7 @@ class WecomClient:
return bool(self.access_token_for_contacts and self.access_token_for_contacts.strip()) return bool(self.access_token_for_contacts and self.access_token_for_contacts.strip())
async def get_access_token(self, secret): 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: async with httpx.AsyncClient() as client:
response = await client.get(url) response = await client.get(url)
data = response.json() data = response.json()
@@ -13,13 +13,22 @@ import aiofiles
class WecomCSClient: 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.corpid = corpid
self.secret = secret self.secret = secret
self.access_token_for_contacts = '' self.access_token_for_contacts = ''
self.token = token self.token = token
self.aes = EncodingAESKey self.aes = EncodingAESKey
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin' self.base_url = api_base_url
self.access_token = '' self.access_token = ''
self.logger = logger self.logger = logger
self.unified_mode = unified_mode 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()) return bool(self.access_token_for_contacts and self.access_token_for_contacts.strip())
async def get_access_token(self, secret): 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: async with httpx.AsyncClient() as client:
response = await client.get(url) response = await client.get(url)
data = response.json() data = response.json()
@@ -172,7 +181,7 @@ class WecomCSClient:
if not await self.check_access_token(): if not await self.check_access_token():
self.access_token = await self.get_access_token(self.secret) 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 = { payload = {
'touser': external_userid, 'touser': external_userid,
@@ -76,6 +76,7 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd
AppID=config['AppID'], AppID=config['AppID'],
logger=logger, logger=logger,
unified_mode=True, unified_mode=True,
api_base_url=config.get('api_base_url', 'https://api.weixin.qq.com'),
) )
elif config['Mode'] == 'passive': elif config['Mode'] == 'passive':
bot = OAClientForLongerResponse( bot = OAClientForLongerResponse(
@@ -86,6 +87,7 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd
LoadingMessage=config.get('LoadingMessage', ''), LoadingMessage=config.get('LoadingMessage', ''),
logger=logger, logger=logger,
unified_mode=True, unified_mode=True,
api_base_url=config.get('api_base_url', 'https://api.weixin.qq.com'),
) )
else: else:
raise KeyError('请设置微信公众号通信模式') raise KeyError('请设置微信公众号通信模式')
@@ -53,6 +53,16 @@ spec:
type: string type: string
required: true required: true
default: "AI正在思考中,请发送任意内容获取回复。" 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: execution:
python: python:
path: ./officialaccount.py path: ./officialaccount.py
@@ -170,6 +170,7 @@ class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
contacts_secret=config['contacts_secret'], contacts_secret=config['contacts_secret'],
logger=logger, logger=logger,
unified_mode=True, unified_mode=True,
api_base_url=config.get('api_base_url', 'https://qyapi.weixin.qq.com/cgi-bin'),
) )
super().__init__( super().__init__(
@@ -46,6 +46,16 @@ spec:
type: string type: string
required: true required: true
default: "" 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: execution:
python: python:
path: ./wecom.py path: ./wecom.py
@@ -141,6 +141,7 @@ class WecomCSAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
EncodingAESKey=config['EncodingAESKey'], EncodingAESKey=config['EncodingAESKey'],
logger=logger, logger=logger,
unified_mode=True, unified_mode=True,
api_base_url=config.get('api_base_url', 'https://qyapi.weixin.qq.com/cgi-bin'),
) )
super().__init__( super().__init__(
@@ -39,6 +39,16 @@ spec:
type: string type: string
required: true required: true
default: "" 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: execution:
python: python:
path: ./wecomcs.py path: ./wecomcs.py
@@ -19,6 +19,7 @@ import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Copy, Check } from 'lucide-react';
import { import {
Dialog, Dialog,
@@ -116,6 +117,7 @@ export default function BotForm({
const [, setIsLoading] = useState<boolean>(false); const [, setIsLoading] = useState<boolean>(false);
const [webhookUrl, setWebhookUrl] = useState<string>(''); const [webhookUrl, setWebhookUrl] = useState<string>('');
const webhookInputRef = React.useRef<HTMLInputElement>(null); const webhookInputRef = React.useRef<HTMLInputElement>(null);
const [copied, setCopied] = useState<boolean>(false);
// Watch adapter and adapter_config for filtering // Watch adapter and adapter_config for filtering
const currentAdapter = form.watch('adapter'); const currentAdapter = form.watch('adapter');
@@ -153,7 +155,6 @@ export default function BotForm({
const inputElement = webhookInputRef.current; const inputElement = webhookInputRef.current;
if (!inputElement) { if (!inputElement) {
console.error('[Copy] Input element not found'); console.error('[Copy] Input element not found');
toast.error(t('common.copyFailed'));
return; return;
} }
@@ -178,7 +179,8 @@ export default function BotForm({
console.log('[Copy] Clipboard API success'); console.log('[Copy] Clipboard API success');
inputElement.blur(); // 取消选中 inputElement.blur(); // 取消选中
inputElement.readOnly = true; inputElement.readOnly = true;
toast.success(t('bots.webhookUrlCopied')); setCopied(true);
setTimeout(() => setCopied(false), 2000);
}) })
.catch((err) => { .catch((err) => {
console.error( console.error(
@@ -191,9 +193,8 @@ export default function BotForm({
inputElement.blur(); inputElement.blur();
inputElement.readOnly = true; inputElement.readOnly = true;
if (successful) { if (successful) {
toast.success(t('bots.webhookUrlCopied')); setCopied(true);
} else { setTimeout(() => setCopied(false), 2000);
toast.error(t('common.copyFailed'));
} }
}); });
} else { } else {
@@ -207,15 +208,13 @@ export default function BotForm({
inputElement.blur(); inputElement.blur();
inputElement.readOnly = true; inputElement.readOnly = true;
if (successful) { if (successful) {
toast.success(t('bots.webhookUrlCopied')); setCopied(true);
} else { setTimeout(() => setCopied(false), 2000);
toast.error(t('common.copyFailed'));
} }
} }
} catch (err) { } catch (err) {
console.error('[Copy] Copy failed:', err); console.error('[Copy] Copy failed:', err);
inputElement.readOnly = true; inputElement.readOnly = true;
toast.error(t('common.copyFailed'));
} }
}; };
@@ -548,6 +547,11 @@ export default function BotForm({
size="sm" size="sm"
onClick={copyToClipboard} onClick={copyToClipboard}
> >
{copied ? (
<Check className="h-4 w-4 text-green-600 mr-2" />
) : (
<Copy className="h-4 w-4 mr-2" />
)}
{t('common.copy')} {t('common.copy')}
</Button> </Button>
</div> </div>
@@ -1,15 +1,17 @@
'use client'; 'use client';
import { useState } from 'react';
import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse'; import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
import styles from './botLog.module.css'; import styles from './botLog.module.css';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
import { PhotoProvider } from 'react-photo-view'; import { PhotoProvider } from 'react-photo-view';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { toast } from 'sonner'; import { Check } from 'lucide-react';
export function BotLogCard({ botLog }: { botLog: BotLog }) { export function BotLogCard({ botLog }: { botLog: BotLog }) {
const { t } = useTranslation(); const { t } = useTranslation();
const baseURL = httpClient.getBaseUrl(); const baseURL = httpClient.getBaseUrl();
const [copied, setCopied] = useState(false);
function formatTime(timestamp: number) { function formatTime(timestamp: number) {
const now = new Date(); const now = new Date();
@@ -75,42 +77,47 @@ export function BotLogCard({ botLog }: { botLog: BotLog }) {
</div> </div>
{botLog.message_session_id && ( {botLog.message_session_id && (
<div <div
className={`${styles.tag} ${styles.chatTag}`} className={`${styles.tag} ${styles.chatTag} relative`}
onClick={() => { onClick={() => {
navigator.clipboard navigator.clipboard
.writeText(botLog.message_session_id) .writeText(botLog.message_session_id)
.then(() => { .then(() => {
toast.success(t('common.copySuccess')); setCopied(true);
setTimeout(() => setCopied(false), 2000);
}); });
}} }}
title={t('common.clickToCopy')} title={t('common.clickToCopy')}
> >
<svg {copied ? (
className="icon" <Check className="w-4 h-4 text-green-600" />
viewBox="0 0 1024 1024" ) : (
version="1.1" <svg
xmlns="http://www.w3.org/2000/svg" className="icon"
p-id="1664" viewBox="0 0 1024 1024"
width="16" version="1.1"
height="16" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" p-id="1664"
> width="16"
<path height="16"
d="M96.1 575.7a32.2 32.1 0 1 0 64.4 0 32.2 32.1 0 1 0-64.4 0Z"
p-id="1665"
fill="currentColor" fill="currentColor"
></path> >
<path <path
d="M742.1 450.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.1 26-13.8 26-31s-11.7-31.3-26-31.4zM742.1 577.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.2 26-13.8 26-31s-11.7-31.3-26-31.4z" d="M96.1 575.7a32.2 32.1 0 1 0 64.4 0 32.2 32.1 0 1 0-64.4 0Z"
p-id="1666" p-id="1665"
fill="currentColor" fill="currentColor"
></path> ></path>
<path <path
d="M736.1 63.9H417c-70.4 0-128 57.6-128 128h-64.9c-70.4 0-128 57.6-128 128v128c-0.1 17.7 14.4 32 32.2 32 17.8 0 32.2-14.4 32.2-32.1V320c0-35.2 28.8-64 64-64H289v447.8c0 70.4 57.6 128 128 128h255.1c-0.1 35.2-28.8 63.8-64 63.8H224.5c-35.2 0-64-28.8-64-64V703.5c0-17.7-14.4-32.1-32.2-32.1-17.8 0-32.3 14.4-32.3 32.1v128.3c0 70.4 57.6 128 128 128h384.1c70.4 0 128-57.6 128-128h65c70.4 0 128-57.6 128-128V255.9l-193-192z m0.1 63.4l127.7 128.3H800c-35.2 0-64-28.8-64-64v-64.3h0.2z m64 641H416.1c-35.2 0-64-28.8-64-64v-513c0-35.2 28.8-64 64-64H671V191c0 70.4 57.6 128 128 128h65.2v385.3c0 35.2-28.8 64-64 64z" d="M742.1 450.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.1 26-13.8 26-31s-11.7-31.3-26-31.4zM742.1 577.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.2 26-13.8 26-31s-11.7-31.3-26-31.4z"
p-id="1667" p-id="1666"
fill="currentColor" fill="currentColor"
></path> ></path>
</svg> <path
d="M736.1 63.9H417c-70.4 0-128 57.6-128 128h-64.9c-70.4 0-128 57.6-128 128v128c-0.1 17.7 14.4 32 32.2 32 17.8 0 32.2-14.4 32.2-32.1V320c0-35.2 28.8-64 64-64H289v447.8c0 70.4 57.6 128 128 128h255.1c-0.1 35.2-28.8 63.8-64 63.8H224.5c-35.2 0-64-28.8-64-64V703.5c0-17.7-14.4-32.1-32.2-32.1-17.8 0-32.3 14.4-32.3 32.1v128.3c0 70.4 57.6 128 128 128h384.1c70.4 0 128-57.6 128-128h65c70.4 0 128-57.6 128-128V255.9l-193-192z m0.1 63.4l127.7 128.3H800c-35.2 0-64-28.8-64-64v-64.3h0.2z m64 641H416.1c-35.2 0-64-28.8-64-64v-513c0-35.2 28.8-64 64-64H671V191c0 70.4 57.6 128 128 128h65.2v385.3c0 35.2-28.8 64-64 64z"
p-id="1667"
fill="currentColor"
></path>
</svg>
)}
<span className={`${styles.chatId}`}> <span className={`${styles.chatId}`}>
{getSubChatId(botLog.message_session_id)} {getSubChatId(botLog.message_session_id)}
@@ -4,7 +4,7 @@ import * as React from 'react';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { toast } from 'sonner'; 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 { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { import {
Dialog, Dialog,
@@ -87,6 +87,7 @@ export default function ApiIntegrationDialog({
const [newWebhookDescription, setNewWebhookDescription] = useState(''); const [newWebhookDescription, setNewWebhookDescription] = useState('');
const [newWebhookEnabled, setNewWebhookEnabled] = useState(true); const [newWebhookEnabled, setNewWebhookEnabled] = useState(true);
const [deleteWebhookId, setDeleteWebhookId] = useState<number | null>(null); const [deleteWebhookId, setDeleteWebhookId] = useState<number | null>(null);
const [copiedKey, setCopiedKey] = useState<string | null>(null);
// Sync URL with dialog state // Sync URL with dialog state
useEffect(() => { useEffect(() => {
@@ -182,7 +183,8 @@ export default function ApiIntegrationDialog({
const handleCopyKey = (key: string) => { const handleCopyKey = (key: string) => {
navigator.clipboard.writeText(key); navigator.clipboard.writeText(key);
toast.success(t('common.apiKeyCopied')); setCopiedKey(key);
setTimeout(() => setCopiedKey(null), 2000);
}; };
const maskApiKey = (key: string) => { const maskApiKey = (key: string) => {
@@ -352,7 +354,11 @@ export default function ApiIntegrationDialog({
onClick={() => handleCopyKey(key.key)} onClick={() => handleCopyKey(key.key)}
title={t('common.copyApiKey')} title={t('common.copyApiKey')}
> >
<Copy className="h-4 w-4" /> {copiedKey === key.key ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<Copy className="h-4 w-4" />
)}
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
@@ -543,7 +549,11 @@ export default function ApiIntegrationDialog({
variant="outline" variant="outline"
size="icon" size="icon"
> >
<Copy className="h-4 w-4" /> {copiedKey === createdKey?.key ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<Copy className="h-4 w-4" />
)}
</Button> </Button>
</div> </div>
</div> </div>
@@ -640,7 +650,11 @@ export default function ApiIntegrationDialog({
variant="outline" variant="outline"
size="icon" size="icon"
> >
<Copy className="h-4 w-4" /> {copiedKey === createdKey?.key ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<Copy className="h-4 w-4" />
)}
</Button> </Button>
</div> </div>
</div> </div>
+23 -5
View File
@@ -26,6 +26,7 @@ import {
ChevronLeft, ChevronLeft,
Code, Code,
Copy, Copy,
Check,
Bug, Bug,
} from 'lucide-react'; } from 'lucide-react';
import { import {
@@ -118,6 +119,8 @@ export default function PluginConfigPage() {
plugin_debug_key: string; plugin_debug_key: string;
} | null>(null); } | null>(null);
const [debugPopoverOpen, setDebugPopoverOpen] = useState(false); const [debugPopoverOpen, setDebugPopoverOpen] = useState(false);
const [copiedDebugUrl, setCopiedDebugUrl] = useState(false);
const [copiedDebugKey, setCopiedDebugKey] = useState(false);
useEffect(() => { useEffect(() => {
const fetchPluginSystemStatus = async () => { 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); 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 = () => ( const renderPluginDisabledState = () => (
@@ -536,10 +545,14 @@ export default function PluginConfigPage() {
size="icon" size="icon"
className="h-8 w-8 shrink-0" className="h-8 w-8 shrink-0"
onClick={() => onClick={() =>
handleCopyDebugInfo(debugInfo?.debug_url || '') handleCopyDebugInfo(debugInfo?.debug_url || '', 'url')
} }
> >
<Copy className="w-3.5 h-3.5" /> {copiedDebugUrl ? (
<Check className="w-3.5 h-3.5 text-green-600" />
) : (
<Copy className="w-3.5 h-3.5" />
)}
</Button> </Button>
</div> </div>
@@ -564,11 +577,16 @@ export default function PluginConfigPage() {
onClick={() => onClick={() =>
handleCopyDebugInfo( handleCopyDebugInfo(
debugInfo?.plugin_debug_key || '', debugInfo?.plugin_debug_key || '',
'key',
) )
} }
disabled={!debugInfo?.plugin_debug_key} disabled={!debugInfo?.plugin_debug_key}
> >
<Copy className="w-3.5 h-3.5" /> {copiedDebugKey ? (
<Check className="w-3.5 h-3.5 text-green-600" />
) : (
<Copy className="w-3.5 h-3.5" />
)}
</Button> </Button>
</div> </div>
{!debugInfo?.plugin_debug_key && ( {!debugInfo?.plugin_debug_key && (