Compare commits

...

17 Commits

Author SHA1 Message Date
Junyan Qin
89b25b8985 chore: release v4.2.2 2025-08-29 17:01:26 +08:00
devin-ai-integration[bot]
46b4482a7d feat: add GitHub star count component to sidebar (#1636)
* feat: add GitHub star count component to sidebar

- Add GitHub star component to sidebar bottom section
- Fetch star count from space.langbot.app API
- Display star count with proper internationalization
- Open GitHub repository in new tab when clicked
- Follow existing sidebar styling patterns

Co-Authored-By: Rock <rockchinq@gmail.com>

* perf: ui

* chore: remove githubStars text

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Rock <rockchinq@gmail.com>
2025-08-28 21:04:36 +08:00
Bruce
d9fa1cbb06 perf: add cmd enable config & fix announce request timeout & fix send card with disconnect ai platform (#1633)
* add cmd config && fix bugs

* perf: use `get`

* update bansess fix block match rule

* perf: comment for access-control session str

---------

Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-08-28 12:59:50 +08:00
Bruce
8858f432b5 fix dingtalk message sender id & update dingtalk streaming card without content (#1630) 2025-08-27 18:09:30 +08:00
Junyan Qin
8f5ec48522 doc: update shengsuanyun comment 2025-08-26 16:00:48 +08:00
devin-ai-integration[bot]
83ff64698b feat: add ZIP file upload support for knowledge base (#1626)
* feat: add ZIP file upload support for knowledge base

- Add _parse_zip method to FileParser class using zipfile library
- Support extraction and processing of TXT, PDF, DOCX, MD, HTML files from ZIP
- Update FileUploadZone to accept .zip files
- Add ZIP format to supported formats in internationalization files
- Implement error handling for invalid ZIP files and unsupported content
- Follow existing async parsing patterns and error handling conventions

Co-Authored-By: Rock <rockchinq@gmail.com>

* refactor: modify ZIP processing to store each document as separate file

- Remove _parse_zip method from FileParser as ZIP handling now occurs at knowledge base level
- Add _store_zip_file method to RuntimeKnowledgeBase to extract and store each document separately
- Each document in ZIP is now stored as individual file entry in knowledge base
- Process ZIP files in memory using io.BytesIO to avoid filesystem writes
- Generate unique file names for extracted documents to prevent conflicts

Co-Authored-By: Rock <rockchinq@gmail.com>

* perf: delete temp files

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Rock <rockchinq@gmail.com>
2025-08-23 21:18:13 +08:00
Junyan Qin
87ecb4e519 feat: add note for remove_think & remove dify remove cot code 2025-08-21 21:38:58 +08:00
Ljzd_PRO
df524b8a7a Fix: Fixed the incorrect extraction method of sender ID when converting aiocqhttp reply messages (#1624)
* fix: update invoke_embedding to return only embeddings from client.embed

* fix: Fixed the incorrect extraction method of sender ID when converting aiocqhttp reply messages
2025-08-21 20:46:26 +08:00
Junyan Qin
8a7df423ab chore: update shengsuanyun url 2025-08-21 14:14:25 +08:00
Junyan Qin
cafd623c92 chore: update shengsuanyun 2025-08-21 12:03:04 +08:00
Junyan Qin
4df11ef064 chore: update for shengsuanyun 2025-08-21 11:47:40 +08:00
Junyan Qin
aa7c08ee00 chore: release v4.2.1 2025-08-21 10:15:05 +08:00
Junyan Qin
b98de29b07 feat: add shengsuanyun requester 2025-08-20 23:33:35 +08:00
fdc310
c7c2eb4518 fix:in the gmini tool_calls no id The resulting call failure (#1622)
* fix:in the dify agent llm return message can not joint

* fix:in the gmini tool_calls no id The resulting call failure
2025-08-20 22:39:16 +08:00
Ljzd_PRO
37fa318258 fix: update invoke_embedding to return only embeddings from client.embed (#1619) 2025-08-20 10:25:33 +08:00
fdc310
ff7bebb782 fix:in the dify agent llm return message can not joint (#1617) 2025-08-19 23:23:00 +08:00
Junyan Qin
30bb26f898 doc(README): streaming output 2025-08-18 21:21:50 +08:00
31 changed files with 402 additions and 112 deletions

View File

@@ -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) | ✅ | 大模型聚合平台 |

View File

@@ -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 |

View File

@@ -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プラットフォーム |

View File

@@ -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) | ✅ | 大模型聚合平台 |

View File

@@ -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']

View File

@@ -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)

View File

@@ -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

View File

@@ -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())}')

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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',
},
)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

View 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

View File

@@ -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',

View File

@@ -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

View File

@@ -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'):

View File

@@ -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"""

View File

@@ -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):
"""检查更新并下载源码""" """检查更新并下载源码"""

View File

@@ -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"

View File

@@ -2,6 +2,7 @@ admins: []
api: api:
port: 5300 port: 5300
command: command:
enable: true
prefix: prefix:
- '!' - '!'
- -

View File

@@ -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
} }
} }
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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: []

View File

@@ -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

View File

@@ -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}
/> />

View File

@@ -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...',

View 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: '上传文件中...',