Compare commits

...

14 Commits

Author SHA1 Message Date
Junyan Qin
10ce31cc46 chore: release v3.4.1.3 2024-12-24 19:26:39 +08:00
Junyan Qin
3b4f3c516b Update README.md 2024-12-24 19:08:35 +08:00
Junyan Qin
a1e3981ce4 chore: 更新issue模板 2024-12-24 15:51:19 +08:00
Junyan Qin
89f26781fe chore: 更新issue模板 2024-12-24 15:50:41 +08:00
Junyan Qin
914292a80b chore: 修改issue模板 2024-12-24 15:47:59 +08:00
Junyan Qin
8227e3299b Merge pull request #963 from RockChinQ/feat/dl-image-by-adapters
fix: 下载 QQ 图片时的400问题
2024-12-24 11:28:31 +08:00
Junyan Qin
07ca48d652 fix: 无法传递qq图片的问题 2024-12-24 11:26:33 +08:00
Junyan Qin
243f45c7db fix: 使用 header 避免400 2024-12-24 11:09:45 +08:00
Junyan Qin
12cfce3622 feat: 重构图片消息传递逻辑 (#957, #955) 2024-12-24 10:57:17 +08:00
Junyan Qin
535c4a8a11 fix: anthropic sdk删除proxies导致无法启动 (#962, #960) 2024-12-23 21:35:16 +08:00
Junyan Qin
6606c671b2 doc: README添加demo 2024-12-23 10:43:52 +08:00
Junyan Qin
242f24840d fix: 为dify agent设置项更新schema 2024-12-17 16:24:00 +08:00
Junyan Qin
486f636b2d doc(README): 添加 railway 部署按钮 2024-12-17 14:49:41 +08:00
Junyan Qin
b293d7a7cd doc: README 2024-12-17 01:30:39 +08:00
16 changed files with 134 additions and 83 deletions

View File

@@ -6,26 +6,19 @@ body:
- type: dropdown - type: dropdown
attributes: attributes:
label: 消息平台适配器 label: 消息平台适配器
description: "连接QQ使用的框架" description: "接入的消息平台类型"
options: options:
- 其他(或暂未使用)
- Nakurugo-cqhttp - Nakurugo-cqhttp
- aiocqhttp使用 OneBot 协议接入的) - aiocqhttp使用 OneBot 协议接入的)
- qq-botpyQQ官方API - qq-botpyQQ官方API
- 其他
validations:
required: false
- type: input
attributes:
label: 运行环境
description: 操作系统、系统架构、**Python版本**、**主机地理位置**
placeholder: 例如: CentOS x64 Python 3.10.3、Docker 的直接写 Docker 就行
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: LangBot 版本 label: 运行环境
description: LangBot (QChatGPT) 版本号 description: LangBot 版本、操作系统、系统架构、**Python版本**、**主机地理位置**
placeholder: 例如v3.3.0,可以使用`!version`命令查看,或者到 pkg/utils/constants.py 查看 placeholder: 例如v3.3.0、CentOS x64 Python 3.10.3、Docker 的系统直接写 Docker 就行
validations: validations:
required: true required: true
- type: textarea - type: textarea
@@ -34,6 +27,12 @@ body:
description: 完整描述异常情况,什么时候发生的、发生了什么。**请附带日志信息。** description: 完整描述异常情况,什么时候发生的、发生了什么。**请附带日志信息。**
validations: validations:
required: true required: true
- type: textarea
attributes:
label: 复现步骤
description: 如何重现这个问题,越详细越好
validations:
required: false
- type: textarea - type: textarea
attributes: attributes:
label: 启用的插件 label: 启用的插件

View File

