chore: adjust dir structure

This commit is contained in:
Junyan Qin
2025-11-16 16:28:04 +08:00
parent c5aa5be4d8
commit 75edeb7a01
451 changed files with 299 additions and 525 deletions

View File

@@ -1,45 +0,0 @@
from v1 import client # type: ignore
import asyncio
import os
import json
class TestDifyClient:
async def test_chat_messages(self):
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
async for chunk in cln.chat_messages(inputs={}, query='调用工具查看现在几点?', user='test'):
print(json.dumps(chunk, ensure_ascii=False, indent=4))
async def test_upload_file(self):
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
file_bytes = open('img.png', 'rb').read()
print(type(file_bytes))
file = ('img2.png', file_bytes, 'image/png')
resp = await cln.upload_file(file=file, user='test')
print(json.dumps(resp, ensure_ascii=False, indent=4))
async def test_workflow_run(self):
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
# resp = await cln.workflow_run(inputs={}, user="test")
# # print(json.dumps(resp, ensure_ascii=False, indent=4))
# print(resp)
chunks = []
ignored_events = ['text_chunk']
async for chunk in cln.workflow_run(inputs={}, user='test'):
if chunk['event'] in ignored_events:
continue
chunks.append(chunk)
print(json.dumps(chunks, ensure_ascii=False, indent=4))
if __name__ == '__main__':
asyncio.run(TestDifyClient().test_chat_messages())

View File

@@ -1,115 +0,0 @@
from __future__ import annotations
import json
import typing
import os
import base64
import logging
import pydantic
import requests
from ..core import app
class Announcement(pydantic.BaseModel):
"""公告"""
id: int
time: str
timestamp: int
content: str
enabled: typing.Optional[bool] = True
def to_dict(self) -> dict:
return {
'id': self.id,
'time': self.time,
'timestamp': self.timestamp,
'content': self.content,
'enabled': self.enabled,
}
class AnnouncementManager:
"""公告管理器"""
ap: app.Application = None
def __init__(self, ap: app.Application):
self.ap = ap
async def fetch_all(self) -> list[Announcement]:
"""获取所有公告"""
try:
resp = requests.get(
url='https://api.github.com/repos/langbot-app/LangBot/contents/res/announcement.json',
proxies=self.ap.proxy_mgr.get_forward_proxies(),
timeout=5,
)
resp.raise_for_status() # 检查请求是否成功
obj_json = resp.json()
b64_content = obj_json['content']
# 解码
content = base64.b64decode(b64_content).decode('utf-8')
return [Announcement(**item) for item in json.loads(content)]
except (requests.RequestException, json.JSONDecodeError, KeyError) as e:
self.ap.logger.warning(f'获取公告失败: {e}')
pass
return [] # 请求失败时返回空列表
async def fetch_saved(self) -> list[Announcement]:
if not os.path.exists('data/labels/announcement_saved.json'):
with open('data/labels/announcement_saved.json', 'w', encoding='utf-8') as f:
f.write('[]')
with open('data/labels/announcement_saved.json', 'r', encoding='utf-8') as f:
content = f.read()
if not content:
content = '[]'
return [Announcement(**item) for item in json.loads(content)]
async def write_saved(self, content: list[Announcement]):
with open('data/labels/announcement_saved.json', 'w', encoding='utf-8') as f:
f.write(json.dumps([item.to_dict() for item in content], indent=4, ensure_ascii=False))
async def fetch_new(self) -> list[Announcement]:
"""获取新公告"""
all = await self.fetch_all()
saved = await self.fetch_saved()
to_show: list[Announcement] = []
for item in all:
# 遍历saved检查是否有相同id的公告
for saved_item in saved:
if saved_item.id == item.id:
break
else:
if item.enabled:
# 没有相同id的公告
to_show.append(item)
await self.write_saved(all)
return to_show
async def show_announcements(self) -> typing.Tuple[str, int]:
"""显示公告"""
try:
announcements = await self.fetch_new()
ann_text = ''
for ann in announcements:
ann_text += f'[公告] {ann.time}: {ann.content}\n'
# TODO statistics
return ann_text, logging.INFO
except Exception as e:
return f'获取公告时出错: {e}', logging.WARNING

View File

@@ -1,3 +1,3 @@
"""LangBot - Easy-to-use global IM bot platform designed for LLM era""" """LangBot - Easy-to-use global IM bot platform designed for LLM era"""
__version__ = "4.4.1" __version__ = '4.4.1'

