mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-26 03:44:58 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10ce31cc46 | ||
|
|
3b4f3c516b | ||
|
|
a1e3981ce4 | ||
|
|
89f26781fe | ||
|
|
914292a80b | ||
|
|
8227e3299b | ||
|
|
07ca48d652 | ||
|
|
243f45c7db | ||
|
|
12cfce3622 | ||
|
|
535c4a8a11 | ||
|
|
6606c671b2 | ||
|
|
242f24840d | ||
|
|
486f636b2d | ||
|
|
b293d7a7cd |
23
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
23
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -6,26 +6,19 @@ body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: 消息平台适配器
|
||||
description: "连接QQ使用的框架"
|
||||
description: "接入的消息平台类型"
|
||||
options:
|
||||
- 其他(或暂未使用)
|
||||
- Nakuru(go-cqhttp)
|
||||
- aiocqhttp(使用 OneBot 协议接入的)
|
||||
- qq-botpy(QQ官方API)
|
||||
- 其他
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: 运行环境
|
||||
description: 操作系统、系统架构、**Python版本**、**主机地理位置**
|
||||
placeholder: 例如: CentOS x64 Python 3.10.3、Docker 的直接写 Docker 就行
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: LangBot 版本
|
||||
description: LangBot (QChatGPT) 版本号
|
||||
placeholder: 例如:v3.3.0,可以使用`!version`命令查看,或者到 pkg/utils/constants.py 查看
|
||||
label: 运行环境
|
||||
description: LangBot 版本、操作系统、系统架构、**Python版本**、**主机地理位置**
|
||||
placeholder: 例如:v3.3.0、CentOS x64 Python 3.10.3、Docker 的系统直接写 Docker 就行
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -34,6 +27,12 @@ body:
|
||||
description: 完整描述异常情况,什么时候发生的、发生了什么。**请附带日志信息。**
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 复现步骤
|
||||
description: 如何重现这个问题,越详细越好
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 启用的插件
|
||||
|
||||
12
README.md
12
README.md
@@ -37,14 +37,14 @@
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 💬 大模型对话、Agent:支持多种大模型,适配群聊和私聊;具有多轮对话、工具调用、多模态能力,并支持接入 Dify。目前支持 QQ、QQ频道,后续还将支持微信、WhatsApp、Discord等平台。
|
||||
- 💬 大模型对话、Agent:支持多种大模型,适配群聊和私聊;具有多轮对话、工具调用、多模态能力,并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道,后续还将支持微信、WhatsApp、Discord等平台。
|
||||
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。
|
||||
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;丰富生态,目前已有数十个[插件](https://docs.langbot.app/plugin/plugin-intro.html)
|
||||
- 😻 [New] Web 管理面板:支持通过浏览器管理 LangBot 实例,具体支持功能,查看[文档](https://docs.langbot.app/webui/intro.html)
|
||||
|
||||
## 📦 开始使用
|
||||
|
||||
> **INFO**
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 在您开始任何方式部署之前,请务必阅读[新手指引](https://docs.langbot.app/insight/guide.html)。
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
|
||||
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
||||
|
||||
#### Railway 云部署
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### 手动部署
|
||||
|
||||
直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/deploy/langbot/manual.html)。
|
||||
@@ -69,3 +73,7 @@
|
||||
## 📸 效果展示
|
||||
|
||||
<img alt="回复效果(带有联网插件)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
|
||||
|
||||
- WebUI Demo: https://demo.langbot.dev/
|
||||
- 登录信息:邮箱:`demo@langbot.app` 密码:`langbot123456`
|
||||
- 注意:仅展示webui效果,公开环境,请不要在其中填入您的任何敏感信息。
|
||||
|
||||
@@ -143,9 +143,7 @@ class Application:
|
||||
self.logger.warning("WebUI 文件缺失,请根据文档获取:https://docs.langbot.app/webui/intro.html")
|
||||
return
|
||||
|
||||
import socket
|
||||
|
||||
host_ip = socket.gethostbyname(socket.gethostname())
|
||||
host_ip = "127.0.0.1"
|
||||
|
||||
public_ip = await ip.get_myip()
|
||||
|
||||
|
||||
@@ -61,9 +61,9 @@ class PreProcessor(stage.PipelineStage):
|
||||
)
|
||||
elif isinstance(me, platform_message.Image):
|
||||
if self.ap.provider_cfg.data['enable-vision'] and (self.ap.provider_cfg.data['runner'] != 'local-agent' or query.use_model.vision_supported):
|
||||
if me.url is not None:
|
||||
if me.base64 is not None:
|
||||
content_list.append(
|
||||
llm_entities.ContentElement.from_image_url(str(me.url))
|
||||
llm_entities.ContentElement.from_image_base64(me.base64)
|
||||
)
|
||||
|
||||
query.user_message = llm_entities.Message(
|
||||
|
||||
@@ -6,6 +6,7 @@ import time
|
||||
import datetime
|
||||
|
||||
import aiocqhttp
|
||||
import aiohttp
|
||||
|
||||
from .. import adapter
|
||||
from ...pipeline.longtext.strategies import forward
|
||||
@@ -13,12 +14,12 @@ from ...core import app
|
||||
from ..types import message as platform_message
|
||||
from ..types import events as platform_events
|
||||
from ..types import entities as platform_entities
|
||||
|
||||
from ...utils import image
|
||||
|
||||
class AiocqhttpMessageConverter(adapter.MessageConverter):
|
||||
|
||||
@staticmethod
|
||||
def yiri2target(message_chain: platform_message.MessageChain) -> typing.Tuple[list, int, datetime.datetime]:
|
||||
async def yiri2target(message_chain: platform_message.MessageChain) -> typing.Tuple[list, int, datetime.datetime]:
|
||||
msg_list = aiocqhttp.Message()
|
||||
|
||||
msg_id = 0
|
||||
@@ -59,7 +60,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
|
||||
elif type(msg) is forward.Forward:
|
||||
|
||||
for node in msg.node_list:
|
||||
msg_list.extend(AiocqhttpMessageConverter.yiri2target(node.message_chain)[0])
|
||||
msg_list.extend(await AiocqhttpMessageConverter.yiri2target(node.message_chain)[0])
|
||||
|
||||
else:
|
||||
msg_list.append(aiocqhttp.MessageSegment.text(str(msg)))
|
||||
@@ -67,7 +68,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
|
||||
return msg_list, msg_id, msg_time
|
||||
|
||||
@staticmethod
|
||||
def target2yiri(message: str, message_id: int = -1):
|
||||
async def target2yiri(message: str, message_id: int = -1):
|
||||
message = aiocqhttp.Message(message)
|
||||
|
||||
yiri_msg_list = []
|
||||
@@ -89,7 +90,8 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
|
||||
elif msg.type == "text":
|
||||
yiri_msg_list.append(platform_message.Plain(text=msg.data["text"]))
|
||||
elif msg.type == "image":
|
||||
yiri_msg_list.append(platform_message.Image(url=msg.data["url"]))
|
||||
image_base64, image_format = await image.qq_image_url_to_base64(msg.data['url'])
|
||||
yiri_msg_list.append(platform_message.Image(base64=f"data:image/{image_format};base64,{image_base64}"))
|
||||
|
||||
chain = platform_message.MessageChain(yiri_msg_list)
|
||||
|
||||
@@ -99,9 +101,9 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
|
||||
class AiocqhttpEventConverter(adapter.EventConverter):
|
||||
|
||||
@staticmethod
|
||||
def yiri2target(event: platform_events.Event, bot_account_id: int):
|
||||
async def yiri2target(event: platform_events.Event, bot_account_id: int):
|
||||
|
||||
msg, msg_id, msg_time = AiocqhttpMessageConverter.yiri2target(event.message_chain)
|
||||
msg, msg_id, msg_time = await AiocqhttpMessageConverter.yiri2target(event.message_chain)
|
||||
|
||||
if type(event) is platform_events.GroupMessage:
|
||||
role = "member"
|
||||
@@ -164,8 +166,8 @@ class AiocqhttpEventConverter(adapter.EventConverter):
|
||||
return aiocqhttp.Event.from_payload(payload)
|
||||
|
||||
@staticmethod
|
||||
def target2yiri(event: aiocqhttp.Event):
|
||||
yiri_chain = AiocqhttpMessageConverter.target2yiri(
|
||||
async def target2yiri(event: aiocqhttp.Event):
|
||||
yiri_chain = await AiocqhttpMessageConverter.target2yiri(
|
||||
event.message, event.message_id
|
||||
)
|
||||
|
||||
@@ -242,7 +244,7 @@ class AiocqhttpAdapter(adapter.MessageSourceAdapter):
|
||||
async def send_message(
|
||||
self, target_type: str, target_id: str, message: platform_message.MessageChain
|
||||
):
|
||||
aiocq_msg = AiocqhttpMessageConverter.yiri2target(message)[0]
|
||||
aiocq_msg = await AiocqhttpMessageConverter.yiri2target(message)[0]
|
||||
|
||||
if target_type == "group":
|
||||
await self.bot.send_group_msg(group_id=int(target_id), message=aiocq_msg)
|
||||
@@ -255,8 +257,8 @@ class AiocqhttpAdapter(adapter.MessageSourceAdapter):
|
||||
message: platform_message.MessageChain,
|
||||
quote_origin: bool = False,
|
||||
):
|
||||
aiocq_event = AiocqhttpEventConverter.yiri2target(message_source, self.bot_account_id)
|
||||
aiocq_msg = AiocqhttpMessageConverter.yiri2target(message)[0]
|
||||
aiocq_event = await AiocqhttpEventConverter.yiri2target(message_source, self.bot_account_id)
|
||||
aiocq_msg = (await AiocqhttpMessageConverter.yiri2target(message))[0]
|
||||
if quote_origin:
|
||||
aiocq_msg = aiocqhttp.MessageSegment.reply(aiocq_event.message_id) + aiocq_msg
|
||||
|
||||
@@ -276,7 +278,7 @@ class AiocqhttpAdapter(adapter.MessageSourceAdapter):
|
||||
async def on_message(event: aiocqhttp.Event):
|
||||
self.bot_account_id = event.self_id
|
||||
try:
|
||||
return await callback(self.event_converter.target2yiri(event), self)
|
||||
return await callback(await self.event_converter.target2yiri(event), self)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ class ContentElement(pydantic.BaseModel):
|
||||
|
||||
image_url: typing.Optional[ImageURLContentObject] = None
|
||||
|
||||
image_base64: typing.Optional[str] = None
|
||||
|
||||
def __str__(self):
|
||||
if self.type == 'text':
|
||||
return self.text
|
||||
@@ -53,6 +55,10 @@ class ContentElement(pydantic.BaseModel):
|
||||
@classmethod
|
||||
def from_image_url(cls, image_url: str):
|
||||
return cls(type='image_url', image_url=ImageURLContentObject(url=image_url))
|
||||
|
||||
@classmethod
|
||||
def from_image_base64(cls, image_base64: str):
|
||||
return cls(type='image_base64', image_base64=image_base64)
|
||||
|
||||
|
||||
class Message(pydantic.BaseModel):
|
||||
|
||||
@@ -48,6 +48,7 @@ class LLMAPIRequester(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
async def call(
|
||||
self,
|
||||
query: core_entities.Query,
|
||||
model: modelmgr_entities.LLMModelInfo,
|
||||
messages: typing.List[llm_entities.Message],
|
||||
funcs: typing.List[tools_entities.LLMFunction] = None,
|
||||
|
||||
@@ -2,8 +2,10 @@ from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import traceback
|
||||
import base64
|
||||
|
||||
import anthropic
|
||||
import httpx
|
||||
|
||||
from .. import entities, errors, requester
|
||||
|
||||
@@ -21,15 +23,24 @@ class AnthropicMessages(requester.LLMAPIRequester):
|
||||
client: anthropic.AsyncAnthropic
|
||||
|
||||
async def initialize(self):
|
||||
|
||||
httpx_client = anthropic._base_client.AsyncHttpxClientWrapper(
|
||||
base_url=self.ap.provider_cfg.data['requester']['anthropic-messages']['base-url'],
|
||||
# cast to a valid type because mypy doesn't understand our type narrowing
|
||||
timeout=typing.cast(httpx.Timeout, self.ap.provider_cfg.data['requester']['anthropic-messages']['timeout']),
|
||||
limits=anthropic._constants.DEFAULT_CONNECTION_LIMITS,
|
||||
follow_redirects=True,
|
||||
proxies=self.ap.proxy_mgr.get_forward_proxies()
|
||||
)
|
||||
|
||||
self.client = anthropic.AsyncAnthropic(
|
||||
api_key="",
|
||||
base_url=self.ap.provider_cfg.data['requester']['anthropic-messages']['base-url'],
|
||||
timeout=self.ap.provider_cfg.data['requester']['anthropic-messages']['timeout'],
|
||||
proxies=self.ap.proxy_mgr.get_forward_proxies()
|
||||
http_client=httpx_client,
|
||||
)
|
||||
|
||||
async def call(
|
||||
self,
|
||||
query: core_entities.Query,
|
||||
model: entities.LLMModelInfo,
|
||||
messages: typing.List[llm_entities.Message],
|
||||
funcs: typing.List[tools_entities.LLMFunction] = None,
|
||||
@@ -61,24 +72,20 @@ class AnthropicMessages(requester.LLMAPIRequester):
|
||||
if isinstance(m.content, str) and m.content.strip() != "":
|
||||
req_messages.append(m.dict(exclude_none=True))
|
||||
elif isinstance(m.content, list):
|
||||
# m.content = [
|
||||
# c for c in m.content if c.type == "text"
|
||||
# ]
|
||||
|
||||
# if len(m.content) > 0:
|
||||
# req_messages.append(m.dict(exclude_none=True))
|
||||
|
||||
msg_dict = m.dict(exclude_none=True)
|
||||
|
||||
for i, ce in enumerate(m.content):
|
||||
if ce.type == "image_url":
|
||||
base64_image, image_format = await image.qq_image_url_to_base64(ce.image_url.url)
|
||||
|
||||
if ce.type == "image_base64":
|
||||
image_b64, image_format = await image.extract_b64_and_format(ce.image_base64)
|
||||
|
||||
alter_image_ele = {
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": f"image/{image_format}",
|
||||
"data": base64_image
|
||||
"data": image_b64
|
||||
}
|
||||
}
|
||||
msg_dict["content"][i] = alter_image_ele
|
||||
|
||||
@@ -65,6 +65,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
|
||||
|
||||
async def _closure(
|
||||
self,
|
||||
query: core_entities.Query,
|
||||
req_messages: list[dict],
|
||||
use_model: entities.LLMModelInfo,
|
||||
use_funcs: list[tools_entities.LLMFunction] = None,
|
||||
@@ -87,8 +88,12 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
|
||||
for msg in messages:
|
||||
if 'content' in msg and isinstance(msg["content"], list):
|
||||
for me in msg["content"]:
|
||||
if me["type"] == "image_url":
|
||||
me["image_url"]['url'] = await self.get_base64_str(me["image_url"]['url'])
|
||||
if me["type"] == "image_base64":
|
||||
me["image_url"] = {
|
||||
"url": me["image_base64"]
|
||||
}
|
||||
me["type"] = "image_url"
|
||||
del me["image_base64"]
|
||||
|
||||
args["messages"] = messages
|
||||
|
||||
@@ -102,6 +107,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
|
||||
|
||||
async def call(
|
||||
self,
|
||||
query: core_entities.Query,
|
||||
model: entities.LLMModelInfo,
|
||||
messages: typing.List[llm_entities.Message],
|
||||
funcs: typing.List[tools_entities.LLMFunction] = None,
|
||||
@@ -118,7 +124,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
|
||||
req_messages.append(msg_dict)
|
||||
|
||||
try:
|
||||
return await self._closure(req_messages, model, funcs)
|
||||
return await self._closure(query, req_messages, model, funcs)
|
||||
except asyncio.TimeoutError:
|
||||
raise errors.RequesterError('请求超时')
|
||||
except openai.BadRequestError as e:
|
||||
@@ -134,11 +140,3 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
|
||||
raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}')
|
||||
except openai.APIError as e:
|
||||
raise errors.RequesterError(f'请求错误: {e.message}')
|
||||
|
||||
@async_lru.alru_cache(maxsize=128)
|
||||
async def get_base64_str(
|
||||
self,
|
||||
original_url: str,
|
||||
) -> str:
|
||||
base64_image, image_format = await image.qq_image_url_to_base64(original_url)
|
||||
return f"data:image/{image_format};base64,{base64_image}"
|
||||
|
||||
@@ -6,6 +6,7 @@ import typing
|
||||
from typing import Union, Mapping, Any, AsyncIterator
|
||||
import uuid
|
||||
import json
|
||||
import base64
|
||||
|
||||
import async_lru
|
||||
import ollama
|
||||
@@ -13,7 +14,7 @@ import ollama
|
||||
from .. import entities, errors, requester
|
||||
from ... import entities as llm_entities
|
||||
from ...tools import entities as tools_entities
|
||||
from ....core import app
|
||||
from ....core import app, entities as core_entities
|
||||
from ....utils import image
|
||||
|
||||
REQUESTER_NAME: str = "ollama-chat"
|
||||
@@ -43,7 +44,7 @@ class OllamaChatCompletions(requester.LLMAPIRequester):
|
||||
**args
|
||||
)
|
||||
|
||||
async def _closure(self, req_messages: list[dict], use_model: entities.LLMModelInfo,
|
||||
async def _closure(self, query: core_entities.Query, req_messages: list[dict], use_model: entities.LLMModelInfo,
|
||||
user_funcs: list[tools_entities.LLMFunction] = None) -> (
|
||||
llm_entities.Message):
|
||||
args: Any = self.request_cfg['args'].copy()
|
||||
@@ -57,9 +58,9 @@ class OllamaChatCompletions(requester.LLMAPIRequester):
|
||||
for me in msg["content"]:
|
||||
if me["type"] == "text":
|
||||
text_content.append(me["text"])
|
||||
elif me["type"] == "image_url":
|
||||
image_url = await self.get_base64_str(me["image_url"]['url'])
|
||||
image_urls.append(image_url)
|
||||
elif me["type"] == "image_base64":
|
||||
image_urls.append(me["image_base64"])
|
||||
|
||||
msg["content"] = "\n".join(text_content)
|
||||
msg["images"] = [url.split(',')[1] for url in image_urls]
|
||||
if 'tool_calls' in msg: # LangBot 内部以 str 存储 tool_calls 的参数,这里需要转换为 dict
|
||||
@@ -109,6 +110,7 @@ class OllamaChatCompletions(requester.LLMAPIRequester):
|
||||
|
||||
async def call(
|
||||
self,
|
||||
query: core_entities.Query,
|
||||
model: entities.LLMModelInfo,
|
||||
messages: typing.List[llm_entities.Message],
|
||||
funcs: typing.List[tools_entities.LLMFunction] = None,
|
||||
@@ -122,14 +124,6 @@ class OllamaChatCompletions(requester.LLMAPIRequester):
|
||||
msg_dict["content"] = "\n".join(part["text"] for part in content)
|
||||
req_messages.append(msg_dict)
|
||||
try:
|
||||
return await self._closure(req_messages, model, funcs)
|
||||
return await self._closure(query, req_messages, model, funcs)
|
||||
except asyncio.TimeoutError:
|
||||
raise errors.RequesterError('请求超时')
|
||||
|
||||
@async_lru.alru_cache(maxsize=128)
|
||||
async def get_base64_str(
|
||||
self,
|
||||
original_url: str,
|
||||
) -> str:
|
||||
base64_image, image_format = await image.qq_image_url_to_base64(original_url)
|
||||
return f"data:image/{image_format};base64,{base64_image}"
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import typing
|
||||
import json
|
||||
import uuid
|
||||
import base64
|
||||
|
||||
from .. import runner
|
||||
from ...core import entities as core_entities
|
||||
@@ -52,10 +53,9 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
||||
for ce in query.user_message.content:
|
||||
if ce.type == "text":
|
||||
plain_text += ce.text
|
||||
elif ce.type == "image_url":
|
||||
file_bytes, image_format = await image.get_qq_image_bytes(
|
||||
ce.image_url.url
|
||||
)
|
||||
elif ce.type == "image_base64":
|
||||
image_b64, image_format = await image.extract_b64_and_format(ce.image_base64)
|
||||
file_bytes = base64.b64decode(image_b64)
|
||||
file = ("img.png", file_bytes, f"image/{image_format}")
|
||||
file_upload_resp = await self.dify_client.upload_file(
|
||||
file,
|
||||
|
||||
@@ -23,7 +23,7 @@ class LocalAgentRunner(runner.RequestRunner):
|
||||
req_messages = query.prompt.messages.copy() + query.messages.copy() + [query.user_message]
|
||||
|
||||
# 首次请求
|
||||
msg = await query.use_model.requester.call(query.use_model, req_messages, query.use_funcs)
|
||||
msg = await query.use_model.requester.call(query, query.use_model, req_messages, query.use_funcs)
|
||||
|
||||
yield msg
|
||||
|
||||
@@ -61,7 +61,7 @@ class LocalAgentRunner(runner.RequestRunner):
|
||||
req_messages.append(err_msg)
|
||||
|
||||
# 处理完所有调用,再次请求
|
||||
msg = await query.use_model.requester.call(query.use_model, req_messages, query.use_funcs)
|
||||
msg = await query.use_model.requester.call(query, query.use_model, req_messages, query.use_funcs)
|
||||
|
||||
yield msg
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
semantic_version = "v3.4.1.2"
|
||||
semantic_version = "v3.4.1.3"
|
||||
|
||||
debug_mode = False
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import base64
|
||||
import typing
|
||||
import io
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
import ssl
|
||||
|
||||
import aiohttp
|
||||
import PIL.Image
|
||||
|
||||
|
||||
def get_qq_image_downloadable_url(image_url: str) -> tuple[str, dict]:
|
||||
@@ -13,9 +15,10 @@ def get_qq_image_downloadable_url(image_url: str) -> tuple[str, dict]:
|
||||
return f"http://{parsed.netloc}{parsed.path}", query
|
||||
|
||||
|
||||
async def get_qq_image_bytes(image_url: str) -> tuple[bytes, str]:
|
||||
"""获取QQ图片的bytes"""
|
||||
image_url, query = get_qq_image_downloadable_url(image_url)
|
||||
async def get_qq_image_bytes(image_url: str, query: dict={}) -> tuple[bytes, str]:
|
||||
"""[弃用]获取QQ图片的bytes"""
|
||||
image_url, query_in_url = get_qq_image_downloadable_url(image_url)
|
||||
query = {**query, **query_in_url}
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
@@ -24,8 +27,11 @@ async def get_qq_image_bytes(image_url: str) -> tuple[bytes, str]:
|
||||
resp.raise_for_status()
|
||||
file_bytes = await resp.read()
|
||||
content_type = resp.headers.get('Content-Type')
|
||||
if not content_type or not content_type.startswith('image/'):
|
||||
if not content_type:
|
||||
image_format = 'jpeg'
|
||||
elif not content_type.startswith('image/'):
|
||||
pil_img = PIL.Image.open(io.BytesIO(file_bytes))
|
||||
image_format = pil_img.format.lower()
|
||||
else:
|
||||
image_format = content_type.split('/')[-1]
|
||||
return file_bytes, image_format
|
||||
@@ -34,7 +40,7 @@ async def get_qq_image_bytes(image_url: str) -> tuple[bytes, str]:
|
||||
async def qq_image_url_to_base64(
|
||||
image_url: str
|
||||
) -> typing.Tuple[str, str]:
|
||||
"""将QQ图片URL转为base64,并返回图片格式
|
||||
"""[弃用]将QQ图片URL转为base64,并返回图片格式
|
||||
|
||||
Args:
|
||||
image_url (str): QQ图片URL
|
||||
@@ -47,8 +53,18 @@ async def qq_image_url_to_base64(
|
||||
# Flatten the query dictionary
|
||||
query = {k: v[0] for k, v in query.items()}
|
||||
|
||||
file_bytes, image_format = await get_qq_image_bytes(image_url)
|
||||
file_bytes, image_format = await get_qq_image_bytes(image_url, query)
|
||||
|
||||
base64_str = base64.b64encode(file_bytes).decode()
|
||||
|
||||
return base64_str, image_format
|
||||
|
||||
async def extract_b64_and_format(image_base64_data: str) -> typing.Tuple[str, str]:
|
||||
"""提取base64编码和图片格式
|
||||
|
||||
data:image/jpeg;base64,xxx
|
||||
提取出base64编码和图片格式
|
||||
"""
|
||||
base64_str = image_base64_data.split(',')[-1]
|
||||
image_format = image_base64_data.split(':')[-1].split(';')[0].split('/')[-1]
|
||||
return base64_str, image_format
|
||||
@@ -2,7 +2,7 @@ import aiohttp
|
||||
|
||||
async def get_myip() -> str:
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
|
||||
async with session.get("https://ip.useragentinfo.com/myip") as response:
|
||||
return await response.text()
|
||||
except Exception as e:
|
||||
|
||||
@@ -255,6 +255,24 @@
|
||||
"api-key": {
|
||||
"type": "string",
|
||||
"title": "API 密钥"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "number",
|
||||
"title":"API 请求超时时间"
|
||||
}
|
||||
}
|
||||
},
|
||||
"agent": {
|
||||
"type": "object",
|
||||
"title": "Agent API 参数",
|
||||
"properties": {
|
||||
"api-key": {
|
||||
"type": "string",
|
||||
"title": "API 密钥"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "number",
|
||||
"title":"API 请求超时时间"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -271,6 +289,10 @@
|
||||
"title": "工作流输出键",
|
||||
"description": "设置工作流输出键,用于从 Dify Workflow 结束节点返回的 JSON 数据中提取输出内容",
|
||||
"default": "summary"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "number",
|
||||
"title": "API 请求超时时间"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user