@@ -37,14 +37,14 @@
## ✨ Features ## ✨ Features
- 💬 大模型对话、Agent支持多种大模型适配群聊和私聊具有多轮对话、工具调用、多模态能力支持接入 Dify。目前支持 QQ、QQ频道后续还将支持微信、WhatsApp、Discord等平台。 - 💬 大模型对话、Agent支持多种大模型适配群聊和私聊具有多轮对话、工具调用、多模态能力深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道后续还将支持微信、WhatsApp、Discord等平台。
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。 - 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;丰富生态,目前已有数十个[插件](https://docs.langbot.app/plugin/plugin-intro.html) - 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;丰富生态,目前已有数十个[插件](https://docs.langbot.app/plugin/plugin-intro.html)
- 😻 [New] Web 管理面板:支持通过浏览器管理 LangBot 实例,具体支持功能,查看[文档](https://docs.langbot.app/webui/intro.html) - 😻 [New] Web 管理面板:支持通过浏览器管理 LangBot 实例,具体支持功能,查看[文档](https://docs.langbot.app/webui/intro.html)
## 📦 开始使用 ## 📦 开始使用
> **INFO** > [!IMPORTANT]
> >
> 在您开始任何方式部署之前,请务必阅读[新手指引](https://docs.langbot.app/insight/guide.html)。 > 在您开始任何方式部署之前,请务必阅读[新手指引](https://docs.langbot.app/insight/guide.html)。
@@ -62,6 +62,10 @@
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH)
#### Railway 云部署
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
#### 手动部署 #### 手动部署
直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/deploy/langbot/manual.html)。 直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/deploy/langbot/manual.html)。
@@ -69,3 +73,7 @@
## 📸 效果展示 ## 📸 效果展示
<img alt="回复效果(带有联网插件)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/> <img alt="回复效果(带有联网插件)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
- WebUI Demo: https://demo.langbot.dev/
- 登录信息:邮箱:`demo@langbot.app` 密码:`langbot123456`
- 注意仅展示webui效果公开环境请不要在其中填入您的任何敏感信息。

View File

@@ -143,9 +143,7 @@ class Application:
self.logger.warning("WebUI 文件缺失请根据文档获取https://docs.langbot.app/webui/intro.html") self.logger.warning("WebUI 文件缺失请根据文档获取https://docs.langbot.app/webui/intro.html")
return return
import socket host_ip = "127.0.0.1"
host_ip = socket.gethostbyname(socket.gethostname())
public_ip = await ip.get_myip() public_ip = await ip.get_myip()

View File

@@ -61,9 +61,9 @@ class PreProcessor(stage.PipelineStage):
) )
elif isinstance(me, platform_message.Image): 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 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( 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( query.user_message = llm_entities.Message(

View File

@@ -6,6 +6,7 @@ import time
import datetime import datetime
import aiocqhttp import aiocqhttp
import aiohttp
from .. import adapter from .. import adapter
from ...pipeline.longtext.strategies import forward from ...pipeline.longtext.strategies import forward
@@ -13,12 +14,12 @@ from ...core import app
from ..types import message as platform_message from ..types import message as platform_message
from ..types import events as platform_events from ..types import events as platform_events
from ..types import entities as platform_entities from ..types import entities as platform_entities
from ...utils import image
class AiocqhttpMessageConverter(adapter.MessageConverter): class AiocqhttpMessageConverter(adapter.MessageConverter):
@staticmethod @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_list = aiocqhttp.Message()
msg_id = 0 msg_id = 0
@@ -59,7 +60,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
elif type(msg) is forward.Forward: elif type(msg) is forward.Forward:
for node in msg.node_list: 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: else:
msg_list.append(aiocqhttp.MessageSegment.text(str(msg))) msg_list.append(aiocqhttp.MessageSegment.text(str(msg)))
@@ -67,7 +68,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
return msg_list, msg_id, msg_time return msg_list, msg_id, msg_time
@staticmethod @staticmethod
def target2yiri(message: str, message_id: int = -1): async def target2yiri(message: str, message_id: int = -1):
message = aiocqhttp.Message(message) message = aiocqhttp.Message(message)
yiri_msg_list = [] yiri_msg_list = []
@@ -89,7 +90,8 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
elif msg.type == "text": elif msg.type == "text":
yiri_msg_list.append(platform_message.Plain(text=msg.data["text"])) yiri_msg_list.append(platform_message.Plain(text=msg.data["text"]))
elif msg.type == "image": 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) chain = platform_message.MessageChain(yiri_msg_list)
@@ -99,9 +101,9 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
class AiocqhttpEventConverter(adapter.EventConverter): class AiocqhttpEventConverter(adapter.EventConverter):
@staticmethod @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: if type(event) is platform_events.GroupMessage:
role = "member" role = "member"
@@ -164,8 +166,8 @@ class AiocqhttpEventConverter(adapter.EventConverter):
return aiocqhttp.Event.from_payload(payload) return aiocqhttp.Event.from_payload(payload)
@staticmethod @staticmethod
def target2yiri(event: aiocqhttp.Event): async def target2yiri(event: aiocqhttp.Event):
yiri_chain = AiocqhttpMessageConverter.target2yiri( yiri_chain = await AiocqhttpMessageConverter.target2yiri(
event.message, event.message_id event.message, event.message_id
) )
@@ -242,7 +244,7 @@ class AiocqhttpAdapter(adapter.MessageSourceAdapter):
async def send_message( async def send_message(
self, target_type: str, target_id: str, message: platform_message.MessageChain 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": if target_type == "group":
await self.bot.send_group_msg(group_id=int(target_id), message=aiocq_msg) 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, message: platform_message.MessageChain,
quote_origin: bool = False, quote_origin: bool = False,
): ):
aiocq_event = AiocqhttpEventConverter.yiri2target(message_source, self.bot_account_id) aiocq_event = await AiocqhttpEventConverter.yiri2target(message_source, self.bot_account_id)
aiocq_msg = AiocqhttpMessageConverter.yiri2target(message)[0] aiocq_msg = (await AiocqhttpMessageConverter.yiri2target(message))[0]
if quote_origin: if quote_origin:
aiocq_msg = aiocqhttp.MessageSegment.reply(aiocq_event.message_id) + aiocq_msg 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): async def on_message(event: aiocqhttp.Event):
self.bot_account_id = event.self_id self.bot_account_id = event.self_id
try: try:
return await callback(self.event_converter.target2yiri(event), self) return await callback(await self.event_converter.target2yiri(event), self)
except: except:
traceback.print_exc() traceback.print_exc()

View File

@@ -38,6 +38,8 @@ class ContentElement(pydantic.BaseModel):
image_url: typing.Optional[ImageURLContentObject] = None image_url: typing.Optional[ImageURLContentObject] = None
image_base64: typing.Optional[str] = None
def __str__(self): def __str__(self):
if self.type == 'text': if self.type == 'text':
return self.text return self.text
@@ -53,6 +55,10 @@ class ContentElement(pydantic.BaseModel):
@classmethod @classmethod
def from_image_url(cls, image_url: str): def from_image_url(cls, image_url: str):
return cls(type='image_url', image_url=ImageURLContentObject(url=image_url)) 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): class Message(pydantic.BaseModel):

