mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-04 12:56:02 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89b25b8985 | ||
|
|
46b4482a7d | ||
|
|
d9fa1cbb06 | ||
|
|
8858f432b5 | ||
|
|
8f5ec48522 | ||
|
|
83ff64698b | ||
|
|
87ecb4e519 | ||
|
|
df524b8a7a | ||
|
|
8a7df423ab | ||
|
|
cafd623c92 | ||
|
|
4df11ef064 | ||
|
|
aa7c08ee00 | ||
|
|
b98de29b07 | ||
|
|
c7c2eb4518 | ||
|
|
37fa318258 | ||
|
|
ff7bebb782 | ||
|
|
30bb26f898 |
@@ -69,7 +69,7 @@ docker compose up -d
|
|||||||
|
|
||||||
## ✨ 特性
|
## ✨ 特性
|
||||||
|
|
||||||
- 💬 大模型对话、Agent:支持多种大模型,适配群聊和私聊;具有多轮对话、工具调用、多模态能力,自带 RAG(知识库)实现,并深度适配 [Dify](https://dify.ai)。
|
- 💬 大模型对话、Agent:支持多种大模型,适配群聊和私聊;具有多轮对话、工具调用、多模态、流式输出能力,自带 RAG(知识库)实现,并深度适配 [Dify](https://dify.ai)。
|
||||||
- 🤖 多平台支持:目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
|
- 🤖 多平台支持:目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
|
||||||
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
|
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
|
||||||
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
|
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
|
||||||
@@ -107,6 +107,7 @@ docker compose up -d
|
|||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
| [xAI](https://x.ai/) | ✅ | |
|
||||||
| [智谱AI](https://open.bigmodel.cn/) | ✅ | |
|
| [智谱AI](https://open.bigmodel.cn/) | ✅ | |
|
||||||
|
| [胜算云](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | 全球大模型都可调用(友情推荐) |
|
||||||
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 资源平台 |
|
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 资源平台 |
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 |
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 |
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
|
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Click the Star and Watch button in the upper right corner of the repository to g
|
|||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, and multi-modal capabilities. Built-in RAG (knowledge base) implementation, and deeply integrates with [Dify](https://dify.ai).
|
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, multi-modal, and streaming output capabilities. Built-in RAG (knowledge base) implementation, and deeply integrates with [Dify](https://dify.ai).
|
||||||
- 🤖 Multi-platform Support: Currently supports QQ, QQ Channel, WeCom, personal WeChat, Lark, DingTalk, Discord, Telegram, etc.
|
- 🤖 Multi-platform Support: Currently supports QQ, QQ Channel, WeCom, personal WeChat, Lark, DingTalk, Discord, Telegram, etc.
|
||||||
- 🛠️ High Stability, Feature-rich: Native access control, rate limiting, sensitive word filtering, etc. mechanisms; Easy to use, supports multiple deployment methods. Supports multiple pipeline configurations, different bots can be used for different scenarios.
|
- 🛠️ High Stability, Feature-rich: Native access control, rate limiting, sensitive word filtering, etc. mechanisms; Easy to use, supports multiple deployment methods. Supports multiple pipeline configurations, different bots can be used for different scenarios.
|
||||||
- 🧩 Plugin Extension, Active Community: Support event-driven, component extension, etc. plugin mechanisms; Integrate Anthropic [MCP protocol](https://modelcontextprotocol.io/); Currently has hundreds of plugins.
|
- 🧩 Plugin Extension, Active Community: Support event-driven, component extension, etc. plugin mechanisms; Integrate Anthropic [MCP protocol](https://modelcontextprotocol.io/); Currently has hundreds of plugins.
|
||||||
@@ -103,6 +103,7 @@ Or visit the demo environment: https://demo.langbot.dev/
|
|||||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | LLM and GPU resource platform |
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | LLM and GPU resource platform |
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOps platform |
|
| [Dify](https://dify.ai) | ✅ | LLMOps platform |
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform |
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLM and GPU resource platform |
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLM gateway(MaaS) |
|
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLM gateway(MaaS) |
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||||
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
|
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
|
|||||||
|
|
||||||
## ✨ 機能
|
## ✨ 機能
|
||||||
|
|
||||||
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル機能をサポート、RAG(知識ベース)を組み込み、[Dify](https://dify.ai) と深く統合。
|
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル、ストリーミング出力機能をサポート、RAG(知識ベース)を組み込み、[Dify](https://dify.ai) と深く統合。
|
||||||
- 🤖 多プラットフォーム対応: 現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
|
- 🤖 多プラットフォーム対応: 現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
|
||||||
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。複数のパイプライン設定をサポートし、異なるボットを異なる用途に使用できます。
|
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。複数のパイプライン設定をサポートし、異なるボットを異なる用途に使用できます。
|
||||||
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
|
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
|
||||||
@@ -102,6 +102,7 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
|
|||||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
||||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLMとGPUリソースプラットフォーム |
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLMゲートウェイ(MaaS) |
|
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLMゲートウェイ(MaaS) |
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム |
|
| [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム |
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ docker compose up -d
|
|||||||
|
|
||||||
## ✨ 特性
|
## ✨ 特性
|
||||||
|
|
||||||
- 💬 大模型對話、Agent:支援多種大模型,適配群聊和私聊;具有多輪對話、工具調用、多模態能力,自帶 RAG(知識庫)實現,並深度適配 [Dify](https://dify.ai)。
|
- 💬 大模型對話、Agent:支援多種大模型,適配群聊和私聊;具有多輪對話、工具調用、多模態、流式輸出能力,自帶 RAG(知識庫)實現,並深度適配 [Dify](https://dify.ai)。
|
||||||
- 🤖 多平台支援:目前支援 QQ、QQ頻道、企業微信、個人微信、飛書、Discord、Telegram 等平台。
|
- 🤖 多平台支援:目前支援 QQ、QQ頻道、企業微信、個人微信、飛書、Discord、Telegram 等平台。
|
||||||
- 🛠️ 高穩定性、功能完備:原生支援訪問控制、限速、敏感詞過濾等機制;配置簡單,支援多種部署方式。支援多流水線配置,不同機器人用於不同應用場景。
|
- 🛠️ 高穩定性、功能完備:原生支援訪問控制、限速、敏感詞過濾等機制;配置簡單,支援多種部署方式。支援多流水線配置,不同機器人用於不同應用場景。
|
||||||
- 🧩 外掛擴展、活躍社群:支援事件驅動、組件擴展等外掛機制;適配 Anthropic [MCP 協議](https://modelcontextprotocol.io/);目前已有數百個外掛。
|
- 🧩 外掛擴展、活躍社群:支援事件驅動、組件擴展等外掛機制;適配 Anthropic [MCP 協議](https://modelcontextprotocol.io/);目前已有數百個外掛。
|
||||||
@@ -102,6 +102,7 @@ docker compose up -d
|
|||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
| [xAI](https://x.ai/) | ✅ | |
|
||||||
| [智譜AI](https://open.bigmodel.cn/) | ✅ | |
|
| [智譜AI](https://open.bigmodel.cn/) | ✅ | |
|
||||||
|
| [勝算雲](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | 大模型和 GPU 資源平台 |
|
||||||
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 資源平台 |
|
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 資源平台 |
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 資源平台 |
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 資源平台 |
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
|
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ class DBMigrateV3Config(migration.DBMigration):
|
|||||||
self.ap.instance_config.data['api']['port'] = self.ap.system_cfg.data['http-api']['port']
|
self.ap.instance_config.data['api']['port'] = self.ap.system_cfg.data['http-api']['port']
|
||||||
self.ap.instance_config.data['command'] = {
|
self.ap.instance_config.data['command'] = {
|
||||||
'prefix': self.ap.command_cfg.data['command-prefix'],
|
'prefix': self.ap.command_cfg.data['command-prefix'],
|
||||||
|
'enable': self.ap.command_cfg.data['command-enable'],
|
||||||
'privilege': self.ap.command_cfg.data['privilege'],
|
'privilege': self.ap.command_cfg.data['privilege'],
|
||||||
}
|
}
|
||||||
self.ap.instance_config.data['concurrency']['pipeline'] = self.ap.system_cfg.data['pipeline-concurrency']
|
self.ap.instance_config.data['concurrency']['pipeline'] = self.ap.system_cfg.data['pipeline-concurrency']
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class DBMigratePipelineRemoveCotConfig(migration.DBMigration):
|
|||||||
config = serialized_pipeline['config']
|
config = serialized_pipeline['config']
|
||||||
|
|
||||||
if 'remove-think' not in config['output']['misc']:
|
if 'remove-think' not in config['output']['misc']:
|
||||||
config['output']['misc']['remove-think'] = True
|
config['output']['misc']['remove-think'] = False
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
await self.ap.persistence_mgr.execute_async(
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ class BanSessionCheckStage(stage.PipelineStage):
|
|||||||
if sess == f'{query.launcher_type.value}_{query.launcher_id}':
|
if sess == f'{query.launcher_type.value}_{query.launcher_id}':
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
|
# 使用 *_id 来表示加白/拉黑某用户的私聊和群聊场景
|
||||||
|
if sess.startswith('*_') and (sess[2:] == query.launcher_id or sess[2:] == query.sender_id):
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
ctn = False
|
ctn = False
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class ChatMessageHandler(handler.MessageHandler):
|
|||||||
query=query,
|
query=query,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
is_create_card = False # 判断下是否需要创建流式卡片
|
||||||
if event_ctx.is_prevented_default():
|
if event_ctx.is_prevented_default():
|
||||||
if event_ctx.event.reply is not None:
|
if event_ctx.event.reply is not None:
|
||||||
mc = platform_message.MessageChain(event_ctx.event.reply)
|
mc = platform_message.MessageChain(event_ctx.event.reply)
|
||||||
@@ -72,14 +72,17 @@ class ChatMessageHandler(handler.MessageHandler):
|
|||||||
raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}')
|
raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}')
|
||||||
if is_stream:
|
if is_stream:
|
||||||
resp_message_id = uuid.uuid4()
|
resp_message_id = uuid.uuid4()
|
||||||
await query.adapter.create_message_card(str(resp_message_id), query.message_event)
|
|
||||||
async for result in runner.run(query):
|
async for result in runner.run(query):
|
||||||
result.resp_message_id = str(resp_message_id)
|
result.resp_message_id = str(resp_message_id)
|
||||||
if query.resp_messages:
|
if query.resp_messages:
|
||||||
query.resp_messages.pop()
|
query.resp_messages.pop()
|
||||||
if query.resp_message_chain:
|
if query.resp_message_chain:
|
||||||
query.resp_message_chain.pop()
|
query.resp_message_chain.pop()
|
||||||
|
# 此时连接外部 AI 服务正常,创建卡片
|
||||||
|
if not is_create_card: # 只有不是第一次才创建卡片
|
||||||
|
await query.adapter.create_message_card(str(resp_message_id), query.message_event)
|
||||||
|
is_create_card = True
|
||||||
query.resp_messages.append(result)
|
query.resp_messages.append(result)
|
||||||
self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}')
|
self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}')
|
||||||
|
|
||||||
|
|||||||
@@ -42,12 +42,14 @@ class Processor(stage.PipelineStage):
|
|||||||
|
|
||||||
async def generator():
|
async def generator():
|
||||||
cmd_prefix = self.ap.instance_config.data['command']['prefix']
|
cmd_prefix = self.ap.instance_config.data['command']['prefix']
|
||||||
|
cmd_enable = self.ap.instance_config.data['command'].get('enable', True)
|
||||||
|
|
||||||
if any(message_text.startswith(prefix) for prefix in cmd_prefix):
|
if cmd_enable and any(message_text.startswith(prefix) for prefix in cmd_prefix):
|
||||||
async for result in self.cmd_handler.handle(query):
|
handler_to_use = self.cmd_handler
|
||||||
yield result
|
|
||||||
else:
|
else:
|
||||||
async for result in self.chat_handler.handle(query):
|
handler_to_use = self.chat_handler
|
||||||
yield result
|
|
||||||
|
async for result in handler_to_use.handle(query):
|
||||||
|
yield result
|
||||||
|
|
||||||
return generator()
|
return generator()
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
|
|||||||
await process_message_data(msg_data, reply_list)
|
await process_message_data(msg_data, reply_list)
|
||||||
|
|
||||||
reply_msg = platform_message.Quote(
|
reply_msg = platform_message.Quote(
|
||||||
message_id=msg.data['id'], sender_id=msg_datas['user_id'], origin=reply_list
|
message_id=msg.data['id'], sender_id=msg_datas['sender']['user_id'], origin=reply_list
|
||||||
)
|
)
|
||||||
yiri_msg_list.append(reply_msg)
|
yiri_msg_list.append(reply_msg)
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ class DingTalkMessageConverter(adapter.MessageConverter):
|
|||||||
at = True
|
at = True
|
||||||
if type(msg) is platform_message.Plain:
|
if type(msg) is platform_message.Plain:
|
||||||
content += msg.text
|
content += msg.text
|
||||||
|
if type(msg) is platform_message.Forward:
|
||||||
|
for node in msg.node_list:
|
||||||
|
content += (await DingTalkMessageConverter.yiri2target(node.message_chain))[0]
|
||||||
return content, at
|
return content, at
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -61,7 +64,7 @@ class DingTalkEventConverter(adapter.EventConverter):
|
|||||||
if event.conversation == 'FriendMessage':
|
if event.conversation == 'FriendMessage':
|
||||||
return platform_events.FriendMessage(
|
return platform_events.FriendMessage(
|
||||||
sender=platform_entities.Friend(
|
sender=platform_entities.Friend(
|
||||||
id=event.incoming_message.sender_id,
|
id=event.incoming_message.sender_staff_id,
|
||||||
nickname=event.incoming_message.sender_nick,
|
nickname=event.incoming_message.sender_nick,
|
||||||
remark='',
|
remark='',
|
||||||
),
|
),
|
||||||
@@ -71,7 +74,7 @@ class DingTalkEventConverter(adapter.EventConverter):
|
|||||||
)
|
)
|
||||||
elif event.conversation == 'GroupMessage':
|
elif event.conversation == 'GroupMessage':
|
||||||
sender = platform_entities.GroupMember(
|
sender = platform_entities.GroupMember(
|
||||||
id=event.incoming_message.sender_id,
|
id=event.incoming_message.sender_staff_id,
|
||||||
member_name=event.incoming_message.sender_nick,
|
member_name=event.incoming_message.sender_nick,
|
||||||
permission='MEMBER',
|
permission='MEMBER',
|
||||||
group=platform_entities.Group(
|
group=platform_entities.Group(
|
||||||
@@ -166,8 +169,11 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
content, at = await DingTalkMessageConverter.yiri2target(message)
|
content, at = await DingTalkMessageConverter.yiri2target(message)
|
||||||
|
|
||||||
card_instance, card_instance_id = self.card_instance_id_dict[message_id]
|
card_instance, card_instance_id = self.card_instance_id_dict[message_id]
|
||||||
|
if not content and bot_message.content:
|
||||||
|
content = bot_message.content # 兼容直接传入content的情况
|
||||||
# print(card_instance_id)
|
# print(card_instance_id)
|
||||||
await self.bot.send_card_message(card_instance, card_instance_id, content, is_final)
|
if content:
|
||||||
|
await self.bot.send_card_message(card_instance, card_instance_id, content, is_final)
|
||||||
if is_final and bot_message.tool_calls is None:
|
if is_final and bot_message.tool_calls is None:
|
||||||
# self.seq = 1 # 消息回复结束之后重置seq
|
# self.seq = 1 # 消息回复结束之后重置seq
|
||||||
self.card_instance_id_dict.pop(message_id) # 消息回复结束之后删除卡片实例id
|
self.card_instance_id_dict.pop(message_id) # 消息回复结束之后删除卡片实例id
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ import typing
|
|||||||
|
|
||||||
from . import chatcmpl
|
from . import chatcmpl
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from .. import errors, requester
|
||||||
|
from ....core import entities as core_entities
|
||||||
|
from ... import entities as llm_entities
|
||||||
|
from ...tools import entities as tools_entities
|
||||||
|
|
||||||
|
|
||||||
class GeminiChatCompletions(chatcmpl.OpenAIChatCompletions):
|
class GeminiChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||||
"""Google Gemini API 请求器"""
|
"""Google Gemini API 请求器"""
|
||||||
@@ -12,3 +19,127 @@ class GeminiChatCompletions(chatcmpl.OpenAIChatCompletions):
|
|||||||
'base_url': 'https://generativelanguage.googleapis.com/v1beta/openai',
|
'base_url': 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||||
'timeout': 120,
|
'timeout': 120,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _closure_stream(
|
||||||
|
self,
|
||||||
|
query: core_entities.Query,
|
||||||
|
req_messages: list[dict],
|
||||||
|
use_model: requester.RuntimeLLMModel,
|
||||||
|
use_funcs: list[tools_entities.LLMFunction] = None,
|
||||||
|
extra_args: dict[str, typing.Any] = {},
|
||||||
|
remove_think: bool = False,
|
||||||
|
) -> llm_entities.MessageChunk:
|
||||||
|
self.client.api_key = use_model.token_mgr.get_token()
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['model'] = use_model.model_entity.name
|
||||||
|
|
||||||
|
if use_funcs:
|
||||||
|
tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs)
|
||||||
|
if tools:
|
||||||
|
args['tools'] = tools
|
||||||
|
|
||||||
|
# 设置此次请求中的messages
|
||||||
|
messages = req_messages.copy()
|
||||||
|
|
||||||
|
# 检查vision
|
||||||
|
for msg in messages:
|
||||||
|
if 'content' in msg and isinstance(msg['content'], list):
|
||||||
|
for me in msg['content']:
|
||||||
|
if me['type'] == 'image_base64':
|
||||||
|
me['image_url'] = {'url': me['image_base64']}
|
||||||
|
me['type'] = 'image_url'
|
||||||
|
del me['image_base64']
|
||||||
|
|
||||||
|
args['messages'] = messages
|
||||||
|
args['stream'] = True
|
||||||
|
|
||||||
|
# 流式处理状态
|
||||||
|
tool_calls_map: dict[str, llm_entities.ToolCall] = {}
|
||||||
|
chunk_idx = 0
|
||||||
|
thinking_started = False
|
||||||
|
thinking_ended = False
|
||||||
|
role = 'assistant' # 默认角色
|
||||||
|
tool_id = ""
|
||||||
|
tool_name = ''
|
||||||
|
# accumulated_reasoning = '' # 仅用于判断何时结束思维链
|
||||||
|
|
||||||
|
async for chunk in self._req_stream(args, extra_body=extra_args):
|
||||||
|
# 解析 chunk 数据
|
||||||
|
|
||||||
|
if hasattr(chunk, 'choices') and chunk.choices:
|
||||||
|
choice = chunk.choices[0]
|
||||||
|
delta = choice.delta.model_dump() if hasattr(choice, 'delta') else {}
|
||||||
|
|
||||||
|
finish_reason = getattr(choice, 'finish_reason', None)
|
||||||
|
else:
|
||||||
|
delta = {}
|
||||||
|
finish_reason = None
|
||||||
|
# 从第一个 chunk 获取 role,后续使用这个 role
|
||||||
|
if 'role' in delta and delta['role']:
|
||||||
|
role = delta['role']
|
||||||
|
|
||||||
|
# 获取增量内容
|
||||||
|
delta_content = delta.get('content', '')
|
||||||
|
reasoning_content = delta.get('reasoning_content', '')
|
||||||
|
|
||||||
|
# 处理 reasoning_content
|
||||||
|
if reasoning_content:
|
||||||
|
# accumulated_reasoning += reasoning_content
|
||||||
|
# 如果设置了 remove_think,跳过 reasoning_content
|
||||||
|
if remove_think:
|
||||||
|
chunk_idx += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 第一次出现 reasoning_content,添加 <think> 开始标签
|
||||||
|
if not thinking_started:
|
||||||
|
thinking_started = True
|
||||||
|
delta_content = '<think>\n' + reasoning_content
|
||||||
|
else:
|
||||||
|
# 继续输出 reasoning_content
|
||||||
|
delta_content = reasoning_content
|
||||||
|
elif thinking_started and not thinking_ended and delta_content:
|
||||||
|
# reasoning_content 结束,normal content 开始,添加 </think> 结束标签
|
||||||
|
thinking_ended = True
|
||||||
|
delta_content = '\n</think>\n' + delta_content
|
||||||
|
|
||||||
|
# 处理 content 中已有的 <think> 标签(如果需要移除)
|
||||||
|
# if delta_content and remove_think and '<think>' in delta_content:
|
||||||
|
# import re
|
||||||
|
#
|
||||||
|
# # 移除 <think> 标签及其内容
|
||||||
|
# delta_content = re.sub(r'<think>.*?</think>', '', delta_content, flags=re.DOTALL)
|
||||||
|
|
||||||
|
# 处理工具调用增量
|
||||||
|
# delta_tool_calls = None
|
||||||
|
if delta.get('tool_calls'):
|
||||||
|
for tool_call in delta['tool_calls']:
|
||||||
|
if tool_call['id'] == '' and tool_id == '':
|
||||||
|
tool_id = str(uuid.uuid4())
|
||||||
|
if tool_call['function']['name']:
|
||||||
|
tool_name = tool_call['function']['name']
|
||||||
|
tool_call['id'] = tool_id
|
||||||
|
tool_call['function']['name'] = tool_name
|
||||||
|
if tool_call['type'] is None:
|
||||||
|
tool_call['type'] = 'function'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 跳过空的第一个 chunk(只有 role 没有内容)
|
||||||
|
if chunk_idx == 0 and not delta_content and not reasoning_content and not delta.get('tool_calls'):
|
||||||
|
chunk_idx += 1
|
||||||
|
continue
|
||||||
|
# 构建 MessageChunk - 只包含增量内容
|
||||||
|
chunk_data = {
|
||||||
|
'role': role,
|
||||||
|
'content': delta_content if delta_content else None,
|
||||||
|
'tool_calls': delta.get('tool_calls'),
|
||||||
|
'is_final': bool(finish_reason),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 移除 None 值
|
||||||
|
chunk_data = {k: v for k, v in chunk_data.items() if v is not None}
|
||||||
|
|
||||||
|
yield llm_entities.MessageChunk(**chunk_data)
|
||||||
|
chunk_idx += 1
|
||||||
@@ -139,8 +139,8 @@ class OllamaChatCompletions(requester.ProviderAPIRequester):
|
|||||||
input_text: list[str],
|
input_text: list[str],
|
||||||
extra_args: dict[str, typing.Any] = {},
|
extra_args: dict[str, typing.Any] = {},
|
||||||
) -> list[list[float]]:
|
) -> list[list[float]]:
|
||||||
return await self.client.embed(
|
return (await self.client.embed(
|
||||||
model=model.model_entity.name,
|
model=model.model_entity.name,
|
||||||
input=input_text,
|
input=input_text,
|
||||||
**extra_args,
|
**extra_args,
|
||||||
)
|
)).embeddings
|
||||||
|
|||||||
32
pkg/provider/modelmgr/requesters/shengsuanyun.py
Normal file
32
pkg/provider/modelmgr/requesters/shengsuanyun.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import openai
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from . import chatcmpl
|
||||||
|
import openai.types.chat.chat_completion as chat_completion
|
||||||
|
|
||||||
|
|
||||||
|
class ShengSuanYunChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||||
|
"""胜算云(ModelSpot.AI) ChatCompletion API 请求器"""
|
||||||
|
|
||||||
|
client: openai.AsyncClient
|
||||||
|
|
||||||
|
default_config: dict[str, typing.Any] = {
|
||||||
|
'base_url': 'https://router.shengsuanyun.com/api/v1',
|
||||||
|
'timeout': 120,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _req(
|
||||||
|
self,
|
||||||
|
args: dict,
|
||||||
|
extra_body: dict = {},
|
||||||
|
) -> chat_completion.ChatCompletion:
|
||||||
|
return await self.client.chat.completions.create(
|
||||||
|
**args,
|
||||||
|
extra_body=extra_body,
|
||||||
|
extra_headers={
|
||||||
|
'HTTP-Referer': 'https://langbot.app',
|
||||||
|
'X-Title': 'LangBot',
|
||||||
|
},
|
||||||
|
)
|
||||||
1
pkg/provider/modelmgr/requesters/shengsuanyun.svg
Normal file
1
pkg/provider/modelmgr/requesters/shengsuanyun.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.4 KiB |
38
pkg/provider/modelmgr/requesters/shengsuanyun.yaml
Normal file
38
pkg/provider/modelmgr/requesters/shengsuanyun.yaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: LLMAPIRequester
|
||||||
|
metadata:
|
||||||
|
name: shengsuanyun-chat-completions
|
||||||
|
label:
|
||||||
|
en_US: ShengSuanYun
|
||||||
|
zh_Hans: 胜算云
|
||||||
|
icon: shengsuanyun.svg
|
||||||
|
spec:
|
||||||
|
config:
|
||||||
|
- name: base_url
|
||||||
|
label:
|
||||||
|
en_US: Base URL
|
||||||
|
zh_Hans: 基础 URL
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
default: "https://router.shengsuanyun.com/api/v1"
|
||||||
|
- name: args
|
||||||
|
label:
|
||||||
|
en_US: Args
|
||||||
|
zh_Hans: 附加参数
|
||||||
|
type: object
|
||||||
|
required: true
|
||||||
|
default: {}
|
||||||
|
- name: timeout
|
||||||
|
label:
|
||||||
|
en_US: Timeout
|
||||||
|
zh_Hans: 超时时间
|
||||||
|
type: int
|
||||||
|
required: true
|
||||||
|
default: 120
|
||||||
|
support_type:
|
||||||
|
- llm
|
||||||
|
- text-embedding
|
||||||
|
execution:
|
||||||
|
python:
|
||||||
|
path: ./shengsuanyun.py
|
||||||
|
attr: ShengSuanYunChatCompletions
|
||||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|||||||
import typing
|
import typing
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import re
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
|
||||||
@@ -38,33 +37,9 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
base_url=self.pipeline_config['ai']['dify-service-api']['base-url'],
|
base_url=self.pipeline_config['ai']['dify-service-api']['base-url'],
|
||||||
)
|
)
|
||||||
|
|
||||||
def _try_convert_thinking(self, resp_text: str) -> str:
|
|
||||||
"""尝试转换 Dify 的思考提示"""
|
|
||||||
if not resp_text.startswith(
|
|
||||||
'<details style="color:gray;background-color: #f8f8f8;padding: 8px;border-radius: 4px;" open> <summary> Thinking... </summary>'
|
|
||||||
):
|
|
||||||
return resp_text
|
|
||||||
|
|
||||||
if self.pipeline_config['ai']['dify-service-api']['thinking-convert'] == 'original':
|
|
||||||
return resp_text
|
|
||||||
|
|
||||||
if self.pipeline_config['ai']['dify-service-api']['thinking-convert'] == 'remove':
|
|
||||||
return re.sub(
|
|
||||||
r'<details style="color:gray;background-color: #f8f8f8;padding: 8px;border-radius: 4px;" open> <summary> Thinking... </summary>.*?</details>',
|
|
||||||
'',
|
|
||||||
resp_text,
|
|
||||||
flags=re.DOTALL,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.pipeline_config['ai']['dify-service-api']['thinking-convert'] == 'plain':
|
|
||||||
pattern = r'<details style="color:gray;background-color: #f8f8f8;padding: 8px;border-radius: 4px;" open> <summary> Thinking... </summary>(.*?)</details>'
|
|
||||||
thinking_text = re.search(pattern, resp_text, flags=re.DOTALL)
|
|
||||||
content_text = re.sub(pattern, '', resp_text, flags=re.DOTALL)
|
|
||||||
return f'<think>{thinking_text.group(1)}</think>\n{content_text}'
|
|
||||||
|
|
||||||
def _process_thinking_content(
|
def _process_thinking_content(
|
||||||
self,
|
self,
|
||||||
content: str,
|
content: str,
|
||||||
) -> tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
"""处理思维链内容
|
"""处理思维链内容
|
||||||
|
|
||||||
@@ -354,8 +329,9 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
|
|
||||||
yield msg
|
yield msg
|
||||||
|
|
||||||
|
async def _chat_messages_chunk(
|
||||||
async def _chat_messages_chunk(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.MessageChunk, None]:
|
self, query: core_entities.Query
|
||||||
|
) -> typing.AsyncGenerator[llm_entities.MessageChunk, None]:
|
||||||
"""调用聊天助手"""
|
"""调用聊天助手"""
|
||||||
cov_id = query.session.using_conversation.uuid or ''
|
cov_id = query.session.using_conversation.uuid or ''
|
||||||
query.variables['conversation_id'] = cov_id
|
query.variables['conversation_id'] = cov_id
|
||||||
@@ -371,8 +347,6 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
for image_id in image_ids
|
for image_id in image_ids
|
||||||
]
|
]
|
||||||
|
|
||||||
mode = 'basic' # 标记是基础编排还是工作流编排
|
|
||||||
|
|
||||||
basic_mode_pending_chunk = ''
|
basic_mode_pending_chunk = ''
|
||||||
|
|
||||||
inputs = {}
|
inputs = {}
|
||||||
@@ -411,6 +385,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
continue
|
continue
|
||||||
if '</think>' in chunk['answer'] and not think_end:
|
if '</think>' in chunk['answer'] and not think_end:
|
||||||
import re
|
import re
|
||||||
|
|
||||||
content = re.sub(r'^\n</think>', '', chunk['answer'])
|
content = re.sub(r'^\n</think>', '', chunk['answer'])
|
||||||
basic_mode_pending_chunk += content
|
basic_mode_pending_chunk += content
|
||||||
think_end = True
|
think_end = True
|
||||||
@@ -433,13 +408,11 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
is_final=is_final,
|
is_final=is_final,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if chunk is None:
|
if chunk is None:
|
||||||
raise errors.DifyAPIError('Dify API 没有返回任何响应,请检查网络连接和API配置')
|
raise errors.DifyAPIError('Dify API 没有返回任何响应,请检查网络连接和API配置')
|
||||||
|
|
||||||
query.session.using_conversation.uuid = chunk['conversation_id']
|
query.session.using_conversation.uuid = chunk['conversation_id']
|
||||||
|
|
||||||
|
|
||||||
async def _agent_chat_messages_chunk(
|
async def _agent_chat_messages_chunk(
|
||||||
self, query: core_entities.Query
|
self, query: core_entities.Query
|
||||||
) -> typing.AsyncGenerator[llm_entities.MessageChunk, None]:
|
) -> typing.AsyncGenerator[llm_entities.MessageChunk, None]:
|
||||||
@@ -496,10 +469,11 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
continue
|
continue
|
||||||
if '</think>' in chunk['answer'] and not think_end:
|
if '</think>' in chunk['answer'] and not think_end:
|
||||||
import re
|
import re
|
||||||
|
|
||||||
content = re.sub(r'^\n</think>', '', chunk['answer'])
|
content = re.sub(r'^\n</think>', '', chunk['answer'])
|
||||||
pending_agent_message += content
|
pending_agent_message += content
|
||||||
think_end = True
|
think_end = True
|
||||||
elif think_end:
|
elif think_end or not think_start:
|
||||||
pending_agent_message += chunk['answer']
|
pending_agent_message += chunk['answer']
|
||||||
if think_start:
|
if think_start:
|
||||||
continue
|
continue
|
||||||
@@ -509,7 +483,6 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
elif chunk['event'] == 'message_end':
|
elif chunk['event'] == 'message_end':
|
||||||
is_final = True
|
is_final = True
|
||||||
else:
|
else:
|
||||||
|
|
||||||
if chunk['event'] == 'agent_thought':
|
if chunk['event'] == 'agent_thought':
|
||||||
if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过
|
if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过
|
||||||
continue
|
continue
|
||||||
@@ -543,7 +516,6 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
role='assistant',
|
role='assistant',
|
||||||
content=[llm_entities.ContentElement.from_image_url(image_url)],
|
content=[llm_entities.ContentElement.from_image_url(image_url)],
|
||||||
is_final=is_final,
|
is_final=is_final,
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if chunk['event'] == 'error':
|
if chunk['event'] == 'error':
|
||||||
@@ -560,7 +532,9 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
|
|
||||||
query.session.using_conversation.uuid = chunk['conversation_id']
|
query.session.using_conversation.uuid = chunk['conversation_id']
|
||||||
|
|
||||||
async def _workflow_messages_chunk(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.MessageChunk, None]:
|
async def _workflow_messages_chunk(
|
||||||
|
self, query: core_entities.Query
|
||||||
|
) -> typing.AsyncGenerator[llm_entities.MessageChunk, None]:
|
||||||
"""调用工作流"""
|
"""调用工作流"""
|
||||||
|
|
||||||
if not query.session.using_conversation.uuid:
|
if not query.session.using_conversation.uuid:
|
||||||
@@ -618,6 +592,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
continue
|
continue
|
||||||
if '</think>' in chunk['data']['text'] and not think_end:
|
if '</think>' in chunk['data']['text'] and not think_end:
|
||||||
import re
|
import re
|
||||||
|
|
||||||
content = re.sub(r'^\n</think>', '', chunk['data']['text'])
|
content = re.sub(r'^\n</think>', '', chunk['data']['text'])
|
||||||
workflow_contents += content
|
workflow_contents += content
|
||||||
think_end = True
|
think_end = True
|
||||||
@@ -650,7 +625,6 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
|
|
||||||
yield msg
|
yield msg
|
||||||
|
|
||||||
|
|
||||||
if messsage_idx % 8 == 0 or is_final:
|
if messsage_idx % 8 == 0 or is_final:
|
||||||
yield llm_entities.MessageChunk(
|
yield llm_entities.MessageChunk(
|
||||||
role='assistant',
|
role='assistant',
|
||||||
@@ -694,4 +668,4 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
else:
|
else:
|
||||||
raise errors.DifyAPIError(
|
raise errors.DifyAPIError(
|
||||||
f'不支持的 Dify 应用类型: {self.pipeline_config["ai"]["dify-service-api"]["app-type"]}'
|
f'不支持的 Dify 应用类型: {self.pipeline_config["ai"]["dify-service-api"]["app-type"]}'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
|
import zipfile
|
||||||
|
import io
|
||||||
from .services import parser, chunker
|
from .services import parser, chunker
|
||||||
from pkg.core import app
|
from pkg.core import app
|
||||||
from pkg.rag.knowledge.services.embedder import Embedder
|
from pkg.rag.knowledge.services.embedder import Embedder
|
||||||
@@ -89,16 +91,23 @@ class RuntimeKnowledgeBase:
|
|||||||
)
|
)
|
||||||
|
|
||||||
raise
|
raise
|
||||||
|
finally:
|
||||||
|
# delete file from storage
|
||||||
|
await self.ap.storage_mgr.storage_provider.delete(file.file_name)
|
||||||
|
|
||||||
async def store_file(self, file_id: str) -> str:
|
async def store_file(self, file_id: str) -> str:
|
||||||
# pre checking
|
# pre checking
|
||||||
if not await self.ap.storage_mgr.storage_provider.exists(file_id):
|
if not await self.ap.storage_mgr.storage_provider.exists(file_id):
|
||||||
raise Exception(f'File {file_id} not found')
|
raise Exception(f'File {file_id} not found')
|
||||||
|
|
||||||
|
file_name = file_id
|
||||||
|
extension = file_name.split('.')[-1].lower()
|
||||||
|
|
||||||
|
if extension == 'zip':
|
||||||
|
return await self._store_zip_file(file_id)
|
||||||
|
|
||||||
file_uuid = str(uuid.uuid4())
|
file_uuid = str(uuid.uuid4())
|
||||||
kb_id = self.knowledge_base_entity.uuid
|
kb_id = self.knowledge_base_entity.uuid
|
||||||
file_name = file_id
|
|
||||||
extension = file_name.split('.')[-1]
|
|
||||||
|
|
||||||
file_obj_data = {
|
file_obj_data = {
|
||||||
'uuid': file_uuid,
|
'uuid': file_uuid,
|
||||||
@@ -123,6 +132,61 @@ class RuntimeKnowledgeBase:
|
|||||||
)
|
)
|
||||||
return wrapper.id
|
return wrapper.id
|
||||||
|
|
||||||
|
async def _store_zip_file(self, zip_file_id: str) -> str:
|
||||||
|
"""Handle ZIP file by extracting each document and storing them separately."""
|
||||||
|
self.ap.logger.info(f'Processing ZIP file: {zip_file_id}')
|
||||||
|
|
||||||
|
zip_bytes = await self.ap.storage_mgr.storage_provider.load(zip_file_id)
|
||||||
|
|
||||||
|
supported_extensions = {'txt', 'pdf', 'docx', 'md', 'html'}
|
||||||
|
stored_file_tasks = []
|
||||||
|
|
||||||
|
# use utf-8 encoding
|
||||||
|
with zipfile.ZipFile(io.BytesIO(zip_bytes), 'r', metadata_encoding='utf-8') as zip_ref:
|
||||||
|
for file_info in zip_ref.filelist:
|
||||||
|
# skip directories and hidden files
|
||||||
|
if file_info.is_dir() or file_info.filename.startswith('.'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
file_extension = file_info.filename.split('.')[-1].lower()
|
||||||
|
if file_extension not in supported_extensions:
|
||||||
|
self.ap.logger.debug(f'Skipping unsupported file in ZIP: {file_info.filename}')
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_content = zip_ref.read(file_info.filename)
|
||||||
|
|
||||||
|
base_name = file_info.filename.replace('/', '_').replace('\\', '_')
|
||||||
|
extension = base_name.split('.')[-1]
|
||||||
|
file_name = base_name.split('.')[0]
|
||||||
|
|
||||||
|
if file_name.startswith('__MACOSX'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
extracted_file_id = file_name + '_' + str(uuid.uuid4())[:8] + '.' + extension
|
||||||
|
# save file to storage
|
||||||
|
|
||||||
|
await self.ap.storage_mgr.storage_provider.save(extracted_file_id, file_content)
|
||||||
|
|
||||||
|
task_id = await self.store_file(extracted_file_id)
|
||||||
|
stored_file_tasks.append(task_id)
|
||||||
|
|
||||||
|
self.ap.logger.info(
|
||||||
|
f'Extracted and stored file from ZIP: {file_info.filename} -> {extracted_file_id}'
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.ap.logger.warning(f'Failed to extract file {file_info.filename} from ZIP: {e}')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not stored_file_tasks:
|
||||||
|
raise Exception('No supported files found in ZIP archive')
|
||||||
|
|
||||||
|
self.ap.logger.info(f'Successfully processed ZIP file {zip_file_id}, extracted {len(stored_file_tasks)} files')
|
||||||
|
await self.ap.storage_mgr.storage_provider.delete(zip_file_id)
|
||||||
|
|
||||||
|
return stored_file_tasks[0] if stored_file_tasks else ''
|
||||||
|
|
||||||
async def retrieve(self, query: str, top_k: int) -> list[retriever_entities.RetrieveResultEntry]:
|
async def retrieve(self, query: str, top_k: int) -> list[retriever_entities.RetrieveResultEntry]:
|
||||||
embedding_model = await self.ap.model_mgr.get_embedding_model_by_uuid(
|
embedding_model = await self.ap.model_mgr.get_embedding_model_by_uuid(
|
||||||
self.knowledge_base_entity.embedding_model_uuid
|
self.knowledge_base_entity.embedding_model_uuid
|
||||||
|
|||||||
@@ -45,17 +45,23 @@ class AnnouncementManager:
|
|||||||
|
|
||||||
async def fetch_all(self) -> list[Announcement]:
|
async def fetch_all(self) -> list[Announcement]:
|
||||||
"""获取所有公告"""
|
"""获取所有公告"""
|
||||||
resp = requests.get(
|
try:
|
||||||
url='https://api.github.com/repos/langbot-app/LangBot/contents/res/announcement.json',
|
resp = requests.get(
|
||||||
proxies=self.ap.proxy_mgr.get_forward_proxies(),
|
url='https://api.github.com/repos/langbot-app/LangBot/contents/res/announcement.json',
|
||||||
timeout=5,
|
proxies=self.ap.proxy_mgr.get_forward_proxies(),
|
||||||
)
|
timeout=5,
|
||||||
obj_json = resp.json()
|
)
|
||||||
b64_content = obj_json['content']
|
resp.raise_for_status() # 检查请求是否成功
|
||||||
# 解码
|
obj_json = resp.json()
|
||||||
content = base64.b64decode(b64_content).decode('utf-8')
|
b64_content = obj_json['content']
|
||||||
|
# 解码
|
||||||
|
content = base64.b64decode(b64_content).decode('utf-8')
|
||||||
|
|
||||||
return [Announcement(**item) for item in json.loads(content)]
|
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]:
|
async def fetch_saved(self) -> list[Announcement]:
|
||||||
if not os.path.exists('data/labels/announcement_saved.json'):
|
if not os.path.exists('data/labels/announcement_saved.json'):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
semantic_version = 'v4.2.0'
|
semantic_version = 'v4.2.2'
|
||||||
|
|
||||||
required_database_version = 5
|
required_database_version = 5
|
||||||
"""Tag the version of the database schema, used to check if the database needs to be migrated"""
|
"""Tag the version of the database schema, used to check if the database needs to be migrated"""
|
||||||
|
|||||||
@@ -28,15 +28,19 @@ class VersionManager:
|
|||||||
|
|
||||||
async def get_release_list(self) -> list:
|
async def get_release_list(self) -> list:
|
||||||
"""获取发行列表"""
|
"""获取发行列表"""
|
||||||
rls_list_resp = requests.get(
|
try:
|
||||||
url='https://api.github.com/repos/langbot-app/LangBot/releases',
|
rls_list_resp = requests.get(
|
||||||
proxies=self.ap.proxy_mgr.get_forward_proxies(),
|
url='https://api.github.com/repos/langbot-app/LangBot/releases',
|
||||||
timeout=5,
|
proxies=self.ap.proxy_mgr.get_forward_proxies(),
|
||||||
)
|
timeout=5,
|
||||||
|
)
|
||||||
rls_list = rls_list_resp.json()
|
rls_list_resp.raise_for_status() # 检查请求是否成功
|
||||||
|
rls_list = rls_list_resp.json()
|
||||||
return rls_list
|
return rls_list
|
||||||
|
except Exception as e:
|
||||||
|
self.ap.logger.warning(f"获取发行列表失败: {e}")
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
|
||||||
async def update_all(self):
|
async def update_all(self):
|
||||||
"""检查更新并下载源码"""
|
"""检查更新并下载源码"""
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "langbot"
|
name = "langbot"
|
||||||
version = "4.1.0"
|
version = "4.2.2"
|
||||||
description = "高稳定、支持扩展、多模态 - 大模型原生即时通信机器人平台"
|
description = "高稳定、支持扩展、多模态 - 大模型原生即时通信机器人平台"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10.1"
|
requires-python = ">=3.10.1"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ admins: []
|
|||||||
api:
|
api:
|
||||||
port: 5300
|
port: 5300
|
||||||
command:
|
command:
|
||||||
|
enable: true
|
||||||
prefix:
|
prefix:
|
||||||
- '!'
|
- '!'
|
||||||
- !
|
- !
|
||||||
|
|||||||
@@ -51,7 +51,6 @@
|
|||||||
"base-url": "https://api.dify.ai/v1",
|
"base-url": "https://api.dify.ai/v1",
|
||||||
"app-type": "chat",
|
"app-type": "chat",
|
||||||
"api-key": "your-api-key",
|
"api-key": "your-api-key",
|
||||||
"thinking-convert": "plain",
|
|
||||||
"timeout": 30
|
"timeout": 30
|
||||||
},
|
},
|
||||||
"dashscope-app-api": {
|
"dashscope-app-api": {
|
||||||
@@ -88,7 +87,7 @@
|
|||||||
"at-sender": true,
|
"at-sender": true,
|
||||||
"quote-origin": true,
|
"quote-origin": true,
|
||||||
"track-function-calls": false,
|
"track-function-calls": false,
|
||||||
"remove-think": true
|
"remove-think": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,28 +118,6 @@ stages:
|
|||||||
zh_Hans: API 密钥
|
zh_Hans: API 密钥
|
||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
- name: thinking-convert
|
|
||||||
label:
|
|
||||||
en_US: CoT Convert
|
|
||||||
zh_Hans: 思维链转换策略
|
|
||||||
type: select
|
|
||||||
required: true
|
|
||||||
default: plain
|
|
||||||
options:
|
|
||||||
- name: plain
|
|
||||||
label:
|
|
||||||
en_US: Convert to <think>...</think>
|
|
||||||
zh_Hans: 转换成 <think>...</think>
|
|
||||||
- name: original
|
|
||||||
label:
|
|
||||||
en_US: Original
|
|
||||||
zh_Hans: 原始
|
|
||||||
- name: remove
|
|
||||||
label:
|
|
||||||
en_US: Remove
|
|
||||||
zh_Hans: 移除
|
|
||||||
|
|
||||||
|
|
||||||
- name: dashscope-app-api
|
- name: dashscope-app-api
|
||||||
label:
|
label:
|
||||||
en_US: Aliyun Dashscope App API
|
en_US: Aliyun Dashscope App API
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ stages:
|
|||||||
en_US: Remove CoT
|
en_US: Remove CoT
|
||||||
zh_Hans: 删除思维链
|
zh_Hans: 删除思维链
|
||||||
description:
|
description:
|
||||||
en_US: If enabled, LangBot will remove the LLM thought content in response
|
en_US: 'If enabled, LangBot will remove the LLM thought content in response. Note: When using streaming response, removing CoT may cause the first token to wait for a long time.'
|
||||||
zh_Hans: 如果启用,将自动删除大模型回复中的模型思考内容
|
zh_Hans: '如果启用,将自动删除大模型回复中的模型思考内容。注意:当您使用流式响应时,删除思维链可能会导致首个 Token 的等待时间过长'
|
||||||
type: boolean
|
type: boolean
|
||||||
required: true
|
required: true
|
||||||
default: true
|
default: false
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ stages:
|
|||||||
label:
|
label:
|
||||||
en_US: Blacklist
|
en_US: Blacklist
|
||||||
zh_Hans: 黑名单
|
zh_Hans: 黑名单
|
||||||
|
description:
|
||||||
|
en_US: Sessions in the blacklist will be ignored, the format is `{launcher_type}_{launcher_id}`(remove quotes), for example `person_123` matches private chat, `group_456` matches group chat, `person_*` matches all private chats, `group_*` matches all group chats, `*_123` matches private and group chats with user ID 123
|
||||||
|
zh_Hans: 黑名单中的会话将被忽略;会话格式:`{launcher_type}_{launcher_id}`(删除引号),例如 `person_123` 匹配私聊会话,`group_456` 匹配群聊会话;`person_*` 匹配所有私聊会话,`group_*` 匹配所有群聊会话;`*_123` 匹配用户 ID 为 123 的私聊和群聊消息
|
||||||
type: array[string]
|
type: array[string]
|
||||||
required: true
|
required: true
|
||||||
default: []
|
default: []
|
||||||
@@ -86,6 +89,9 @@ stages:
|
|||||||
label:
|
label:
|
||||||
en_US: Whitelist
|
en_US: Whitelist
|
||||||
zh_Hans: 白名单
|
zh_Hans: 白名单
|
||||||
|
description:
|
||||||
|
en_US: Only respond to sessions in the whitelist, the format is `{launcher_type}_{launcher_id}`(remove quotes), for example `person_123` matches private chat, `group_456` matches group chat, `person_*` matches all private chats, `group_*` matches all group chats, `*_123` matches private and group chats with user ID 123
|
||||||
|
zh_Hans: 仅响应白名单中的会话;会话格式:`{launcher_type}_{launcher_id}`(删除引号),例如 `person_123` 匹配私聊会话,`group_456` 匹配群聊会话;`person_*` 匹配所有私聊会话,`group_*` 匹配所有群聊会话;`*_123` 匹配用户 ID 为 123 的私聊和群聊消息
|
||||||
type: array[string]
|
type: array[string]
|
||||||
required: true
|
required: true
|
||||||
default: []
|
default: []
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConfigList';
|
import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConfigList';
|
||||||
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||||
import { systemInfo } from '@/app/infra/http/HttpClient';
|
import { systemInfo, spaceClient } from '@/app/infra/http/HttpClient';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Moon, Sun, Monitor } from 'lucide-react';
|
import { Moon, Sun, Monitor } from 'lucide-react';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||||
import { LanguageSelector } from '@/components/ui/language-selector';
|
import { LanguageSelector } from '@/components/ui/language-selector';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
import PasswordChangeDialog from '@/app/home/components/password-change-dialog/PasswordChangeDialog';
|
import PasswordChangeDialog from '@/app/home/components/password-change-dialog/PasswordChangeDialog';
|
||||||
|
|
||||||
// TODO 侧边导航栏要加动画
|
// TODO 侧边导航栏要加动画
|
||||||
@@ -44,6 +45,7 @@ export default function HomeSidebar({
|
|||||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
const [passwordChangeOpen, setPasswordChangeOpen] = useState(false);
|
const [passwordChangeOpen, setPasswordChangeOpen] = useState(false);
|
||||||
const [languageSelectorOpen, setLanguageSelectorOpen] = useState(false);
|
const [languageSelectorOpen, setLanguageSelectorOpen] = useState(false);
|
||||||
|
const [starCount, setStarCount] = useState<number | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initSelect();
|
initSelect();
|
||||||
@@ -51,6 +53,16 @@ export default function HomeSidebar({
|
|||||||
localStorage.setItem('token', 'test-token');
|
localStorage.setItem('token', 'test-token');
|
||||||
localStorage.setItem('userEmail', 'test@example.com');
|
localStorage.setItem('userEmail', 'test@example.com');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spaceClient
|
||||||
|
.get('/api/v1/dist/info/repo')
|
||||||
|
.then((response) => {
|
||||||
|
const data = response as { repo: { stargazers_count: number } };
|
||||||
|
setStarCount(data.repo.stargazers_count);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to fetch GitHub star count:', error);
|
||||||
|
});
|
||||||
return () => console.log('sidebar.unmounted');
|
return () => console.log('sidebar.unmounted');
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
@@ -150,6 +162,30 @@ export default function HomeSidebar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`${styles.sidebarBottomContainer}`}>
|
<div className={`${styles.sidebarBottomContainer}`}>
|
||||||
|
{starCount !== null && (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
window.open('https://github.com/langbot-app/LangBot', '_blank');
|
||||||
|
}}
|
||||||
|
className="flex justify-center cursor-pointer p-2 rounded-lg hover:bg-accent/30 transition-colors"
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="hover:bg-secondary/50 px-3 py-1.5 text-sm font-medium transition-colors border-border relative overflow-hidden group"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 mr-2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z" />
|
||||||
|
</svg>
|
||||||
|
<div className="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/20 to-transparent group-hover:translate-x-full transition-transform duration-1000 ease-out"></div>
|
||||||
|
{starCount.toLocaleString()}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<SidebarChild
|
<SidebarChild
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// open docs.langbot.app
|
// open docs.langbot.app
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default function FileUploadZone({
|
|||||||
id="file-upload"
|
id="file-upload"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={handleFileSelect}
|
onChange={handleFileSelect}
|
||||||
accept=".pdf,.doc,.docx,.txt,.md,.html"
|
accept=".pdf,.doc,.docx,.txt,.md,.html,.zip"
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ const enUS = {
|
|||||||
dragAndDrop: 'Drag and drop files here or click to upload',
|
dragAndDrop: 'Drag and drop files here or click to upload',
|
||||||
uploading: 'Uploading...',
|
uploading: 'Uploading...',
|
||||||
supportedFormats:
|
supportedFormats:
|
||||||
'Supports PDF, Word, TXT, Markdown and other document formats',
|
'Supports PDF, Word, TXT, Markdown, HTML, ZIP and other document formats',
|
||||||
uploadSuccess: 'File uploaded successfully!',
|
uploadSuccess: 'File uploaded successfully!',
|
||||||
uploadError: 'File upload failed, please try again',
|
uploadError: 'File upload failed, please try again',
|
||||||
uploadingFile: 'Uploading file...',
|
uploadingFile: 'Uploading file...',
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ const zhHans = {
|
|||||||
noResults: '暂无文档',
|
noResults: '暂无文档',
|
||||||
dragAndDrop: '拖拽文件到此处或点击上传',
|
dragAndDrop: '拖拽文件到此处或点击上传',
|
||||||
uploading: '上传中...',
|
uploading: '上传中...',
|
||||||
supportedFormats: '支持 PDF、Word、TXT、Markdown 等文档格式',
|
supportedFormats: '支持 PDF、Word、TXT、Markdown、HTML、ZIP 等文档格式',
|
||||||
uploadSuccess: '文件上传成功!',
|
uploadSuccess: '文件上传成功!',
|
||||||
uploadError: '文件上传失败,请重试',
|
uploadError: '文件上传失败,请重试',
|
||||||
uploadingFile: '上传文件中...',
|
uploadingFile: '上传文件中...',
|
||||||
|
|||||||
Reference in New Issue
Block a user