View File

@@ -1,4 +1,5 @@
"""LangBot entry point for package execution""" """LangBot entry point for package execution"""
import asyncio import asyncio
import argparse import argparse
import sys import sys
@@ -42,7 +43,7 @@ async def main_entry(loop: asyncio.AbstractEventLoop):
print(asciiart) print(asciiart)
# Check dependencies # Check dependencies
from pkg.core.bootutils import deps from langbot.pkg.core.bootutils import deps
missing_deps = await deps.check_deps() missing_deps = await deps.check_deps()

View File

@@ -7,10 +7,8 @@ import os
from pathlib import Path from pathlib import Path
class AsyncCozeAPIClient: class AsyncCozeAPIClient:
def __init__(self, api_key: str, api_base: str = "https://api.coze.cn"): def __init__(self, api_key: str, api_base: str = 'https://api.coze.cn'):
self.api_key = api_key self.api_key = api_key
self.api_base = api_base self.api_base = api_base
self.session = None self.session = None
@@ -24,13 +22,11 @@ class AsyncCozeAPIClient:
"""退出时自动关闭会话""" """退出时自动关闭会话"""
await self.close() await self.close()
async def coze_session(self): async def coze_session(self):
"""确保HTTP session存在""" """确保HTTP session存在"""
if self.session is None: if self.session is None:
connector = aiohttp.TCPConnector( connector = aiohttp.TCPConnector(
ssl=False if self.api_base.startswith("http://") else True, ssl=False if self.api_base.startswith('http://') else True,
limit=100, limit=100,
limit_per_host=30, limit_per_host=30,
keepalive_timeout=30, keepalive_timeout=30,
@@ -42,12 +38,10 @@ class AsyncCozeAPIClient:
sock_read=120, sock_read=120,
) )
headers = { headers = {
"Authorization": f"Bearer {self.api_key}", 'Authorization': f'Bearer {self.api_key}',
"Accept": "text/event-stream", 'Accept': 'text/event-stream',
} }
self.session = aiohttp.ClientSession( self.session = aiohttp.ClientSession(headers=headers, timeout=timeout, connector=connector)
headers=headers, timeout=timeout, connector=connector
)
return self.session return self.session
async def close(self): async def close(self):
@@ -63,15 +57,15 @@ class AsyncCozeAPIClient:
# 处理 Path 对象 # 处理 Path 对象
if isinstance(file, Path): if isinstance(file, Path):
if not file.exists(): if not file.exists():
raise ValueError(f"File not found: {file}") raise ValueError(f'File not found: {file}')
with open(file, "rb") as f: with open(file, 'rb') as f:
file = f.read() file = f.read()
# 处理文件路径字符串 # 处理文件路径字符串
elif isinstance(file, str): elif isinstance(file, str):
if not os.path.isfile(file): if not os.path.isfile(file):
raise ValueError(f"File not found: {file}") raise ValueError(f'File not found: {file}')
with open(file, "rb") as f: with open(file, 'rb') as f:
file = f.read() file = f.read()
# 处理文件对象 # 处理文件对象
@@ -79,43 +73,39 @@ class AsyncCozeAPIClient:
file = file.read() file = file.read()
session = await self.coze_session() session = await self.coze_session()
url = f"{self.api_base}/v1/files/upload" url = f'{self.api_base}/v1/files/upload'
try: try:
file_io = io.BytesIO(file) file_io = io.BytesIO(file)
async with session.post( async with session.post(
url, url,
data={ data={
"file": file_io, 'file': file_io,
}, },
timeout=aiohttp.ClientTimeout(total=60), timeout=aiohttp.ClientTimeout(total=60),
) as response: ) as response:
if response.status == 401: if response.status == 401:
raise Exception("Coze API 认证失败,请检查 API Key 是否正确") raise Exception('Coze API 认证失败,请检查 API Key 是否正确')
response_text = await response.text() response_text = await response.text()
if response.status != 200: if response.status != 200:
raise Exception( raise Exception(f'文件上传失败,状态码: {response.status}, 响应: {response_text}')
f"文件上传失败,状态码: {response.status}, 响应: {response_text}"
)
try: try:
result = await response.json() result = await response.json()
except json.JSONDecodeError: except json.JSONDecodeError:
raise Exception(f"文件上传响应解析失败: {response_text}") raise Exception(f'文件上传响应解析失败: {response_text}')
if result.get("code") != 0: if result.get('code') != 0:
raise Exception(f"文件上传失败: {result.get('msg', '未知错误')}") raise Exception(f'文件上传失败: {result.get("msg", "未知错误")}')
file_id = result["data"]["id"] file_id = result['data']['id']
return file_id return file_id
except asyncio.TimeoutError: except asyncio.TimeoutError:
raise Exception("文件上传超时") raise Exception('文件上传超时')
except Exception as e: except Exception as e:
raise Exception(f"文件上传失败: {str(e)}") raise Exception(f'文件上传失败: {str(e)}')
async def chat_messages( async def chat_messages(
self, self,
@@ -139,22 +129,21 @@ class AsyncCozeAPIClient:
timeout: 超时时间 timeout: 超时时间
""" """
session = await self.coze_session() session = await self.coze_session()
url = f"{self.api_base}/v3/chat" url = f'{self.api_base}/v3/chat'
payload = { payload = {
"bot_id": bot_id, 'bot_id': bot_id,
"user_id": user_id, 'user_id': user_id,
"stream": stream, 'stream': stream,
"auto_save_history": auto_save_history, 'auto_save_history': auto_save_history,
} }
if additional_messages: if additional_messages:
payload["additional_messages"] = additional_messages payload['additional_messages'] = additional_messages
params = {} params = {}
if conversation_id: if conversation_id:
params["conversation_id"] = conversation_id params['conversation_id'] = conversation_id
try: try:
async with session.post( async with session.post(
@@ -164,29 +153,25 @@ class AsyncCozeAPIClient:
timeout=aiohttp.ClientTimeout(total=timeout), timeout=aiohttp.ClientTimeout(total=timeout),
) as response: ) as response:
if response.status == 401: if response.status == 401:
raise Exception("Coze API 认证失败,请检查 API Key 是否正确") raise Exception('Coze API 认证失败,请检查 API Key 是否正确')
if response.status != 200: if response.status != 200:
raise Exception(f"Coze API 流式请求失败,状态码: {response.status}") raise Exception(f'Coze API 流式请求失败,状态码: {response.status}')
async for chunk in response.content: async for chunk in response.content:
chunk = chunk.decode("utf-8") chunk = chunk.decode('utf-8')
if chunk != '\n': if chunk != '\n':
if chunk.startswith("event:"): if chunk.startswith('event:'):
chunk_type = chunk.replace("event:", "", 1).strip() chunk_type = chunk.replace('event:', '', 1).strip()
elif chunk.startswith("data:"): elif chunk.startswith('data:'):
chunk_data = chunk.replace("data:", "", 1).strip() chunk_data = chunk.replace('data:', '', 1).strip()
else: else:
yield {"event": chunk_type, "data": json.loads(chunk_data) if chunk_data else {}} # 处理本地部署时接口返回的data为空值 yield {
'event': chunk_type,
'data': json.loads(chunk_data) if chunk_data else {},
} # 处理本地部署时接口返回的data为空值
except asyncio.TimeoutError: except asyncio.TimeoutError:
raise Exception(f"Coze API 流式请求超时 ({timeout}秒)") raise Exception(f'Coze API 流式请求超时 ({timeout}秒)')
except Exception as e: except Exception as e:
raise Exception(f"Coze API 流式请求失败: {str(e)}") raise Exception(f'Coze API 流式请求失败: {str(e)}')

View File

@@ -39,7 +39,6 @@ class DingTalkEvent(dict):
def name(self): def name(self):
return self.get('Name', '') return self.get('Name', '')
@property @property
def conversation(self): def conversation(self):
return self.get('conversation_type', '') return self.get('conversation_type', '')

View File

@@ -219,10 +219,7 @@ class WecomBotClient:
self.ReceiveId = '' self.ReceiveId = ''
self.app = Quart(__name__) self.app = Quart(__name__)
self.app.add_url_rule( self.app.add_url_rule(
'/callback/command', '/callback/command', 'handle_callback', self.handle_callback_request, methods=['POST', 'GET']
'handle_callback',
self.handle_callback_request,
methods=['POST', 'GET']
) )
self._message_handlers = { self._message_handlers = {
'example': [], 'example': [],
@@ -420,7 +417,7 @@ class WecomBotClient:
await self.logger.error("请求体中缺少 'encrypt' 字段") await self.logger.error("请求体中缺少 'encrypt' 字段")
return Response('Bad Request', status=400) return Response('Bad Request', status=400)
xml_post_data = f"<xml><Encrypt><![CDATA[{encrypted_msg}]]></Encrypt></xml>" xml_post_data = f'<xml><Encrypt><![CDATA[{encrypted_msg}]]></Encrypt></xml>'
ret, decrypted_xml = self.wxcpt.DecryptMsg(xml_post_data, msg_signature, timestamp, nonce) ret, decrypted_xml = self.wxcpt.DecryptMsg(xml_post_data, msg_signature, timestamp, nonce)
if ret != 0: if ret != 0:
await self.logger.error('解密失败') await self.logger.error('解密失败')
@@ -458,7 +455,7 @@ class WecomBotClient:
picurl = item.get('image', {}).get('url') picurl = item.get('image', {}).get('url')
if texts: if texts:
message_data['content'] = "".join(texts) # 拼接所有 text message_data['content'] = ''.join(texts) # 拼接所有 text
if picurl: if picurl:
base64 = await self.download_url_to_base64(picurl, self.EnCodingAESKey) base64 = await self.download_url_to_base64(picurl, self.EnCodingAESKey)
message_data['picurl'] = base64 # 只保留第一个 image message_data['picurl'] = base64 # 只保留第一个 image
@@ -466,7 +463,9 @@ class WecomBotClient:
# Extract user information # Extract user information
from_info = msg_json.get('from', {}) from_info = msg_json.get('from', {})
message_data['userid'] = from_info.get('userid', '') message_data['userid'] = from_info.get('userid', '')
message_data['username'] = from_info.get('alias', '') or from_info.get('name', '') or from_info.get('userid', '') message_data['username'] = (
from_info.get('alias', '') or from_info.get('name', '') or from_info.get('userid', '')
)
# Extract chat/group information # Extract chat/group information
if msg_json.get('chattype', '') == 'group': if msg_json.get('chattype', '') == 'group':
@@ -555,7 +554,7 @@ class WecomBotClient:
encrypted_bytes = response.content encrypted_bytes = response.content
aes_key = base64.b64decode(encoding_aes_key + "=") # base64 补齐 aes_key = base64.b64decode(encoding_aes_key + '=') # base64 补齐
iv = aes_key[:16] iv = aes_key[:16]
cipher = AES.new(aes_key, AES.MODE_CBC, iv) cipher = AES.new(aes_key, AES.MODE_CBC, iv)
@@ -564,22 +563,22 @@ class WecomBotClient:
pad_len = decrypted[-1] pad_len = decrypted[-1]
decrypted = decrypted[:-pad_len] decrypted = decrypted[:-pad_len]
if decrypted.startswith(b"\xff\xd8"): # JPEG if decrypted.startswith(b'\xff\xd8'): # JPEG
mime_type = "image/jpeg" mime_type = 'image/jpeg'
elif decrypted.startswith(b"\x89PNG"): # PNG elif decrypted.startswith(b'\x89PNG'): # PNG
mime_type = "image/png" mime_type = 'image/png'
elif decrypted.startswith((b"GIF87a", b"GIF89a")): # GIF elif decrypted.startswith((b'GIF87a', b'GIF89a')): # GIF
mime_type = "image/gif" mime_type = 'image/gif'
elif decrypted.startswith(b"BM"): # BMP elif decrypted.startswith(b'BM'): # BMP
mime_type = "image/bmp" mime_type = 'image/bmp'
elif decrypted.startswith(b"II*\x00") or decrypted.startswith(b"MM\x00*"): # TIFF elif decrypted.startswith(b'II*\x00') or decrypted.startswith(b'MM\x00*'): # TIFF
mime_type = "image/tiff" mime_type = 'image/tiff'
else: else:
mime_type = "application/octet-stream" mime_type = 'application/octet-stream'
# 转 base64 # 转 base64
base64_str = base64.b64encode(decrypted).decode("utf-8") base64_str = base64.b64encode(decrypted).decode('utf-8')
return f"data:{mime_type};base64,{base64_str}" return f'data:{mime_type};base64,{base64_str}'
async def run_task(self, host: str, port: int, *args, **kwargs): async def run_task(self, host: str, port: int, *args, **kwargs):
""" """

View File

@@ -29,7 +29,12 @@ class WecomBotEvent(dict):
""" """
用户名称 用户名称
""" """
return self.get('username', '') or self.get('from', {}).get('alias', '') or self.get('from', {}).get('name', '') or self.userid return (
self.get('username', '')
or self.get('from', {}).get('alias', '')
or self.get('from', {}).get('name', '')
or self.userid
)
@property @property
def chatname(self) -> str: def chatname(self) -> str:

View File

@@ -340,4 +340,3 @@ class WecomClient:
async def get_media_id(self, image: platform_message.Image): async def get_media_id(self, image: platform_message.Image):
media_id = await self.upload_to_work(image=image) media_id = await self.upload_to_work(image=image)
return media_id return media_id

View File

@@ -124,7 +124,9 @@ class RouterGroup(abc.ABC):
token = quart.request.headers.get('Authorization', '').replace('Bearer ', '') token = quart.request.headers.get('Authorization', '').replace('Bearer ', '')
if not token: if not token:
return self.http_status(401, -1, 'No valid authentication provided (user token or API key required)') return self.http_status(
401, -1, 'No valid authentication provided (user token or API key required)'
)
try: try:
user_email = await self.ap.user_service.verify_jwt_token(token) user_email = await self.ap.user_service.verify_jwt_token(token)

View File

@@ -27,7 +27,9 @@ class PipelinesRouterGroup(group.RouterGroup):
async def _() -> str: async def _() -> str:
return self.success(data={'configs': await self.ap.pipeline_service.get_pipeline_metadata()}) return self.success(data={'configs': await self.ap.pipeline_service.get_pipeline_metadata()})
@self.route('/<pipeline_uuid>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY) @self.route(
'/<pipeline_uuid>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY
)
async def _(pipeline_uuid: str) -> str: async def _(pipeline_uuid: str) -> str:
if quart.request.method == 'GET': if quart.request.method == 'GET':
pipeline = await self.ap.pipeline_service.get_pipeline(pipeline_uuid) pipeline = await self.ap.pipeline_service.get_pipeline(pipeline_uuid)
@@ -47,7 +49,9 @@ class PipelinesRouterGroup(group.RouterGroup):
return self.success() return self.success()
@self.route('/<pipeline_uuid>/extensions', methods=['GET', 'PUT'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY) @self.route(
'/<pipeline_uuid>/extensions', methods=['GET', 'PUT'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY
)
async def _(pipeline_uuid: str) -> str: async def _(pipeline_uuid: str) -> str:
if quart.request.method == 'GET': if quart.request.method == 'GET':
# Get current extensions and available plugins # Get current extensions and available plugins

View File

@@ -61,9 +61,7 @@ class ApiKeyService:
async def delete_api_key(self, key_id: int) -> None: async def delete_api_key(self, key_id: int) -> None:
"""Delete an API key""" """Delete an API key"""
await self.ap.persistence_mgr.execute_async( await self.ap.persistence_mgr.execute_async(sqlalchemy.delete(apikey.ApiKey).where(apikey.ApiKey.id == key_id))
sqlalchemy.delete(apikey.ApiKey).where(apikey.ApiKey.id == key_id)
)
async def update_api_key(self, key_id: int, name: str = None, description: str = None) -> None: async def update_api_key(self, key_id: int, name: str = None, description: str = None) -> None:
"""Update an API key's metadata (name, description)""" """Update an API key's metadata (name, description)"""

View File

@@ -84,8 +84,6 @@ class MCPService:
new_enable = server_data.get('enable', False) new_enable = server_data.get('enable', False)
need_remove = old_server_name and old_server_name in self.ap.tool_mgr.mcp_tool_loader.sessions need_remove = old_server_name and old_server_name in self.ap.tool_mgr.mcp_tool_loader.sessions
need_start = new_enable
if old_enable and not new_enable: if old_enable and not new_enable:
if need_remove: if need_remove:
@@ -113,7 +111,6 @@ class MCPService:
task = asyncio.create_task(self.ap.tool_mgr.mcp_tool_loader.host_mcp_server(server_config)) task = asyncio.create_task(self.ap.tool_mgr.mcp_tool_loader.host_mcp_server(server_config))
self.ap.tool_mgr.mcp_tool_loader._hosted_mcp_tasks.append(task) self.ap.tool_mgr.mcp_tool_loader._hosted_mcp_tasks.append(task)
async def delete_mcp_server(self, server_uuid: str) -> None: async def delete_mcp_server(self, server_uuid: str) -> None:
result = await self.ap.persistence_mgr.execute_async( result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_mcp.MCPServer).where(persistence_mcp.MCPServer.uuid == server_uuid) sqlalchemy.select(persistence_mcp.MCPServer).where(persistence_mcp.MCPServer.uuid == server_uuid)

Some files were not shown because too many files have changed in this diff Show More