View File

@@ -48,6 +48,7 @@ class LLMAPIRequester(metaclass=abc.ABCMeta):
@abc.abstractmethod @abc.abstractmethod
async def call( async def call(
self, self,
query: core_entities.Query,
model: modelmgr_entities.LLMModelInfo, model: modelmgr_entities.LLMModelInfo,
messages: typing.List[llm_entities.Message], messages: typing.List[llm_entities.Message],
funcs: typing.List[tools_entities.LLMFunction] = None, funcs: typing.List[tools_entities.LLMFunction] = None,

View File

@@ -2,8 +2,10 @@ from __future__ import annotations
import typing import typing
import traceback import traceback
import base64
import anthropic import anthropic
import httpx
from .. import entities, errors, requester from .. import entities, errors, requester
@@ -21,15 +23,24 @@ class AnthropicMessages(requester.LLMAPIRequester):
client: anthropic.AsyncAnthropic client: anthropic.AsyncAnthropic
async def initialize(self): 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( self.client = anthropic.AsyncAnthropic(
api_key="", api_key="",
base_url=self.ap.provider_cfg.data['requester']['anthropic-messages']['base-url'], http_client=httpx_client,
timeout=self.ap.provider_cfg.data['requester']['anthropic-messages']['timeout'],
proxies=self.ap.proxy_mgr.get_forward_proxies()
) )
async def call( async def call(
self, self,
query: core_entities.Query,
model: entities.LLMModelInfo, model: entities.LLMModelInfo,
messages: typing.List[llm_entities.Message], messages: typing.List[llm_entities.Message],
funcs: typing.List[tools_entities.LLMFunction] = None, funcs: typing.List[tools_entities.LLMFunction] = None,
@@ -61,24 +72,20 @@ class AnthropicMessages(requester.LLMAPIRequester):
if isinstance(m.content, str) and m.content.strip() != "": if isinstance(m.content, str) and m.content.strip() != "":
req_messages.append(m.dict(exclude_none=True)) req_messages.append(m.dict(exclude_none=True))
elif isinstance(m.content, list): 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) msg_dict = m.dict(exclude_none=True)
for i, ce in enumerate(m.content): 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 = { alter_image_ele = {
"type": "image", "type": "image",
"source": { "source": {
"type": "base64", "type": "base64",
"media_type": f"image/{image_format}", "media_type": f"image/{image_format}",
"data": base64_image "data": image_b64
} }
} }
msg_dict["content"][i] = alter_image_ele msg_dict["content"][i] = alter_image_ele

View File

@@ -65,6 +65,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
async def _closure( async def _closure(
self, self,
query: core_entities.Query,
req_messages: list[dict], req_messages: list[dict],
use_model: entities.LLMModelInfo, use_model: entities.LLMModelInfo,
use_funcs: list[tools_entities.LLMFunction] = None, use_funcs: list[tools_entities.LLMFunction] = None,
@@ -87,8 +88,12 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
for msg in messages: for msg in messages:
if 'content' in msg and isinstance(msg["content"], list): if 'content' in msg and isinstance(msg["content"], list):
for me in msg["content"]: for me in msg["content"]:
if me["type"] == "image_url": if me["type"] == "image_base64":
me["image_url"]['url'] = await self.get_base64_str(me["image_url"]['url']) me["image_url"] = {
"url": me["image_base64"]
}
me["type"] = "image_url"
del me["image_base64"]
args["messages"] = messages args["messages"] = messages
@@ -102,6 +107,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
async def call( async def call(
self, self,
query: core_entities.Query,
model: entities.LLMModelInfo, model: entities.LLMModelInfo,
messages: typing.List[llm_entities.Message], messages: typing.List[llm_entities.Message],
funcs: typing.List[tools_entities.LLMFunction] = None, funcs: typing.List[tools_entities.LLMFunction] = None,
@@ -118,7 +124,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
req_messages.append(msg_dict) req_messages.append(msg_dict)
try: try:
return await self._closure(req_messages, model, funcs) return await self._closure(query, req_messages, model, funcs)
except asyncio.TimeoutError: except asyncio.TimeoutError:
raise errors.RequesterError('请求超时') raise errors.RequesterError('请求超时')
except openai.BadRequestError as e: except openai.BadRequestError as e:
@@ -134,11 +140,3 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}') raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}')
except openai.APIError as e: except openai.APIError as e:
raise errors.RequesterError(f'请求错误: {e.message}') 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}"

View File

@@ -6,6 +6,7 @@ import typing
from typing import Union, Mapping, Any, AsyncIterator from typing import Union, Mapping, Any, AsyncIterator
import uuid import uuid
import json import json
import base64
import async_lru import async_lru
import ollama import ollama
@@ -13,7 +14,7 @@ import ollama
from .. import entities, errors, requester from .. import entities, errors, requester
from ... import entities as llm_entities from ... import entities as llm_entities
from ...tools import entities as tools_entities from ...tools import entities as tools_entities
from ....core import app from ....core import app, entities as core_entities
from ....utils import image from ....utils import image
REQUESTER_NAME: str = "ollama-chat" REQUESTER_NAME: str = "ollama-chat"
@@ -43,7 +44,7 @@ class OllamaChatCompletions(requester.LLMAPIRequester):
**args **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) -> ( user_funcs: list[tools_entities.LLMFunction] = None) -> (
llm_entities.Message): llm_entities.Message):
args: Any = self.request_cfg['args'].copy() args: Any = self.request_cfg['args'].copy()
@@ -57,9 +58,9 @@ class OllamaChatCompletions(requester.LLMAPIRequester):
for me in msg["content"]: for me in msg["content"]:
if me["type"] == "text": if me["type"] == "text":
text_content.append(me["text"]) text_content.append(me["text"])
elif me["type"] == "image_url": elif me["type"] == "image_base64":
image_url = await self.get_base64_str(me["image_url"]['url']) image_urls.append(me["image_base64"])
image_urls.append(image_url)
msg["content"] = "\n".join(text_content) msg["content"] = "\n".join(text_content)
msg["images"] = [url.split(',')[1] for url in image_urls] msg["images"] = [url.split(',')[1] for url in image_urls]
if 'tool_calls' in msg: # LangBot 内部以 str 存储 tool_calls 的参数,这里需要转换为 dict if 'tool_calls' in msg: # LangBot 内部以 str 存储 tool_calls 的参数,这里需要转换为 dict
@@ -109,6 +110,7 @@ class OllamaChatCompletions(requester.LLMAPIRequester):
async def call( async def call(
self, self,
query: core_entities.Query,
model: entities.LLMModelInfo, model: entities.LLMModelInfo,
messages: typing.List[llm_entities.Message], messages: typing.List[llm_entities.Message],
funcs: typing.List[tools_entities.LLMFunction] = None, 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) msg_dict["content"] = "\n".join(part["text"] for part in content)
req_messages.append(msg_dict) req_messages.append(msg_dict)
try: try:
return await self._closure(req_messages, model, funcs) return await self._closure(query, req_messages, model, funcs)
except asyncio.TimeoutError: except asyncio.TimeoutError:
raise errors.RequesterError('请求超时') 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}"

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import typing import typing
import json import json
import uuid import uuid
import base64
from .. import runner from .. import runner
from ...core import entities as core_entities from ...core import entities as core_entities
@@ -52,10 +53,9 @@ class DifyServiceAPIRunner(runner.RequestRunner):
for ce in query.user_message.content: for ce in query.user_message.content:
if ce.type == "text": if ce.type == "text":
plain_text += ce.text plain_text += ce.text
elif ce.type == "image_url": elif ce.type == "image_base64":
file_bytes, image_format = await image.get_qq_image_bytes( image_b64, image_format = await image.extract_b64_and_format(ce.image_base64)
ce.image_url.url file_bytes = base64.b64decode(image_b64)
)
file = ("img.png", file_bytes, f"image/{image_format}") file = ("img.png", file_bytes, f"image/{image_format}")
file_upload_resp = await self.dify_client.upload_file( file_upload_resp = await self.dify_client.upload_file(
file, file,

View File

@@ -23,7 +23,7 @@ class LocalAgentRunner(runner.RequestRunner):
req_messages = query.prompt.messages.copy() + query.messages.copy() + [query.user_message] 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 yield msg
@@ -61,7 +61,7 @@ class LocalAgentRunner(runner.RequestRunner):
req_messages.append(err_msg) 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 yield msg

View File

@@ -1,4 +1,4 @@
semantic_version = "v3.4.1.2" semantic_version = "v3.4.1.3"
debug_mode = False debug_mode = False

View File

@@ -1,9 +1,11 @@
import base64 import base64
import typing import typing
import io
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
import ssl import ssl
import aiohttp import aiohttp
import PIL.Image
def get_qq_image_downloadable_url(image_url: str) -> tuple[str, dict]: 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 return f"http://{parsed.netloc}{parsed.path}", query
async def get_qq_image_bytes(image_url: str) -> tuple[bytes, str]: async def get_qq_image_bytes(image_url: str, query: dict={}) -> tuple[bytes, str]:
"""获取QQ图片的bytes""" """[弃用]获取QQ图片的bytes"""
image_url, query = get_qq_image_downloadable_url(image_url) 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 = ssl.create_default_context()
ssl_context.check_hostname = False ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE 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() resp.raise_for_status()
file_bytes = await resp.read() file_bytes = await resp.read()
content_type = resp.headers.get('Content-Type') content_type = resp.headers.get('Content-Type')
if not content_type or not content_type.startswith('image/'): if not content_type:
image_format = 'jpeg' 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: else:
image_format = content_type.split('/')[-1] image_format = content_type.split('/')[-1]
return file_bytes, image_format 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( async def qq_image_url_to_base64(
image_url: str image_url: str
) -> typing.Tuple[str, str]: ) -> typing.Tuple[str, str]:
"""将QQ图片URL转为base64并返回图片格式 """[弃用]将QQ图片URL转为base64并返回图片格式
Args: Args:
image_url (str): QQ图片URL image_url (str): QQ图片URL
@@ -47,8 +53,18 @@ async def qq_image_url_to_base64(
# Flatten the query dictionary # Flatten the query dictionary
query = {k: v[0] for k, v in query.items()} 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() base64_str = base64.b64encode(file_bytes).decode()
return base64_str, image_format 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

View File

@@ -2,7 +2,7 @@ import aiohttp
async def get_myip() -> str: async def get_myip() -> str:
try: 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: async with session.get("https://ip.useragentinfo.com/myip") as response:
return await response.text() return await response.text()
except Exception as e: except Exception as e:

View File

@@ -255,6 +255,24 @@
"api-key": { "api-key": {
"type": "string", "type": "string",
"title": "API 密钥" "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": "工作流输出键", "title": "工作流输出键",
"description": "设置工作流输出键,用于从 Dify Workflow 结束节点返回的 JSON 数据中提取输出内容", "description": "设置工作流输出键,用于从 Dify Workflow 结束节点返回的 JSON 数据中提取输出内容",
"default": "summary" "default": "summary"
},
"timeout": {
"type": "number",
"title": "API 请求超时时间"
} }
} }
} }