Compare commits

..

8 Commits

Author SHA1 Message Date
Hyu ec63978ecf docs: replace legacy documentation shortlinks (#2510)
* docs: replace legacy documentation shortlinks

* style: format updated documentation URLs

---------

Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
2026-09-05 01:19:28 +08:00
Hyu 1cfe87186c docs: update published documentation links (#2509)
Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
2026-09-05 00:55:18 +08:00
leonoxo 9794df0933 feat(n8n-runner): support async response handling (#2487)
* feat(n8n-runner): support async response handling

* fix(n8n-runner): expose response handling in form

* fix(n8n-runner): preserve async response semantics

---------

Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
2026-09-04 22:36:40 +08:00
Hyu a63808caa6 chore(release): prepare LangBot 4.10.10 (#2507)
Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
2026-09-04 21:47:31 +08:00
mintya de3c0b00ad fix(bot): roll back inserted bot row when adapter fails to load (#2497)
create_bot inserts the Bot row first and only then instantiates the
adapter via platform_mgr.load_bot. When the adapter constructor raises
(e.g. KeyError on a missing credential key), the insert is already
committed and nothing removes the row: the HTTP layer returns 500 but
a permanently disabled orphan bot stays in the DB. Callers never
receive the bot uuid, so they cannot compensate by deleting it, and
load_bots_from_db skips enable=False bots, so the orphan is never
loaded or surfaced anywhere.

Wrap load_bot in try/except and delete the inserted row before
re-raising. Add a regression test asserting the DELETE is issued when
the adapter constructor fails.
2026-09-04 13:06:12 +08:00
mintya cb45807b12 fix(qqofficial): tolerate missing optional token in adapter config (#2496)
Since b55f073e the token field is optional in qqofficial.yaml ("the
current adapter implementation does not use it either, so it can be
safely left blank"), but the adapter constructor still reads it with
config['token']. Creating a bot via QR binding (which only returns
appid/secret) or with the token field left blank raises KeyError and
the API returns 500.

Read it with config.get('token', '') instead. The value is never used
by QQOfficialClient beyond being stored, so an empty string default is
safe.
2026-09-04 13:05:36 +08:00
Amir Fathi d942bfe19a fix(wecom): read media_id instead of media in send_message() (#2447)
WecomMessageConverter.yiri2target() always emits {'media_id': ...} for
image/voice/file parts (never 'media'), matching the correct usage
already in reply_message(). send_message(), the entry point plugins
use via PluginToRuntimeAction.SEND_MESSAGE, instead read
content['media'], which is never set, so any image/voice/file part
raises KeyError and aborts the send.

Refs #1687

Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
2026-09-04 13:02:40 +08:00
Hyu b44b8f474d fix(cloud): provision login workspace just in time (#2505)
* fix(cloud): provision login workspace just in time

* fix(oauth): send callback URI during code exchange

* fix(oauth): preserve callback URI through browser exchange

* fix(oauth): negotiate redirect-bound codes

---------

Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
2026-09-03 23:14:44 +08:00
79 changed files with 896 additions and 334 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
name: 漏洞反馈 name: 漏洞反馈
description: 【供中文用户】报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭。由于自己操作不当/不甚了解所用技术栈引起的网络连接问题恕无法解决,请勿提 issue。容器间网络连接问题,参考文档 https://link.langbot.app/zh/docs/network description: 【供中文用户】报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭。由于自己操作不当/不甚了解所用技术栈引起的网络连接问题恕无法解决,请勿提 issue。容器间网络连接问题,参考文档 https://langbot.app/docs/zh/workshop/network-details
title: "[Bug]: " title: "[Bug]: "
labels: ["bug?"] labels: ["bug?"]
body: body:
+1 -1
View File
@@ -1,5 +1,5 @@
name: Bug report name: Bug report
description: Report bugs or vulnerabilities using this template. For container network connection issues, refer to the documentation https://link.langbot.app/en/docs/network description: Report bugs or vulnerabilities using this template. For container network connection issues, refer to the documentation https://langbot.app/docs/en/workshop/network-details
title: "[Bug]: " title: "[Bug]: "
labels: ["bug?"] labels: ["bug?"]
body: body:
+2 -2
View File
@@ -43,8 +43,8 @@ Run the narrowest useful test first, then broader checks when confidence is need
## Where to Look ## Where to Look
- Architecture map: `ARCHITECTURE.md`. - Architecture map: `ARCHITECTURE.md`.
- Dev environment guide: https://docs.langbot.app/zh/develop/dev-config. - Dev environment guide: https://langbot.app/docs/zh/develop/dev-config.
- Plugin runtime / CLI / SDK debugging: https://docs.langbot.app/zh/develop/plugin-runtime. - Plugin runtime / CLI / SDK debugging: https://langbot.app/docs/zh/develop/plugin-runtime.
- API-key auth: `docs/API_KEY_AUTH.md`. - API-key auth: `docs/API_KEY_AUTH.md`.
- Box deep-dive notes: `docs/review/box-architecture.md` and related files. - Box deep-dive notes: `docs/review/box-architecture.md` and related files.
- In-repo skills: `skills/` is the single source of truth for LangBot agent skills. - In-repo skills: `skills/` is the single source of truth for LangBot agent skills.
+6 -6
View File
@@ -19,9 +19,9 @@ English / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本
[![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers) [![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers)
<a href="https://langbot.app">Website</a> <a href="https://langbot.app">Website</a>
<a href="https://link.langbot.app/en/docs/features">Features</a> <a href="https://langbot.app/docs/en/insight/features">Features</a>
<a href="https://link.langbot.app/en/docs/guide">Docs</a> <a href="https://langbot.app/docs/en/insight/guide">Docs</a>
<a href="https://link.langbot.app/en/docs/api">API</a> <a href="https://langbot.app/docs/en/tags/readme">API</a>
<a href="https://space.langbot.app/cloud">Cloud</a> <a href="https://space.langbot.app/cloud">Cloud</a>
<a href="https://space.langbot.app">Plugin Market</a> <a href="https://space.langbot.app">Plugin Market</a>
<a href="https://langbot.featurebase.app/roadmap">Roadmap</a> <a href="https://langbot.featurebase.app/roadmap">Roadmap</a>
@@ -49,7 +49,7 @@ LangBot is an **open-source, production-grade platform** for building AI-powered
- **Web Management Panel** — Configure, manage, and monitor your bots through an intuitive browser interface. No YAML editing required. - **Web Management Panel** — Configure, manage, and monitor your bots through an intuitive browser interface. No YAML editing required.
- **Multi-Pipeline Architecture** — Different bots for different scenarios, with comprehensive monitoring and exception handling. - **Multi-Pipeline Architecture** — Different bots for different scenarios, with comprehensive monitoring and exception handling.
[→ Learn more about all features](https://link.langbot.app/en/docs/features) [→ Learn more about all features](https://langbot.app/docs/en/insight/features)
📍 Practical guides: [deploy a multi-platform AI bot in 5 minutes](https://langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [connect DeepSeek to WeChat, Discord, and Telegram](https://langbot.app/en/blog/connect-deepseek-to-wechat/), [run a Dify Agent in Discord, Telegram, and Slack](https://langbot.app/en/blog/dify-agent-discord-telegram-slack/), and [build an n8n-powered chatbot](https://langbot.app/en/blog/n8n-multi-platform-ai-chatbot/). 📍 Practical guides: [deploy a multi-platform AI bot in 5 minutes](https://langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [connect DeepSeek to WeChat, Discord, and Telegram](https://langbot.app/en/blog/connect-deepseek-to-wechat/), [run a Dify Agent in Discord, Telegram, and Slack](https://langbot.app/en/blog/dify-agent-discord-telegram-slack/), and [build an n8n-powered chatbot](https://langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
@@ -89,7 +89,7 @@ docker compose --profile all up -d
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF) [![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
**More options:** [Docker](https://link.langbot.app/en/docs/docker) · [Manual](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](https://docs.langbot.app/en/deploy/langbot/kubernetes) **More options:** [Docker](https://langbot.app/docs/en/deploy/langbot/docker) · [Manual](https://langbot.app/docs/en/deploy/langbot/manual) · [BTPanel](https://langbot.app/docs/en/deploy/langbot/one-click/bt) · [Kubernetes](https://langbot.app/docs/en/deploy/langbot/kubernetes)
--- ---
@@ -151,7 +151,7 @@ _Note: Public demo environment. Do not enter sensitive information._
| [302.AI](https://share.302ai.cn/SuTG99) | Gateway | ✅ | | [302.AI](https://share.302ai.cn/SuTG99) | Gateway | ✅ |
| [Qiniu](https://www.qiniu.com/ai/agent) | Gateway | ✅ | | [Qiniu](https://www.qiniu.com/ai/agent) | Gateway | ✅ |
[→ View all integrations](https://link.langbot.app/en/docs/features) [→ View all integrations](https://langbot.app/docs/en/insight/features)
--- ---
+6 -6
View File
@@ -21,9 +21,9 @@
[![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot) [![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot)
<a href="https://langbot.app">官网</a> <a href="https://langbot.app">官网</a>
<a href="https://link.langbot.app/zh/docs/features">特性</a> <a href="https://langbot.app/docs/zh/insight/features">特性</a>
<a href="https://link.langbot.app/zh/docs/guide">文档</a> <a href="https://langbot.app/docs/zh/insight/guide">文档</a>
<a href="https://link.langbot.app/zh/docs/api">API</a> <a href="https://langbot.app/docs/zh/tags/readme">API</a>
<a href="https://space.langbot.app/cloud">Cloud</a> <a href="https://space.langbot.app/cloud">Cloud</a>
<a href="https://space.langbot.app">扩展市场</a> <a href="https://space.langbot.app">扩展市场</a>
<a href="https://langbot.featurebase.app/roadmap">路线图</a> <a href="https://langbot.featurebase.app/roadmap">路线图</a>
@@ -49,7 +49,7 @@ LangBot 是一个**开源的生产级平台**,用于构建 AI 驱动的即时
- **Web 管理面板** — 通过浏览器直观地配置、管理和监控机器人,无需手动编辑配置文件。 - **Web 管理面板** — 通过浏览器直观地配置、管理和监控机器人,无需手动编辑配置文件。
- **多流水线架构** — 不同机器人用于不同场景,具备全面的监控和异常处理能力。 - **多流水线架构** — 不同机器人用于不同场景,具备全面的监控和异常处理能力。
[→ 了解更多功能特性](https://link.langbot.app/zh/docs/features) [→ 了解更多功能特性](https://langbot.app/docs/zh/insight/features)
📍 实践指南:[5 分钟部署多平台 AI 机器人](https://langbot.app/zh/blog/deploy-ai-bot-in-5-minutes/)、[将 DeepSeek 接入微信、企业微信与 Discord](https://langbot.app/zh/blog/connect-deepseek-to-wechat/)、[让 Dify Agent 跑在 Discord、Telegram 和 Slack 上](https://langbot.app/zh/blog/dify-agent-discord-telegram-slack/),以及[用 n8n 构建多平台 AI 聊天机器人](https://langbot.app/zh/blog/n8n-multi-platform-ai-chatbot/)。 📍 实践指南:[5 分钟部署多平台 AI 机器人](https://langbot.app/zh/blog/deploy-ai-bot-in-5-minutes/)、[将 DeepSeek 接入微信、企业微信与 Discord](https://langbot.app/zh/blog/connect-deepseek-to-wechat/)、[让 Dify Agent 跑在 Discord、Telegram 和 Slack 上](https://langbot.app/zh/blog/dify-agent-discord-telegram-slack/),以及[用 n8n 构建多平台 AI 聊天机器人](https://langbot.app/zh/blog/n8n-multi-platform-ai-chatbot/)。
@@ -89,7 +89,7 @@ docker compose --profile all up -d
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF) [![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
**更多方式:** [Docker](https://link.langbot.app/zh/docs/docker) · [手动部署](https://link.langbot.app/zh/docs/manual-deploy) · [宝塔面板](https://link.langbot.app/zh/docs/bt-panel) · [Kubernetes](https://docs.langbot.app/zh/deploy/langbot/kubernetes) **更多方式:** [Docker](https://langbot.app/docs/zh/deploy/langbot/docker) · [手动部署](https://langbot.app/docs/zh/deploy/langbot/manual) · [宝塔面板](https://langbot.app/docs/zh/deploy/langbot/one-click/bt) · [Kubernetes](https://langbot.app/docs/zh/deploy/langbot/kubernetes)
--- ---
@@ -152,7 +152,7 @@ docker compose --profile all up -d
| [百宝箱Tbox](https://www.tbox.cn/open) | 智能体平台 | ✅ | | [百宝箱Tbox](https://www.tbox.cn/open) | 智能体平台 | ✅ |
| [七牛云Qiniu](https://www.qiniu.com/ai/agent) | 聚合平台 | ✅ | | [七牛云Qiniu](https://www.qiniu.com/ai/agent) | 聚合平台 | ✅ |
[→ 查看完整集成列表](https://link.langbot.app/zh/docs/features) [→ 查看完整集成列表](https://langbot.app/docs/zh/insight/features)
### TTS(语音合成) ### TTS(语音合成)
+6 -6
View File
@@ -19,9 +19,9 @@
[![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers) [![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers)
<a href="https://langbot.app">Inicio</a> <a href="https://langbot.app">Inicio</a>
<a href="https://link.langbot.app/en/docs/features">Características</a> <a href="https://langbot.app/docs/en/insight/features">Características</a>
<a href="https://link.langbot.app/en/docs/guide">Documentación</a> <a href="https://langbot.app/docs/en/insight/guide">Documentación</a>
<a href="https://link.langbot.app/en/docs/api">API</a> <a href="https://langbot.app/docs/en/tags/readme">API</a>
<a href="https://space.langbot.app">Mercado de Plugins</a> <a href="https://space.langbot.app">Mercado de Plugins</a>
<a href="https://langbot.featurebase.app/roadmap">Hoja de Ruta</a> <a href="https://langbot.featurebase.app/roadmap">Hoja de Ruta</a>
@@ -48,7 +48,7 @@ LangBot es una **plataforma de código abierto y grado de producción** para con
- **Panel de Gestión Web** — Configure, gestione y monitoree sus bots a través de una interfaz de navegador intuitiva. Sin necesidad de editar YAML. - **Panel de Gestión Web** — Configure, gestione y monitoree sus bots a través de una interfaz de navegador intuitiva. Sin necesidad de editar YAML.
- **Arquitectura Multi-Pipeline** — Diferentes bots para diferentes escenarios, con monitoreo completo y manejo de excepciones. - **Arquitectura Multi-Pipeline** — Diferentes bots para diferentes escenarios, con monitoreo completo y manejo de excepciones.
[→ Conocer más sobre todas las funcionalidades](https://link.langbot.app/en/docs/features) [→ Conocer más sobre todas las funcionalidades](https://langbot.app/docs/en/insight/features)
📍 Guías prácticas: [desplegar un bot de IA multiplataforma en 5 minutos](https://langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [conectar DeepSeek a WeChat, Discord y Telegram](https://langbot.app/en/blog/connect-deepseek-to-wechat/), [ejecutar un Dify Agent en Discord, Telegram y Slack](https://langbot.app/en/blog/dify-agent-discord-telegram-slack/) y [crear un chatbot con n8n](https://langbot.app/en/blog/n8n-multi-platform-ai-chatbot/). 📍 Guías prácticas: [desplegar un bot de IA multiplataforma en 5 minutos](https://langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [conectar DeepSeek a WeChat, Discord y Telegram](https://langbot.app/en/blog/connect-deepseek-to-wechat/), [ejecutar un Dify Agent en Discord, Telegram y Slack](https://langbot.app/en/blog/dify-agent-discord-telegram-slack/) y [crear un chatbot con n8n](https://langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
@@ -88,7 +88,7 @@ docker compose --profile all up -d
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF) [![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
**Más opciones:** [Docker](https://link.langbot.app/en/docs/docker) · [Manual](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](https://docs.langbot.app/en/deploy/langbot/kubernetes) **Más opciones:** [Docker](https://langbot.app/docs/en/deploy/langbot/docker) · [Manual](https://langbot.app/docs/en/deploy/langbot/manual) · [BTPanel](https://langbot.app/docs/en/deploy/langbot/one-click/bt) · [Kubernetes](https://langbot.app/docs/en/deploy/langbot/kubernetes)
--- ---
@@ -149,7 +149,7 @@ docker compose --profile all up -d
| [302.AI](https://share.302ai.cn/SuTG99) | Pasarela | ✅ | | [302.AI](https://share.302ai.cn/SuTG99) | Pasarela | ✅ |
| [Qiniu](https://www.qiniu.com/ai/agent) | Pasarela | ✅ | | [Qiniu](https://www.qiniu.com/ai/agent) | Pasarela | ✅ |
[→ Ver todas las integraciones](https://link.langbot.app/en/docs/features) [→ Ver todas las integraciones](https://langbot.app/docs/en/insight/features)
--- ---
+6 -6
View File
@@ -19,9 +19,9 @@
[![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers) [![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers)
<a href="https://langbot.app">Accueil</a> <a href="https://langbot.app">Accueil</a>
<a href="https://link.langbot.app/en/docs/features">Fonctionnalités</a> <a href="https://langbot.app/docs/en/insight/features">Fonctionnalités</a>
<a href="https://link.langbot.app/en/docs/guide">Documentation</a> <a href="https://langbot.app/docs/en/insight/guide">Documentation</a>
<a href="https://link.langbot.app/en/docs/api">API</a> <a href="https://langbot.app/docs/en/tags/readme">API</a>
<a href="https://space.langbot.app">Marché des Plugins</a> <a href="https://space.langbot.app">Marché des Plugins</a>
<a href="https://langbot.featurebase.app/roadmap">Feuille de Route</a> <a href="https://langbot.featurebase.app/roadmap">Feuille de Route</a>
@@ -48,7 +48,7 @@ LangBot est une **plateforme open-source de niveau production** pour créer des
- **Panneau de Gestion Web** — Configurez, gérez et surveillez vos bots via une interface navigateur intuitive. Aucune édition de YAML requise. - **Panneau de Gestion Web** — Configurez, gérez et surveillez vos bots via une interface navigateur intuitive. Aucune édition de YAML requise.
- **Architecture Multi-Pipeline** — Différents bots pour différents scénarios, avec surveillance complète et gestion des exceptions. - **Architecture Multi-Pipeline** — Différents bots pour différents scénarios, avec surveillance complète et gestion des exceptions.
[→ En savoir plus sur toutes les fonctionnalités](https://link.langbot.app/en/docs/features) [→ En savoir plus sur toutes les fonctionnalités](https://langbot.app/docs/en/insight/features)
📍 Guides pratiques : [déployer un bot IA multiplateforme en 5 minutes](https://langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [connecter DeepSeek à WeChat, Discord et Telegram](https://langbot.app/en/blog/connect-deepseek-to-wechat/), [exécuter un Dify Agent dans Discord, Telegram et Slack](https://langbot.app/en/blog/dify-agent-discord-telegram-slack/) et [créer un chatbot avec n8n](https://langbot.app/en/blog/n8n-multi-platform-ai-chatbot/). 📍 Guides pratiques : [déployer un bot IA multiplateforme en 5 minutes](https://langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [connecter DeepSeek à WeChat, Discord et Telegram](https://langbot.app/en/blog/connect-deepseek-to-wechat/), [exécuter un Dify Agent dans Discord, Telegram et Slack](https://langbot.app/en/blog/dify-agent-discord-telegram-slack/) et [créer un chatbot avec n8n](https://langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
@@ -88,7 +88,7 @@ docker compose --profile all up -d
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF) [![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
**Plus d'options :** [Docker](https://link.langbot.app/en/docs/docker) · [Manuel](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](https://docs.langbot.app/en/deploy/langbot/kubernetes) **Plus d'options :** [Docker](https://langbot.app/docs/en/deploy/langbot/docker) · [Manuel](https://langbot.app/docs/en/deploy/langbot/manual) · [BTPanel](https://langbot.app/docs/en/deploy/langbot/one-click/bt) · [Kubernetes](https://langbot.app/docs/en/deploy/langbot/kubernetes)
--- ---
@@ -149,7 +149,7 @@ docker compose --profile all up -d
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Plateforme GPU | ✅ | | [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Plateforme GPU | ✅ |
| [Qiniu](https://www.qiniu.com/ai/agent) | Passerelle | ✅ | | [Qiniu](https://www.qiniu.com/ai/agent) | Passerelle | ✅ |
[→ Voir toutes les intégrations](https://link.langbot.app/en/docs/features) [→ Voir toutes les intégrations](https://langbot.app/docs/en/insight/features)
--- ---
+6 -6
View File
@@ -19,9 +19,9 @@
[![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers) [![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers)
<a href="https://langbot.app">ホーム</a> <a href="https://langbot.app">ホーム</a>
<a href="https://link.langbot.app/ja/docs/features">機能</a> <a href="https://langbot.app/docs/ja/insight/features">機能</a>
<a href="https://link.langbot.app/ja/docs/guide">ドキュメント</a> <a href="https://langbot.app/docs/ja/insight/guide">ドキュメント</a>
<a href="https://link.langbot.app/ja/docs/api">API</a> <a href="https://langbot.app/docs/ja/tags/readme">API</a>
<a href="https://space.langbot.app">プラグインマーケット</a> <a href="https://space.langbot.app">プラグインマーケット</a>
<a href="https://langbot.featurebase.app/roadmap">ロードマップ</a> <a href="https://langbot.featurebase.app/roadmap">ロードマップ</a>
@@ -48,7 +48,7 @@ LangBot は、AI搭載のインスタントメッセージングボットを構
- **Web管理パネル** — 直感的なブラウザインターフェースからボットの設定、管理、監視が可能。YAML編集は不要。 - **Web管理パネル** — 直感的なブラウザインターフェースからボットの設定、管理、監視が可能。YAML編集は不要。
- **マルチパイプラインアーキテクチャ** — 異なるシナリオに異なるボットを配置し、包括的な監視と例外処理を実現。 - **マルチパイプラインアーキテクチャ** — 異なるシナリオに異なるボットを配置し、包括的な監視と例外処理を実現。
[→ すべての機能について詳しく見る](https://link.langbot.app/ja/docs/features) [→ すべての機能について詳しく見る](https://langbot.app/docs/ja/insight/features)
📍 実践ガイド: [5分でマルチプラットフォームAIボットをデプロイ](https://langbot.app/en/blog/deploy-ai-bot-in-5-minutes/)、[DeepSeekをWeChat・Discord・Telegramに接続](https://langbot.app/en/blog/connect-deepseek-to-wechat/)、[Dify AgentをDiscord・Telegram・Slackで動かす](https://langbot.app/en/blog/dify-agent-discord-telegram-slack/)、[n8n連携チャットボットを構築](https://langbot.app/en/blog/n8n-multi-platform-ai-chatbot/)。 📍 実践ガイド: [5分でマルチプラットフォームAIボットをデプロイ](https://langbot.app/en/blog/deploy-ai-bot-in-5-minutes/)、[DeepSeekをWeChat・Discord・Telegramに接続](https://langbot.app/en/blog/connect-deepseek-to-wechat/)、[Dify AgentをDiscord・Telegram・Slackで動かす](https://langbot.app/en/blog/dify-agent-discord-telegram-slack/)、[n8n連携チャットボットを構築](https://langbot.app/en/blog/n8n-multi-platform-ai-chatbot/)。
@@ -88,7 +88,7 @@ docker compose --profile all up -d
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF) [![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
**その他:** [Docker](https://link.langbot.app/en/docs/docker) · [手動デプロイ](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](https://docs.langbot.app/en/deploy/langbot/kubernetes) **その他:** [Docker](https://langbot.app/docs/en/deploy/langbot/docker) · [手動デプロイ](https://langbot.app/docs/en/deploy/langbot/manual) · [BTPanel](https://langbot.app/docs/en/deploy/langbot/one-click/bt) · [Kubernetes](https://langbot.app/docs/en/deploy/langbot/kubernetes)
--- ---
@@ -149,7 +149,7 @@ docker compose --profile all up -d
| [302.AI](https://share.302ai.cn/SuTG99) | ゲートウェイ | ✅ | | [302.AI](https://share.302ai.cn/SuTG99) | ゲートウェイ | ✅ |
| [Qiniu](https://www.qiniu.com/ai/agent) | ゲートウェイ | ✅ | | [Qiniu](https://www.qiniu.com/ai/agent) | ゲートウェイ | ✅ |
[→ すべての統合を表示](https://link.langbot.app/en/docs/features) [→ すべての統合を表示](https://langbot.app/docs/en/insight/features)
--- ---
+6 -6
View File
@@ -19,9 +19,9 @@
[![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers) [![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers)
<a href="https://langbot.app">홈</a> <a href="https://langbot.app">홈</a>
<a href="https://link.langbot.app/en/docs/features">기능</a> <a href="https://langbot.app/docs/en/insight/features">기능</a>
<a href="https://link.langbot.app/en/docs/guide">문서</a> <a href="https://langbot.app/docs/en/insight/guide">문서</a>
<a href="https://link.langbot.app/en/docs/api">API</a> <a href="https://langbot.app/docs/en/tags/readme">API</a>
<a href="https://space.langbot.app">플러그인 마켓</a> <a href="https://space.langbot.app">플러그인 마켓</a>
<a href="https://langbot.featurebase.app/roadmap">로드맵</a> <a href="https://langbot.featurebase.app/roadmap">로드맵</a>
@@ -48,7 +48,7 @@ LangBot은 AI 기반 인스턴트 메시징 봇을 구축하기 위한 **오픈
- **웹 관리 패널** — 직관적인 브라우저 인터페이스로 봇을 구성, 관리 및 모니터링. YAML 편집 불필요. - **웹 관리 패널** — 직관적인 브라우저 인터페이스로 봇을 구성, 관리 및 모니터링. YAML 편집 불필요.
- **멀티 파이프라인 아키텍처** — 다양한 시나리오에 맞는 다양한 봇 구성, 종합 모니터링 및 예외 처리. - **멀티 파이프라인 아키텍처** — 다양한 시나리오에 맞는 다양한 봇 구성, 종합 모니터링 및 예외 처리.
[→ 모든 기능 자세히 보기](https://link.langbot.app/en/docs/features) [→ 모든 기능 자세히 보기](https://langbot.app/docs/en/insight/features)
📍 실전 가이드: [5분 만에 멀티 플랫폼 AI 봇 배포하기](https://langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [DeepSeek를 WeChat, Discord, Telegram에 연결하기](https://langbot.app/en/blog/connect-deepseek-to-wechat/), [Dify Agent를 Discord, Telegram, Slack에서 실행하기](https://langbot.app/en/blog/dify-agent-discord-telegram-slack/), [n8n 기반 챗봇 만들기](https://langbot.app/en/blog/n8n-multi-platform-ai-chatbot/). 📍 실전 가이드: [5분 만에 멀티 플랫폼 AI 봇 배포하기](https://langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [DeepSeek를 WeChat, Discord, Telegram에 연결하기](https://langbot.app/en/blog/connect-deepseek-to-wechat/), [Dify Agent를 Discord, Telegram, Slack에서 실행하기](https://langbot.app/en/blog/dify-agent-discord-telegram-slack/), [n8n 기반 챗봇 만들기](https://langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
@@ -88,7 +88,7 @@ docker compose --profile all up -d
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF) [![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
**더 많은 옵션:** [Docker](https://link.langbot.app/en/docs/docker) · [수동 배포](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](https://docs.langbot.app/en/deploy/langbot/kubernetes) **더 많은 옵션:** [Docker](https://langbot.app/docs/en/deploy/langbot/docker) · [수동 배포](https://langbot.app/docs/en/deploy/langbot/manual) · [BTPanel](https://langbot.app/docs/en/deploy/langbot/one-click/bt) · [Kubernetes](https://langbot.app/docs/en/deploy/langbot/kubernetes)
--- ---
@@ -149,7 +149,7 @@ docker compose --profile all up -d
| [302.AI](https://share.302ai.cn/SuTG99) | 게이트웨이 | ✅ | | [302.AI](https://share.302ai.cn/SuTG99) | 게이트웨이 | ✅ |
| [Qiniu](https://www.qiniu.com/ai/agent) | 게이트웨이 | ✅ | | [Qiniu](https://www.qiniu.com/ai/agent) | 게이트웨이 | ✅ |
[→ 모든 통합 보기](https://link.langbot.app/en/docs/features) [→ 모든 통합 보기](https://langbot.app/docs/en/insight/features)
--- ---
+6 -6
View File
@@ -19,9 +19,9 @@
[![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers) [![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers)
<a href="https://langbot.app">Главная</a> <a href="https://langbot.app">Главная</a>
<a href="https://link.langbot.app/en/docs/features">Возможности</a> <a href="https://langbot.app/docs/en/insight/features">Возможности</a>
<a href="https://link.langbot.app/en/docs/guide">Документация</a> <a href="https://langbot.app/docs/en/insight/guide">Документация</a>
<a href="https://link.langbot.app/en/docs/api">API</a> <a href="https://langbot.app/docs/en/tags/readme">API</a>
<a href="https://space.langbot.app">Магазин плагинов</a> <a href="https://space.langbot.app">Магазин плагинов</a>
<a href="https://langbot.featurebase.app/roadmap">Дорожная карта</a> <a href="https://langbot.featurebase.app/roadmap">Дорожная карта</a>
@@ -48,7 +48,7 @@ LangBot — это **платформа с открытым исходным к
- **Веб-панель управления** — Настраивайте, управляйте и мониторьте ваших ботов через интуитивный браузерный интерфейс. Ручное редактирование YAML не требуется. - **Веб-панель управления** — Настраивайте, управляйте и мониторьте ваших ботов через интуитивный браузерный интерфейс. Ручное редактирование YAML не требуется.
- **Мультиконвейерная архитектура** — Разные боты для разных сценариев с комплексным мониторингом и обработкой исключений. - **Мультиконвейерная архитектура** — Разные боты для разных сценариев с комплексным мониторингом и обработкой исключений.
[→ Подробнее обо всех возможностях](https://link.langbot.app/en/docs/features) [→ Подробнее обо всех возможностях](https://langbot.app/docs/en/insight/features)
📍 Практические руководства: [развернуть мультиплатформенного ИИ-бота за 5 минут](https://langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [подключить DeepSeek к WeChat, Discord и Telegram](https://langbot.app/en/blog/connect-deepseek-to-wechat/), [запустить Dify Agent в Discord, Telegram и Slack](https://langbot.app/en/blog/dify-agent-discord-telegram-slack/) и [создать чат-бота на n8n](https://langbot.app/en/blog/n8n-multi-platform-ai-chatbot/). 📍 Практические руководства: [развернуть мультиплатформенного ИИ-бота за 5 минут](https://langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [подключить DeepSeek к WeChat, Discord и Telegram](https://langbot.app/en/blog/connect-deepseek-to-wechat/), [запустить Dify Agent в Discord, Telegram и Slack](https://langbot.app/en/blog/dify-agent-discord-telegram-slack/) и [создать чат-бота на n8n](https://langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
@@ -88,7 +88,7 @@ docker compose --profile all up -d
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF) [![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
**Другие варианты:** [Docker](https://link.langbot.app/en/docs/docker) · [Ручная установка](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](https://docs.langbot.app/en/deploy/langbot/kubernetes) **Другие варианты:** [Docker](https://langbot.app/docs/en/deploy/langbot/docker) · [Ручная установка](https://langbot.app/docs/en/deploy/langbot/manual) · [BTPanel](https://langbot.app/docs/en/deploy/langbot/one-click/bt) · [Kubernetes](https://langbot.app/docs/en/deploy/langbot/kubernetes)
--- ---
@@ -149,7 +149,7 @@ docker compose --profile all up -d
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Платформа GPU | ✅ | | [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Платформа GPU | ✅ |
| [Qiniu](https://www.qiniu.com/ai/agent) | Шлюз | ✅ | | [Qiniu](https://www.qiniu.com/ai/agent) | Шлюз | ✅ |
[→ Смотреть все интеграции](https://link.langbot.app/en/docs/features) [→ Смотреть все интеграции](https://langbot.app/docs/en/insight/features)
--- ---
+6 -6
View File
@@ -21,9 +21,9 @@
[![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot) [![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot)
<a href="https://langbot.app">官網</a> <a href="https://langbot.app">官網</a>
<a href="https://link.langbot.app/zh/docs/features">特性</a> <a href="https://langbot.app/docs/zh/insight/features">特性</a>
<a href="https://link.langbot.app/zh/docs/guide">文件</a> <a href="https://langbot.app/docs/zh/insight/guide">文件</a>
<a href="https://link.langbot.app/zh/docs/api">API</a> <a href="https://langbot.app/docs/zh/tags/readme">API</a>
<a href="https://space.langbot.app">外掛市場</a> <a href="https://space.langbot.app">外掛市場</a>
<a href="https://langbot.featurebase.app/roadmap">路線圖</a> <a href="https://langbot.featurebase.app/roadmap">路線圖</a>
@@ -50,7 +50,7 @@ LangBot 是一個**開源的生產級平台**,用於建構 AI 驅動的即時
- **Web 管理面板** — 透過瀏覽器直觀地配置、管理和監控機器人,無需手動編輯設定檔。 - **Web 管理面板** — 透過瀏覽器直觀地配置、管理和監控機器人,無需手動編輯設定檔。
- **多流水線架構** — 不同機器人用於不同場景,具備全面的監控和異常處理能力。 - **多流水線架構** — 不同機器人用於不同場景,具備全面的監控和異常處理能力。
[→ 了解更多功能特性](https://link.langbot.app/zh/docs/features) [→ 了解更多功能特性](https://langbot.app/docs/zh/insight/features)
📍 實踐指南:[5 分鐘部署多平台 AI 機器人](https://langbot.app/zh/blog/deploy-ai-bot-in-5-minutes/)、[將 DeepSeek 接入微信、企業微信與 Discord](https://langbot.app/zh/blog/connect-deepseek-to-wechat/)、[讓 Dify Agent 跑在 Discord、Telegram 和 Slack 上](https://langbot.app/zh/blog/dify-agent-discord-telegram-slack/),以及[用 n8n 建構多平台 AI 聊天機器人](https://langbot.app/zh/blog/n8n-multi-platform-ai-chatbot/)。 📍 實踐指南:[5 分鐘部署多平台 AI 機器人](https://langbot.app/zh/blog/deploy-ai-bot-in-5-minutes/)、[將 DeepSeek 接入微信、企業微信與 Discord](https://langbot.app/zh/blog/connect-deepseek-to-wechat/)、[讓 Dify Agent 跑在 Discord、Telegram 和 Slack 上](https://langbot.app/zh/blog/dify-agent-discord-telegram-slack/),以及[用 n8n 建構多平台 AI 聊天機器人](https://langbot.app/zh/blog/n8n-multi-platform-ai-chatbot/)。
@@ -90,7 +90,7 @@ docker compose --profile all up -d
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF) [![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
**更多方式:** [Docker](https://link.langbot.app/zh/docs/docker) · [手動部署](https://link.langbot.app/zh/docs/manual-deploy) · [寶塔面板](https://link.langbot.app/zh/docs/bt-panel) · [Kubernetes](https://docs.langbot.app/zh/deploy/langbot/kubernetes) **更多方式:** [Docker](https://langbot.app/docs/zh/deploy/langbot/docker) · [手動部署](https://langbot.app/docs/zh/deploy/langbot/manual) · [寶塔面板](https://langbot.app/docs/zh/deploy/langbot/one-click/bt) · [Kubernetes](https://langbot.app/docs/zh/deploy/langbot/kubernetes)
--- ---
@@ -165,7 +165,7 @@ docker compose --profile all up -d
|-----------|------| |-----------|------|
| 阿里雲百煉 | [外掛](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin) | | 阿里雲百煉 | [外掛](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin) |
[→ 查看完整整合列表](https://link.langbot.app/zh/docs/features) [→ 查看完整整合列表](https://langbot.app/docs/zh/insight/features)
--- ---
+6 -6
View File
@@ -19,9 +19,9 @@
[![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers) [![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers)
<a href="https://langbot.app">Trang chủ</a> <a href="https://langbot.app">Trang chủ</a>
<a href="https://link.langbot.app/en/docs/features">Tính năng</a> <a href="https://langbot.app/docs/en/insight/features">Tính năng</a>
<a href="https://link.langbot.app/en/docs/guide">Tài liệu</a> <a href="https://langbot.app/docs/en/insight/guide">Tài liệu</a>
<a href="https://link.langbot.app/en/docs/api">API</a> <a href="https://langbot.app/docs/en/tags/readme">API</a>
<a href="https://space.langbot.app">Chợ Plugin</a> <a href="https://space.langbot.app">Chợ Plugin</a>
<a href="https://langbot.featurebase.app/roadmap">Lộ trình</a> <a href="https://langbot.featurebase.app/roadmap">Lộ trình</a>
@@ -48,7 +48,7 @@ LangBot là một **nền tảng mã nguồn mở, cấp sản xuất** để x
- **Bảng quản lý Web** — Cấu hình, quản lý và giám sát bot thông qua giao diện trình duyệt trực quan. Không cần chỉnh sửa YAML. - **Bảng quản lý Web** — Cấu hình, quản lý và giám sát bot thông qua giao diện trình duyệt trực quan. Không cần chỉnh sửa YAML.
- **Kiến trúc đa Pipeline** — Các bot khác nhau cho các kịch bản khác nhau, với giám sát toàn diện và xử lý ngoại lệ. - **Kiến trúc đa Pipeline** — Các bot khác nhau cho các kịch bản khác nhau, với giám sát toàn diện và xử lý ngoại lệ.
[→ Tìm hiểu thêm về tất cả tính năng](https://link.langbot.app/en/docs/features) [→ Tìm hiểu thêm về tất cả tính năng](https://langbot.app/docs/en/insight/features)
📍 Hướng dẫn thực hành: [triển khai bot AI đa nền tảng trong 5 phút](https://langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [kết nối DeepSeek với WeChat, Discord và Telegram](https://langbot.app/en/blog/connect-deepseek-to-wechat/), [chạy Dify Agent trên Discord, Telegram và Slack](https://langbot.app/en/blog/dify-agent-discord-telegram-slack/) và [xây dựng chatbot với n8n](https://langbot.app/en/blog/n8n-multi-platform-ai-chatbot/). 📍 Hướng dẫn thực hành: [triển khai bot AI đa nền tảng trong 5 phút](https://langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [kết nối DeepSeek với WeChat, Discord và Telegram](https://langbot.app/en/blog/connect-deepseek-to-wechat/), [chạy Dify Agent trên Discord, Telegram và Slack](https://langbot.app/en/blog/dify-agent-discord-telegram-slack/) và [xây dựng chatbot với n8n](https://langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
@@ -88,7 +88,7 @@ docker compose --profile all up -d
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF) [![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
**Thêm tùy chọn:** [Docker](https://link.langbot.app/en/docs/docker) · [Thủ công](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](https://docs.langbot.app/en/deploy/langbot/kubernetes) **Thêm tùy chọn:** [Docker](https://langbot.app/docs/en/deploy/langbot/docker) · [Thủ công](https://langbot.app/docs/en/deploy/langbot/manual) · [BTPanel](https://langbot.app/docs/en/deploy/langbot/one-click/bt) · [Kubernetes](https://langbot.app/docs/en/deploy/langbot/kubernetes)
--- ---
@@ -149,7 +149,7 @@ docker compose --profile all up -d
| [302.AI](https://share.302ai.cn/SuTG99) | Cổng | ✅ | | [302.AI](https://share.302ai.cn/SuTG99) | Cổng | ✅ |
| [Qiniu](https://www.qiniu.com/ai/agent) | Cổng | ✅ | | [Qiniu](https://www.qiniu.com/ai/agent) | Cổng | ✅ |
[→ Xem tất cả tích hợp](https://link.langbot.app/en/docs/features) [→ Xem tất cả tích hợp](https://langbot.app/docs/en/insight/features)
--- ---
+1 -1
View File
@@ -1,5 +1,5 @@
# Docker Compose configuration for LangBot # Docker Compose configuration for LangBot
# For Kubernetes deployment, see kubernetes.yaml and the deployment guide at https://docs.langbot.app # For Kubernetes deployment, see kubernetes.yaml and the deployment guide at https://langbot.app/docs
version: "3" version: "3"
services: services:
+1 -1
View File
@@ -1,7 +1,7 @@
# Kubernetes Deployment for LangBot # Kubernetes Deployment for LangBot
# This file provides Kubernetes deployment manifests for LangBot based on docker-compose.yaml # This file provides Kubernetes deployment manifests for LangBot based on docker-compose.yaml
# #
# Full deployment guide (zh/en/ja): https://docs.langbot.app -> Installation -> Kubernetes # Full deployment guide (zh/en/ja): https://langbot.app/docs -> Installation -> Kubernetes
# #
# Usage: # Usage:
# kubectl -n langbot create secret generic langbot-plugin-runtime-control \ # kubectl -n langbot create secret generic langbot-plugin-runtime-control \
+2 -2
View File
@@ -218,8 +218,8 @@ metadata:
spec: spec:
categories: [popular, global] categories: [popular, global]
help_links: help_links:
zh: https://docs.langbot.app/zh/platforms/http-bot zh: https://langbot.app/docs/zh/platforms/http-bot
en: https://docs.langbot.app/en/platforms/http-bot en: https://langbot.app/docs/en/platforms/http-bot
config: config:
- { name: inbound_secret, type: string, required: true, default: "" } - { name: inbound_secret, type: string, required: true, default: "" }
- { name: callback_url, type: string, required: false, default: "" } - { name: callback_url, type: string, required: false, default: "" }
+1 -1
View File
@@ -243,7 +243,7 @@ For large datasets:
- SeekDB GitHub: https://github.com/oceanbase/seekdb - SeekDB GitHub: https://github.com/oceanbase/seekdb
- pyseekdb SDK: https://github.com/oceanbase/pyseekdb - pyseekdb SDK: https://github.com/oceanbase/pyseekdb
- OceanBase Documentation: https://oceanbase.ai - OceanBase Documentation: https://oceanbase.ai
- LangBot Documentation: https://docs.langbot.app - LangBot Documentation: https://langbot.app/docs
## License ## License
+1 -1
View File
@@ -6,7 +6,7 @@ Minimal, dependency-light clients for the LangBot **HTTP Bot** platform adapter.
They show the whole loop: signing a request, pushing a message, and receiving They show the whole loop: signing a request, pushing a message, and receiving
multi-part replies on a callback endpoint. multi-part replies on a callback endpoint.
Full guide: [docs.langbot.app — HTTP Bot](https://docs.langbot.app/en/usage/platforms/http-bot). Full guide: [docs.langbot.app — HTTP Bot](https://langbot.app/docs/en/usage/platforms/http-bot).
Machine-readable contract: [`docs/http-bot-openapi.json`](../../docs/http-bot-openapi.json). Machine-readable contract: [`docs/http-bot-openapi.json`](../../docs/http-bot-openapi.json).
## Files ## Files
+1 -1
View File
@@ -6,7 +6,7 @@
它们完整展示了整条链路:对请求签名、推送一条消息、在回调端点接收 它们完整展示了整条链路:对请求签名、推送一条消息、在回调端点接收
1→M 的多段回复。 1→M 的多段回复。
完整指南:[docs.langbot.app —— HTTP Bot](https://docs.langbot.app/zh/usage/platforms/http-bot)。 完整指南:[docs.langbot.app —— HTTP Bot](https://langbot.app/docs/zh/usage/platforms/http-bot)。
机器可读的接口契约:[`docs/http-bot-openapi.json`](../../docs/http-bot-openapi.json)。 机器可读的接口契约:[`docs/http-bot-openapi.json`](../../docs/http-bot-openapi.json)。
## 文件清单 ## 文件清单
+1 -1
View File
@@ -6,7 +6,7 @@ A single self-contained HTML page that demos the LangBot **Page Bot**
(`web_page_bot`) embeddable chat widget — the one you drop onto any website with (`web_page_bot`) embeddable chat widget — the one you drop onto any website with
a single `<script>` tag. a single `<script>` tag.
Full guide: [docs.langbot.app — Page Bot](https://docs.langbot.app/en/usage/platforms/webpage). Full guide: [docs.langbot.app — Page Bot](https://langbot.app/docs/en/usage/platforms/webpage).
## Files ## Files
+1 -1
View File
@@ -6,7 +6,7 @@
(`web_page_bot`) 的可嵌入聊天组件 —— 也就是你用一行 `<script>` 标签就能放到任意 (`web_page_bot`) 的可嵌入聊天组件 —— 也就是你用一行 `<script>` 标签就能放到任意
网站上的那个组件。 网站上的那个组件。
完整指南:[docs.langbot.app —— 页面机器人](https://docs.langbot.app/zh/usage/platforms/webpage)。 完整指南:[docs.langbot.app —— 页面机器人](https://langbot.app/docs/zh/usage/platforms/webpage)。
## 文件清单 ## 文件清单
+3 -3
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "langbot" name = "langbot"
version = "4.10.9" version = "4.10.10"
description = "Production-grade platform for building agentic IM bots" description = "Production-grade platform for building agentic IM bots"
readme = "README.md" readme = "README.md"
license-files = ["LICENSE"] license-files = ["LICENSE"]
@@ -70,7 +70,7 @@ dependencies = [
"langchain-text-splitters>=1.1.2", "langchain-text-splitters>=1.1.2",
"chromadb>=1.0.0,<2.0.0", "chromadb>=1.0.0,<2.0.0",
"qdrant-client (>=1.15.1,<2.0.0)", "qdrant-client (>=1.15.1,<2.0.0)",
"langbot-plugin==0.5.6", "langbot-plugin==0.5.7",
"asyncpg>=0.30.0", "asyncpg>=0.30.0",
"line-bot-sdk>=3.19.0", "line-bot-sdk>=3.19.0",
"matrix-nio>=0.25.2", "matrix-nio>=0.25.2",
@@ -114,7 +114,7 @@ seekdb = [
[project.urls] [project.urls]
Homepage = "https://langbot.app" Homepage = "https://langbot.app"
Documentation = "https://docs.langbot.app" Documentation = "https://langbot.app/docs"
Repository = "https://github.com/langbot-app/LangBot" Repository = "https://github.com/langbot-app/LangBot"
[project.scripts] [project.scripts]
+1 -1
View File
@@ -48,7 +48,7 @@ tools, skill add/edit, and stdio MCP are disabled. Set `box.enabled: false`
## Kubernetes ## Kubernetes
See `docker/kubernetes.yaml` and the deployment guide at See `docker/kubernetes.yaml` and the deployment guide at
https://docs.langbot.app. `docker/deploy-k8s-test.sh` is a test helper. https://langbot.app/docs. `docker/deploy-k8s-test.sh` is a test helper.
## config.yaml (generated at `data/config.yaml` on first run) ## config.yaml (generated at `data/config.yaml` on first run)
+1 -1
View File
@@ -16,7 +16,7 @@ asciiart = r"""
|___/ |___/
Open Source 开源地址: https://github.com/langbot-app/LangBot Open Source 开源地址: https://github.com/langbot-app/LangBot
📖 Documentation 文档地址: https://docs.langbot.app 📖 Documentation 文档地址: https://langbot.app/docs
""" """
@@ -9,7 +9,6 @@ from .. import group
from .....entity.errors import account as account_errors from .....entity.errors import account as account_errors
from ...context import RequestContext from ...context import RequestContext
from .....cloud.launch import SpaceLaunchError from .....cloud.launch import SpaceLaunchError
from .....workspace.errors import WorkspaceNotFoundError
from ...service.user import ControlPlaneDirectoryRequiredError, PublicRegistrationClosedError from ...service.user import ControlPlaneDirectoryRequiredError, PublicRegistrationClosedError
@@ -187,6 +186,9 @@ class UserRouterGroup(group.RouterGroup):
json_data = await quart.request.json json_data = await quart.request.json
code = json_data.get('code') code = json_data.get('code')
state = json_data.get('state') state = json_data.get('state')
redirect_uri = json_data.get('redirect_uri') or (
quart.request.url_root.rstrip('/') + '/auth/space/callback'
)
launch_assertion = json_data.get('launch_assertion') launch_assertion = json_data.get('launch_assertion')
workspace_uuid = json_data.get('workspace_uuid') workspace_uuid = json_data.get('workspace_uuid')
@@ -200,8 +202,11 @@ class UserRouterGroup(group.RouterGroup):
return self.fail(1, 'Missing authorization code') return self.fail(1, 'Missing authorization code')
if not state: if not state:
return self.fail(1, 'Missing state parameter') return self.fail(1, 'Missing state parameter')
if not str(code).startswith('v4_'):
return self.fail(1, 'Unsupported Space OAuth code contract')
try: try:
redirect_uri = self._validate_space_redirect_uri(str(redirect_uri), bind=False)
consumed_state = await self.ap.user_service.consume_space_oauth_state_details(state, 'login') consumed_state = await self.ap.user_service.consume_space_oauth_state_details(state, 'login')
# Exchange code for tokens # Exchange code for tokens
launch_workspace_uuid = consumed_state.launch_workspace_uuid launch_workspace_uuid = consumed_state.launch_workspace_uuid
@@ -219,24 +224,36 @@ class UserRouterGroup(group.RouterGroup):
code, code,
workspace_uuids, workspace_uuids,
workspace_created_ats, workspace_created_ats,
redirect_uri=redirect_uri,
) )
access_token = token_data.get('access_token') access_token = token_data.get('access_token')
refresh_token = token_data.get('refresh_token') refresh_token = token_data.get('refresh_token')
expires_in = token_data.get('expires_in', 0) expires_in = token_data.get('expires_in', 0)
cloud_workspace_uuid = token_data.get('cloud_workspace_uuid')
if not access_token: if not access_token:
return self.fail(1, 'Failed to get access token from Space') return self.fail(1, 'Failed to get access token from Space')
# Authenticate and create/update local user cloud_mode = getattr(getattr(self.ap, 'deployment', None), 'mode', 'oss') == 'cloud'
if cloud_mode and launch_workspace_uuid and launch_workspace_uuid != cloud_workspace_uuid:
return self.fail(1, 'Space OAuth Workspace binding mismatch')
target_workspace_uuid = launch_workspace_uuid or cloud_workspace_uuid
if cloud_mode:
if not target_workspace_uuid:
return self.fail(1, 'Space OAuth response is missing the Cloud Workspace binding')
await self.ap.directory_projection_service.reconcile_workspaces((target_workspace_uuid,))
# Authenticate only after the signed, exact Workspace delta has
# established the Account and membership runtime shadow rows.
jwt_token, user_obj = await self.ap.user_service.authenticate_space_user( jwt_token, user_obj = await self.ap.user_service.authenticate_space_user(
access_token, refresh_token, expires_in access_token, refresh_token, expires_in
) )
if launch_workspace_uuid: if target_workspace_uuid:
try: try:
access = await self.ap.workspace_collaboration_service.resolve_account_workspace( access = await self.ap.workspace_collaboration_service.resolve_account_workspace(
user_obj.uuid, user_obj.uuid,
launch_workspace_uuid, target_workspace_uuid,
) )
except Exception: except Exception:
self.ap.logger.warning('Rejected Space OAuth launch for unauthorized Workspace') self.ap.logger.warning('Rejected Space OAuth launch for unauthorized Workspace')
@@ -368,12 +385,17 @@ class UserRouterGroup(group.RouterGroup):
json_data = await quart.request.json json_data = await quart.request.json
code = json_data.get('code') code = json_data.get('code')
state = json_data.get('state') state = json_data.get('state')
redirect_uri = json_data.get('redirect_uri') or (
quart.request.url_root.rstrip('/') + '/auth/space/callback?mode=bind'
)
if not code: if not code:
return self.http_status(400, -1, 'Missing authorization code') return self.http_status(400, -1, 'Missing authorization code')
if not state: if not state:
return self.http_status(400, -1, 'Missing state parameter') return self.http_status(400, -1, 'Missing state parameter')
if not str(code).startswith('v4_'):
return self.http_status(400, -1, 'Unsupported Space OAuth code contract')
try: try:
user_obj = await self.ap.user_service.consume_space_oauth_state(state, 'bind') user_obj = await self.ap.user_service.consume_space_oauth_state(state, 'bind')
@@ -386,7 +408,10 @@ class UserRouterGroup(group.RouterGroup):
return self.http_status(400, -1, 'Only local accounts can bind to Space') return self.http_status(400, -1, 'Only local accounts can bind to Space')
try: try:
updated_user = await self.ap.user_service.bind_space_account(user_obj.user, code) redirect_uri = self._validate_space_redirect_uri(str(redirect_uri), bind=True)
updated_user = await self.ap.user_service.bind_space_account(
user_obj.user, code, redirect_uri=redirect_uri
)
jwt_token = await self.ap.user_service.generate_jwt_token(updated_user) jwt_token = await self.ap.user_service.generate_jwt_token(updated_user)
return self.success( return self.success(
data={ data={
@@ -429,49 +454,18 @@ class UserRouterGroup(group.RouterGroup):
} }
) )
account = await self.ap.user_service.get_user_by_uuid(launch['account_uuid'])
projection_service = self.ap.directory_projection_service projection_service = self.ap.directory_projection_service
access = None if projection_service is None:
# A first Cloud launch creates the personal Workspace immediately raise SpaceLaunchError('Cloud directory projection is unavailable')
# before redirecting here. Pull a bounded number of signed event await projection_service.reconcile_workspaces((launch['workspace_uuid'],))
# pages until both the Account and its target Workspace membership account = await self.ap.user_service.get_user_by_uuid(launch['account_uuid'])
# are visible instead of rejecting during the background-sync window.
for attempt in range(4):
if account is not None:
self.ap.user_service._require_active_account(account)
try:
access = await self.ap.workspace_collaboration_service.resolve_account_workspace(
account.uuid,
launch['workspace_uuid'],
)
break
except WorkspaceNotFoundError:
if projection_service is None:
raise
elif projection_service is None:
break
if attempt == 3:
break
await projection_service.sync_once()
account = await self.ap.user_service.get_user_by_uuid(launch['account_uuid'])
if access is None and projection_service is not None:
# The target event may be deeper than the bounded incremental
# page budget. One authoritative signed snapshot catches this
# process up without turning the callback into unbounded polling.
await projection_service.refresh_snapshot()
account = await self.ap.user_service.get_user_by_uuid(launch['account_uuid'])
if account is not None:
self.ap.user_service._require_active_account(account)
access = await self.ap.workspace_collaboration_service.resolve_account_workspace(
account.uuid,
launch['workspace_uuid'],
)
if account is None: if account is None:
raise SpaceLaunchError('Launch Account is not projected into Core') raise SpaceLaunchError('Launch Account is not projected into Core')
if access is None: # pragma: no cover - bounded loop resolves or raises. self.ap.user_service._require_active_account(account)
raise SpaceLaunchError('Launch Workspace is not projected into Core') access = await self.ap.workspace_collaboration_service.resolve_account_workspace(
account.uuid,
launch['workspace_uuid'],
)
token = await self.ap.user_service.generate_jwt_token(account) token = await self.ap.user_service.generate_jwt_token(account)
return self.success( return self.success(
data={ data={
+10 -1
View File
@@ -137,7 +137,16 @@ class BotService:
bot = await self.get_bot(context, bot_data['uuid'], include_secret=True) bot = await self.get_bot(context, bot_data['uuid'], include_secret=True)
await self.ap.platform_mgr.load_bot(context, bot) try:
await self.ap.platform_mgr.load_bot(context, bot)
except Exception:
# The bot row was already inserted above; without this rollback a
# failing adapter constructor (e.g. a missing optional credential
# key) would leave a permanently disabled orphan bot in the DB.
await self.ap.persistence_mgr.execute_async(
sqlalchemy.delete(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_data['uuid'])
)
raise
return bot_data['uuid'] return bot_data['uuid']
+4 -1
View File
@@ -119,7 +119,7 @@ class SpaceService:
space_config = self._get_space_config() space_config = self._get_space_config()
authorize_url = space_config['oauth_authorize_url'] authorize_url = space_config['oauth_authorize_url']
params = {'redirect_uri': redirect_uri} params = {'redirect_uri': redirect_uri, 'code_contract': 'redirect-v1'}
if state: if state:
params['state'] = state params['state'] = state
return f'{authorize_url}?{urlencode(params)}' return f'{authorize_url}?{urlencode(params)}'
@@ -129,6 +129,8 @@ class SpaceService:
code: str, code: str,
workspace_uuids: list[str] | None = None, workspace_uuids: list[str] | None = None,
workspace_created_ats: dict[str, int] | None = None, workspace_created_ats: dict[str, int] | None = None,
*,
redirect_uri: str = '',
) -> typing.Dict: ) -> typing.Dict:
"""Exchange OAuth authorization code for tokens""" """Exchange OAuth authorization code for tokens"""
from langbot.pkg.utils import constants from langbot.pkg.utils import constants
@@ -141,6 +143,7 @@ class SpaceService:
f'{space_url}/api/v1/accounts/oauth/token', f'{space_url}/api/v1/accounts/oauth/token',
json={ json={
'code': code, 'code': code,
'redirect_uri': redirect_uri,
'instance_id': constants.instance_id, 'instance_id': constants.instance_id,
# Sending an explicit empty list tells new Space servers not to # Sending an explicit empty list tells new Space servers not to
# synthesize a legacy instance-derived Workspace binding. # synthesize a legacy instance-derived Workspace binding.
+3 -2
View File
@@ -774,7 +774,7 @@ class UserService:
f'email:{normalized_email}', f'email:{normalized_email}',
) )
async def bind_space_account(self, user_email: str, code: str) -> user.User: async def bind_space_account(self, user_email: str, code: str, *, redirect_uri: str = '') -> user.User:
"""Bind Space account to existing local account""" """Bind Space account to existing local account"""
local_account = await self.get_user_by_email(user_email) local_account = await self.get_user_by_email(user_email)
if local_account is None: if local_account is None:
@@ -794,12 +794,13 @@ class UserService:
code, code,
[binding.workspace_uuid], [binding.workspace_uuid],
{binding.workspace_uuid: created_ts}, {binding.workspace_uuid: created_ts},
redirect_uri=redirect_uri,
) )
else: else:
# Compatibility for early/bootstrap call sites that have not wired # Compatibility for early/bootstrap call sites that have not wired
# WorkspaceService yet; old Space servers still derive the legacy # WorkspaceService yet; old Space servers still derive the legacy
# Workspace identity from instance_id when the field is omitted. # Workspace identity from instance_id when the field is omitted.
token_data = await self.ap.space_service.exchange_oauth_code(code) token_data = await self.ap.space_service.exchange_oauth_code(code, redirect_uri=redirect_uri)
access_token = token_data.get('access_token') access_token = token_data.get('access_token')
refresh_token = token_data.get('refresh_token') refresh_token = token_data.get('refresh_token')
expires_in = token_data.get('expires_in', 0) expires_in = token_data.get('expires_in', 0)
+84 -1
View File
@@ -173,6 +173,77 @@ class DirectoryProjectionService:
async with self._sync_lock: async with self._sync_lock:
await self._sync_once() await self._sync_once()
async def reconcile_workspaces(self, workspace_uuids: Iterable[str]) -> None:
"""Synchronously project an exact Workspace set without moving the event cursor."""
requested = tuple(sorted({str(value).strip() for value in workspace_uuids if str(value).strip()}))
if not requested:
raise DirectoryProjectionUnavailableError('Targeted directory reconciliation requires a Workspace')
if len(requested) > self.event_limit:
raise DirectoryProjectionUnavailableError('Targeted directory reconciliation exceeds the batch limit')
async with self._sync_lock:
delta = await self.provider.fetch_workspaces(self.instance_uuid, requested)
await self._apply_targeted_delta(delta, requested)
async def _apply_targeted_delta(
self,
delta: DirectoryDelta,
requested_workspace_uuids: tuple[str, ...],
) -> None:
if not isinstance(delta, DirectoryDelta):
raise DirectoryProjectionUnavailableError('Directory provider returned an invalid delta')
workspace_count, membership_count = self._validate_batch_capacity(
delta.workspaces,
full_snapshot=False,
)
delta = DirectoryDelta.model_validate(delta.model_dump())
if delta.instance_uuid != self.instance_uuid:
raise DirectoryProjectionUnavailableError('Directory delta targets another LangBot instance')
requested = set(requested_workspace_uuids)
if set(delta.requested_workspace_uuids) != requested:
raise DirectoryProjectionUnavailableError('Directory delta does not match the requested Workspaces')
if {workspace.uuid for workspace in delta.workspaces} != requested:
raise DirectoryProjectionUnavailableError('Directory delta omitted a requested Workspace')
directory_uow = getattr(self.ap.persistence_mgr, 'directory_projection_uow', None)
if not callable(directory_uow):
raise DirectoryProjectionUnavailableError('Directory projection persistence scope is unavailable')
async with directory_uow(self.instance_uuid) as uow:
session = uow.session
state = await session.scalar(
sqlalchemy.select(DirectoryProjectionState)
.where(DirectoryProjectionState.instance_uuid == self.instance_uuid)
.with_for_update()
)
if state is None:
raise DirectoryProjectionUnavailableError('Directory projection is not initialized')
snapshot = DirectorySnapshot(
instance_uuid=self.instance_uuid,
cursor=state.cursor,
generated_at=delta.generated_at,
workspaces=delta.workspaces,
)
accounts_by_uuid = await self._apply_accounts(session, snapshot, preserve_existing=True)
await self._apply_workspaces(session, snapshot, accounts_by_uuid=accounts_by_uuid)
active_workspace_count = await self._enforce_active_workspace_capacity(session)
await session.flush()
await self._update_entitlement_workspace_activity(
snapshot.workspaces,
requested_workspace_uuids=requested,
)
self._publish_runtime_execution_projection(
snapshot.workspaces,
affected_workspace_uuids=requested,
)
self._request_model_catalog_sync()
self._record_batch_cardinality(
active_workspaces=active_workspace_count,
workspaces=workspace_count,
memberships=membership_count,
)
async def _sync_once(self) -> None: async def _sync_once(self) -> None:
cursor = self._consumer_cursor cursor = self._consumer_cursor
if cursor is None: if cursor is None:
@@ -723,7 +794,13 @@ class DirectoryProjectionService:
for row in inbox_rows: for row in inbox_rows:
row.applied_at = now row.applied_at = now
async def _apply_accounts(self, session: Any, snapshot: DirectorySnapshot) -> dict[str, User]: async def _apply_accounts(
self,
session: Any,
snapshot: DirectorySnapshot,
*,
preserve_existing: bool = False,
) -> dict[str, User]:
selected: dict[str, DirectoryMember] = {} selected: dict[str, DirectoryMember] = {}
emails: dict[str, str] = {} emails: dict[str, str] = {}
for workspace in snapshot.workspaces: for workspace in snapshot.workspaces:
@@ -788,6 +865,12 @@ class DirectoryProjectionService:
continue continue
if account.source != AccountSource.CLOUD_PROJECTION.value: if account.source != AccountSource.CLOUD_PROJECTION.value:
raise DirectoryProjectionUnavailableError('Directory account UUID collides with a local Core account') raise DirectoryProjectionUnavailableError('Directory account UUID collides with a local Core account')
if preserve_existing:
# A targeted Workspace fetch has no independently monotonic
# Account revision. It may create a missing runtime shadow, but
# ordered event/snapshot projection remains the only updater of
# existing Account identity and status fields.
continue
if account.projection_revision > snapshot.cursor: if account.projection_revision > snapshot.cursor:
raise DirectoryProjectionUnavailableError('Directory account revision rolled back') raise DirectoryProjectionUnavailableError('Directory account revision rolled back')
projected_account = self._account_projection(member) projected_account = self._account_projection(member)
+2 -2
View File
@@ -635,9 +635,9 @@ class Application:
frontend_path = paths.get_frontend_path() frontend_path = paths.get_frontend_path()
if not os.path.exists(frontend_path): if not os.path.exists(frontend_path):
self.logger.warning('WebUI 文件缺失,请根据文档部署:https://docs.langbot.app/zh') self.logger.warning('WebUI 文件缺失,请根据文档部署:https://langbot.app/docs/zh')
self.logger.warning( self.logger.warning(
'WebUI files are missing, please deploy according to the documentation: https://docs.langbot.app/en' 'WebUI files are missing, please deploy according to the documentation: https://langbot.app/docs/en'
) )
return return
@@ -15,9 +15,9 @@ spec:
categories: categories:
- protocol - protocol
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/aiocqhttp zh: https://langbot.app/docs/zh/usage/platforms/qq/aiocqhttp/napcat
en: https://link.langbot.app/en/platforms/aiocqhttp en: https://langbot.app/docs/en/usage/platforms/qq/aiocqhttp/napcat
ja: https://link.langbot.app/ja/platforms/aiocqhttp ja: https://langbot.app/docs/ja/usage/platforms/qq/aiocqhttp/napcat
config: config:
- name: host - name: host
label: label:
@@ -15,9 +15,9 @@ spec:
categories: categories:
- china - china
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/dingtalk zh: https://langbot.app/docs/zh/usage/platforms/dingtalk
en: https://link.langbot.app/en/platforms/dingtalk en: https://langbot.app/docs/en/usage/platforms/dingtalk
ja: https://link.langbot.app/ja/platforms/dingtalk ja: https://langbot.app/docs/ja/usage/platforms/dingtalk
config: config:
- name: one-click-create - name: one-click-create
label: label:
@@ -24,9 +24,9 @@ spec:
- popular - popular
- global - global
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/discord zh: https://langbot.app/docs/zh/usage/platforms/discord
en: https://link.langbot.app/en/platforms/discord en: https://langbot.app/docs/en/usage/platforms/discord
ja: https://link.langbot.app/ja/platforms/discord ja: https://langbot.app/docs/ja/usage/platforms/discord
config: config:
- name: client_id - name: client_id
label: label:
@@ -18,9 +18,9 @@ spec:
- popular - popular
- global - global
help_links: help_links:
zh: https://docs.langbot.app/zh/platforms/http-bot zh: https://langbot.app/docs/zh/platforms/http-bot
en: https://docs.langbot.app/en/platforms/http-bot en: https://langbot.app/docs/en/platforms/http-bot
ja: https://docs.langbot.app/ja/platforms/http-bot ja: https://langbot.app/docs/ja/platforms/http-bot
config: config:
- name: webhook_url - name: webhook_url
label: label:
+3 -3
View File
@@ -15,9 +15,9 @@ spec:
categories: categories:
- china - china
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/kook zh: https://langbot.app/docs/zh/usage/platforms/kook
en: https://link.langbot.app/en/platforms/kook en: https://langbot.app/docs/en/usage/platforms/kook
ja: https://link.langbot.app/ja/platforms/kook ja: https://langbot.app/docs/ja/usage/platforms/kook
config: config:
- name: token - name: token
label: label:
+3 -3
View File
@@ -19,9 +19,9 @@ spec:
- china - china
- global - global
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/lark zh: https://langbot.app/docs/zh/usage/platforms/lark
en: https://link.langbot.app/en/platforms/lark en: https://langbot.app/docs/en/usage/platforms/lark
ja: https://link.langbot.app/ja/platforms/lark ja: https://langbot.app/docs/ja/usage/platforms/lark
config: config:
- name: domain - name: domain
label: label:
+3 -3
View File
@@ -22,9 +22,9 @@ spec:
categories: categories:
- global - global
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/line zh: https://langbot.app/docs/zh/usage/platforms/line
en: https://link.langbot.app/en/platforms/line en: https://langbot.app/docs/en/usage/platforms/line
ja: https://link.langbot.app/ja/platforms/line ja: https://langbot.app/docs/ja/usage/platforms/line
config: config:
- name: webhook_url - name: webhook_url
label: label:
@@ -15,9 +15,9 @@ spec:
categories: categories:
- china - china
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/officialaccount zh: https://langbot.app/docs/zh/usage/platforms/wxoa
en: https://link.langbot.app/en/platforms/officialaccount en: https://langbot.app/docs/en/usage/platforms/wxoa
ja: https://link.langbot.app/ja/platforms/officialaccount ja: https://langbot.app/docs/ja/usage/platforms/wxoa
config: config:
- name: webhook_url - name: webhook_url
label: label:
@@ -16,9 +16,9 @@ spec:
- popular - popular
- china - china
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/openclaw_weixin zh: https://langbot.app/docs/zh/usage/platforms/wechat/weixin
en: https://link.langbot.app/en/platforms/openclaw_weixin en: https://langbot.app/docs/en/usage/platforms/readme
ja: https://link.langbot.app/ja/platforms/openclaw_weixin ja: https://langbot.app/docs/ja/usage/platforms/readme
config: config:
- name: base_url - name: base_url
label: label:
@@ -205,7 +205,7 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter
bot = QQOfficialClient( bot = QQOfficialClient(
app_id=config['appid'], app_id=config['appid'],
secret=config['secret'], secret=config['secret'],
token=config['token'], token=config.get('token', ''),
logger=logger, logger=logger,
unified_mode=enable_webhook, unified_mode=enable_webhook,
) )
@@ -15,9 +15,9 @@ spec:
categories: categories:
- china - china
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/qqofficial zh: https://langbot.app/docs/zh/usage/platforms/qq/official_webhook
en: https://link.langbot.app/en/platforms/qqofficial en: https://langbot.app/docs/en/usage/platforms/qq/official_webhook
ja: https://link.langbot.app/ja/platforms/qqofficial ja: https://langbot.app/docs/ja/usage/platforms/qq/official_webhook
config: config:
- name: __system.outbound_ips - name: __system.outbound_ips
label: label:
+3 -3
View File
@@ -21,9 +21,9 @@ spec:
categories: categories:
- protocol - protocol
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/satori zh: https://langbot.app/docs/zh/usage/platforms/readme
en: https://link.langbot.app/en/platforms/satori en: https://langbot.app/docs/en/usage/platforms/readme
ja: https://link.langbot.app/ja/platforms/satori ja: https://langbot.app/docs/ja/usage/platforms/readme
config: config:
- name: platform - name: platform
label: label:
+3 -3
View File
@@ -24,9 +24,9 @@ spec:
- popular - popular
- global - global
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/slack zh: https://langbot.app/docs/zh/usage/platforms/slack
en: https://link.langbot.app/en/platforms/slack en: https://langbot.app/docs/en/usage/platforms/slack
ja: https://link.langbot.app/ja/platforms/slack ja: https://langbot.app/docs/ja/usage/platforms/slack
config: config:
- name: webhook_url - name: webhook_url
label: label:
@@ -24,9 +24,9 @@ spec:
- popular - popular
- global - global
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/telegram zh: https://langbot.app/docs/zh/usage/platforms/telegram
en: https://link.langbot.app/en/platforms/telegram en: https://langbot.app/docs/en/usage/platforms/telegram
ja: https://link.langbot.app/ja/platforms/telegram ja: https://langbot.app/docs/ja/usage/platforms/telegram
config: config:
- name: token - name: token
label: label:
@@ -15,9 +15,9 @@ spec:
categories: categories:
- china - china
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/wechatpad zh: https://langbot.app/docs/zh/usage/platforms/wechat/wechatpad
en: https://link.langbot.app/en/platforms/wechatpad en: https://langbot.app/docs/en/usage/platforms/readme
ja: https://link.langbot.app/ja/platforms/wechatpad ja: https://langbot.app/docs/ja/usage/platforms/readme
config: config:
- name: wechatpad_url - name: wechatpad_url
label: label:
+3 -3
View File
@@ -274,11 +274,11 @@ class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
if content['type'] == 'text': if content['type'] == 'text':
await self.bot.send_private_msg(user_id, agent_id, content['content']) await self.bot.send_private_msg(user_id, agent_id, content['content'])
if content['type'] == 'image': if content['type'] == 'image':
await self.bot.send_image(user_id, agent_id, content['media']) await self.bot.send_image(user_id, agent_id, content['media_id'])
if content['type'] == 'voice': if content['type'] == 'voice':
await self.bot.send_voice(user_id, agent_id, content['media']) await self.bot.send_voice(user_id, agent_id, content['media_id'])
if content['type'] == 'file': if content['type'] == 'file':
await self.bot.send_file(user_id, agent_id, content['media']) await self.bot.send_file(user_id, agent_id, content['media_id'])
def register_listener( def register_listener(
self, self,
+3 -3
View File
@@ -16,9 +16,9 @@ spec:
- popular - popular
- china - china
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/wecom zh: https://langbot.app/docs/zh/usage/platforms/wecom/wecom
en: https://link.langbot.app/en/platforms/wecom en: https://langbot.app/docs/en/usage/platforms/wecom/wecom
ja: https://link.langbot.app/ja/platforms/wecom ja: https://langbot.app/docs/ja/usage/platforms/wecom/wecom
config: config:
- name: webhook_url - name: webhook_url
label: label:
@@ -15,9 +15,9 @@ spec:
categories: categories:
- china - china
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/wecombot zh: https://langbot.app/docs/zh/usage/platforms/wecom/wecombot
en: https://link.langbot.app/en/platforms/wecombot en: https://langbot.app/docs/en/usage/platforms/wecom/wecombot
ja: https://link.langbot.app/ja/platforms/wecombot ja: https://langbot.app/docs/ja/usage/platforms/wecom/wecombot
config: config:
- name: one-click-create - name: one-click-create
label: label:
@@ -15,9 +15,9 @@ spec:
categories: categories:
- china - china
help_links: help_links:
zh: https://link.langbot.app/zh/platforms/wecomcs zh: https://langbot.app/docs/zh/usage/platforms/wecom/wecomcs
en: https://link.langbot.app/en/platforms/wecomcs en: https://langbot.app/docs/en/usage/platforms/wecom/wecomcs
ja: https://link.langbot.app/ja/platforms/wecomcs ja: https://langbot.app/docs/ja/usage/platforms/wecom/wecomcs
config: config:
- name: webhook_url - name: webhook_url
label: label:
+13 -1
View File
@@ -39,6 +39,9 @@ class N8nServiceAPIRunner(runner.RequestRunner):
# 获取输出键名,默认为response # 获取输出键名,默认为response
self.output_key = self.pipeline_config['ai']['n8n-service-api'].get('output-key', 'response') self.output_key = self.pipeline_config['ai']['n8n-service-api'].get('output-key', 'response')
self.response_handling = self.pipeline_config['ai']['n8n-service-api'].get('response-handling', 'reply')
if self.response_handling not in {'reply', 'ignore'}:
raise ValueError(f'Invalid n8n response-handling: {self.response_handling}')
# 获取认证类型,默认为none # 获取认证类型,默认为none
self.auth_type = self.pipeline_config['ai']['n8n-service-api'].get('auth-type', 'none') self.auth_type = self.pipeline_config['ai']['n8n-service-api'].get('auth-type', 'none')
@@ -262,7 +265,11 @@ class N8nServiceAPIRunner(runner.RequestRunner):
async with session.post( async with session.post(
self.webhook_url, json=payload, headers=headers, auth=auth, timeout=self.timeout self.webhook_url, json=payload, headers=headers, auth=auth, timeout=self.timeout
) as response: ) as response:
if response.status != 200: if self.response_handling == 'ignore':
status_ok = 200 <= response.status < 300
else:
status_ok = response.status == 200
if not status_ok:
error_text = ( error_text = (
await httpclient.read_limited( await httpclient.read_limited(
response, response,
@@ -272,6 +279,11 @@ class N8nServiceAPIRunner(runner.RequestRunner):
self.ap.logger.error(f'n8n webhook call failed: {response.status}, {error_text}') self.ap.logger.error(f'n8n webhook call failed: {response.status}, {error_text}')
raise Exception(f'n8n webhook call failed: {response.status}, {error_text}') raise Exception(f'n8n webhook call failed: {response.status}, {error_text}')
if self.response_handling == 'ignore':
response.release()
self.ap.logger.debug('n8n async webhook accepted; response body ignored')
return
async for chunk in self._process_response(response): async for chunk in self._process_response(response):
if is_stream: if is_stream:
yield chunk yield chunk
+1 -1
View File
@@ -83,7 +83,7 @@ class VersionManager:
try: try:
if await self.is_new_version_available(): if await self.is_new_version_available():
return ( return (
'New version available. Update guide: https://link.langbot.app/en/docs/update', 'New version available. Update guide: https://langbot.app/docs/en/deploy/update',
logging.INFO, logging.INFO,
) )
except Exception as e: except Exception as e:
@@ -269,7 +269,7 @@ class InvitationDeliveryService:
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="width:100%;max-width:600px;"> <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="width:100%;max-width:600px;">
<tr> <tr>
<td style="padding:0 4px 20px;"> <td style="padding:0 4px 20px;">
<img src="https://docs.langbot.app/langbot-logo.png" alt="LangBot" width="34" height="34" style="display:inline-block;width:34px;height:34px;border:0;vertical-align:middle;"> <img src="https://langbot.app/docs/langbot-logo.png" alt="LangBot" width="34" height="34" style="display:inline-block;width:34px;height:34px;border:0;vertical-align:middle;">
<span style="display:inline-block;margin-left:10px;vertical-align:middle;font-size:18px;font-weight:700;letter-spacing:-.01em;">LangBot</span> <span style="display:inline-block;margin-left:10px;vertical-align:middle;font-size:18px;font-weight:700;letter-spacing:-.01em;">LangBot</span>
</td> </td>
</tr> </tr>
@@ -80,7 +80,8 @@
"header-name": "", "header-name": "",
"header-value": "", "header-value": "",
"timeout": 120, "timeout": 120,
"output-key": "response" "output-key": "response",
"response-handling": "reply"
}, },
"langflow-api": { "langflow-api": {
"base-url": "http://localhost:7860", "base-url": "http://localhost:7860",
@@ -475,6 +475,25 @@ stages:
type: string type: string
required: false required: false
default: 'response' default: 'response'
- name: response-handling
label:
en_US: Webhook Response Handling
zh_Hans: Webhook 响应处理方式
description:
en_US: Choose whether LangBot forwards the n8n webhook response to the chat user. Ignore mode requires the n8n Webhook node to use Respond Immediately.
zh_Hans: 选择是否将 n8n Webhook 响应转发给聊天用户。忽略模式要求 n8n Webhook 节点使用“立即响应”。
type: select
required: false
default: 'reply'
options:
- name: reply
label:
en_US: Forward as chat reply
zh_Hans: 转发为聊天回复
- name: ignore
label:
en_US: Ignore response body (asynchronous workflow)
zh_Hans: 忽略响应正文(异步工作流)
- name: coze-api - name: coze-api
label: label:
en_US: coze API en_US: coze API
+156 -71
View File
@@ -11,7 +11,6 @@ import pytest
import quart import quart
from langbot.pkg.api.http.controller.groups.user import UserRouterGroup from langbot.pkg.api.http.controller.groups.user import UserRouterGroup
from langbot.pkg.workspace.errors import WorkspaceNotFoundError
pytestmark = pytest.mark.integration pytestmark = pytest.mark.integration
@@ -71,7 +70,6 @@ async def space_oauth_api():
application.space_service.get_oauth_authorize_url = Mock( application.space_service.get_oauth_authorize_url = Mock(
side_effect=lambda redirect_uri, state: f'https://space.example/authorize?state={state}' side_effect=lambda redirect_uri, state: f'https://space.example/authorize?state={state}'
) )
application.space_service.exchange_oauth_code = AsyncMock( application.space_service.exchange_oauth_code = AsyncMock(
return_value={ return_value={
'access_token': 'space-access-token', 'access_token': 'space-access-token',
@@ -129,7 +127,7 @@ async def test_cloud_launch_state_is_server_issued_and_workspace_bound(space_oau
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_cloud_login_entry_starts_stateful_space_oauth(space_oauth_api): async def test_cloud_login_entry_uses_normal_stateful_oauth(space_oauth_api):
application, client = space_oauth_api application, client = space_oauth_api
application.deployment.mode = 'cloud' application.deployment.mode = 'cloud'
@@ -272,10 +270,14 @@ async def test_server_side_webhook_origin_supports_bundled_ui(space_oauth_api):
async def test_login_callback_requires_and_consumes_server_state(space_oauth_api): async def test_login_callback_requires_and_consumes_server_state(space_oauth_api):
application, client = space_oauth_api application, client = space_oauth_api
missing = await client.post('/api/v1/user/space/callback', json={'code': 'oauth-code'}) missing = await client.post('/api/v1/user/space/callback', json={'code': 'v4_oauth-code'})
response = await client.post( response = await client.post(
'/api/v1/user/space/callback', '/api/v1/user/space/callback',
json={'code': 'oauth-code', 'state': 'opaque-login-state'}, json={
'code': 'v4_oauth-code',
'state': 'opaque-login-state',
'redirect_uri': 'https://oss.example/auth/space/callback',
},
) )
assert (await missing.get_json())['code'] == 1 assert (await missing.get_json())['code'] == 1
@@ -283,12 +285,146 @@ async def test_login_callback_requires_and_consumes_server_state(space_oauth_api
assert (await response.get_json())['data']['token'] == 'space-login-token' assert (await response.get_json())['data']['token'] == 'space-login-token'
application.user_service.consume_space_oauth_state_details.assert_awaited_once_with('opaque-login-state', 'login') application.user_service.consume_space_oauth_state_details.assert_awaited_once_with('opaque-login-state', 'login')
application.space_service.exchange_oauth_code.assert_awaited_once_with( application.space_service.exchange_oauth_code.assert_awaited_once_with(
'oauth-code', 'v4_oauth-code',
[WORKSPACE_UUID], [WORKSPACE_UUID],
{WORKSPACE_UUID: int(WORKSPACE_CREATED_AT.timestamp())}, {WORKSPACE_UUID: int(WORKSPACE_CREATED_AT.timestamp())},
redirect_uri='https://oss.example/auth/space/callback',
) )
@pytest.mark.asyncio
async def test_login_callback_rejects_downgraded_legacy_code(space_oauth_api):
application, client = space_oauth_api
response = await client.post(
'/api/v1/user/space/callback',
json={'code': 'v2_legacy-code', 'state': 'opaque-login-state'},
)
payload = await response.get_json()
assert response.status_code == 200
assert payload['code'] == 1
assert 'code contract' in payload['msg']
application.space_service.exchange_oauth_code.assert_not_awaited()
@pytest.mark.asyncio
async def test_cloud_login_callback_reconciles_authorized_workspace_before_local_authentication(space_oauth_api):
application, client = space_oauth_api
application.deployment.mode = 'cloud'
calls: list[str] = []
application.directory_projection_service = SimpleNamespace(
reconcile_workspaces=AsyncMock(side_effect=lambda _workspace_uuids: calls.append('reconcile'))
)
application.space_service.exchange_oauth_code.return_value = {
'access_token': 'space-access-token',
'refresh_token': 'space-refresh-token',
'expires_in': 3600,
'cloud_workspace_uuid': WORKSPACE_UUID,
}
authenticated_account = application.user_service.authenticate_space_user.return_value[1]
async def authenticate(*_args):
calls.append('authenticate')
return 'space-login-token', authenticated_account
application.user_service.authenticate_space_user.side_effect = authenticate
response = await client.post(
'/api/v1/user/space/callback',
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
)
assert response.status_code == 200
assert (await response.get_json())['data']['workspace_uuid'] == WORKSPACE_UUID
assert calls == ['reconcile', 'authenticate']
application.directory_projection_service.reconcile_workspaces.assert_awaited_once_with((WORKSPACE_UUID,))
@pytest.mark.asyncio
async def test_cloud_login_callback_fails_closed_without_workspace_binding(space_oauth_api):
application, client = space_oauth_api
application.deployment.mode = 'cloud'
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
response = await client.post(
'/api/v1/user/space/callback',
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
)
payload = await response.get_json()
assert response.status_code == 200
assert payload['code'] == 1
assert 'Cloud Workspace binding' in payload['msg']
application.directory_projection_service.reconcile_workspaces.assert_not_awaited()
application.user_service.authenticate_space_user.assert_not_awaited()
@pytest.mark.asyncio
async def test_cloud_login_callback_requires_code_binding_for_launch_state(space_oauth_api):
application, client = space_oauth_api
application.deployment.mode = 'cloud'
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
application.user_service.consume_space_oauth_state_details.return_value = SimpleNamespace(
launch_workspace_uuid=WORKSPACE_UUID
)
response = await client.post(
'/api/v1/user/space/callback',
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
)
payload = await response.get_json()
assert response.status_code == 200
assert payload['code'] == 1
assert 'Workspace binding' in payload['msg']
application.directory_projection_service.reconcile_workspaces.assert_not_awaited()
application.user_service.authenticate_space_user.assert_not_awaited()
@pytest.mark.asyncio
async def test_cloud_login_callback_rejects_conflicting_state_and_code_workspace_bindings(space_oauth_api):
application, client = space_oauth_api
application.deployment.mode = 'cloud'
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
application.user_service.consume_space_oauth_state_details.return_value = SimpleNamespace(
launch_workspace_uuid=WORKSPACE_UUID
)
application.space_service.exchange_oauth_code.return_value = {
'access_token': 'space-access-token',
'refresh_token': 'space-refresh-token',
'expires_in': 3600,
'cloud_workspace_uuid': 'workspace-from-another-flow',
}
response = await client.post(
'/api/v1/user/space/callback',
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
)
payload = await response.get_json()
assert response.status_code == 200
assert payload['code'] == 1
assert 'Workspace binding' in payload['msg']
application.directory_projection_service.reconcile_workspaces.assert_not_awaited()
application.user_service.authenticate_space_user.assert_not_awaited()
@pytest.mark.asyncio
async def test_oss_login_callback_does_not_request_cloud_reconciliation(space_oauth_api):
application, client = space_oauth_api
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
response = await client.post(
'/api/v1/user/space/callback',
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
)
assert response.status_code == 200
application.directory_projection_service.reconcile_workspaces.assert_not_awaited()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_login_callback_launch_state_selects_asserted_workspace(space_oauth_api): async def test_login_callback_launch_state_selects_asserted_workspace(space_oauth_api):
application, client = space_oauth_api application, client = space_oauth_api
@@ -299,7 +435,7 @@ async def test_login_callback_launch_state_selects_asserted_workspace(space_oaut
response = await client.post( response = await client.post(
'/api/v1/user/space/callback', '/api/v1/user/space/callback',
json={'code': 'oauth-code', 'state': 'opaque-login-state'}, json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
) )
assert response.status_code == 200 assert response.status_code == 200
@@ -398,18 +534,22 @@ async def test_bind_callback_uses_opaque_state_and_never_treats_it_as_jwt(space_
rejected = await client.post( rejected = await client.post(
'/api/v1/user/bind-space', '/api/v1/user/bind-space',
json={'code': 'attacker-code', 'state': 'jwt.must-not-be-used'}, json={'code': 'v4_attacker-code', 'state': 'jwt.must-not-be-used'},
) )
response = await client.post( response = await client.post(
'/api/v1/user/bind-space', '/api/v1/user/bind-space',
json={'code': 'oauth-code', 'state': 'opaque-bind-state'}, json={'code': 'v4_oauth-code', 'state': 'opaque-bind-state'},
) )
assert rejected.status_code == 401 assert rejected.status_code == 401
assert response.status_code == 200 assert response.status_code == 200
assert (await response.get_json())['data']['token'] == 'rotated-account-token' assert (await response.get_json())['data']['token'] == 'rotated-account-token'
application.user_service.verify_jwt_token.assert_not_awaited() application.user_service.verify_jwt_token.assert_not_awaited()
application.user_service.bind_space_account.assert_awaited_once_with('owner@example.com', 'oauth-code') application.user_service.bind_space_account.assert_awaited_once_with(
'owner@example.com',
'v4_oauth-code',
redirect_uri='http://localhost/auth/space/callback?mode=bind',
)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -417,6 +557,7 @@ async def test_direct_launch_assertion_does_not_consume_normal_oauth_state(space
application, client = space_oauth_api application, client = space_oauth_api
application.user_service.consume_space_oauth_state.reset_mock() application.user_service.consume_space_oauth_state.reset_mock()
application.space_service.exchange_oauth_code.reset_mock() application.space_service.exchange_oauth_code.reset_mock()
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
response = await client.post( response = await client.post(
'/api/v1/user/space/callback', '/api/v1/user/space/callback',
@@ -440,7 +581,7 @@ async def test_direct_launch_assertion_does_not_consume_normal_oauth_state(space
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_direct_launch_refreshes_new_workspace_projection_before_rejecting_account(space_oauth_api): async def test_direct_launch_reconciles_exact_workspace_before_resolving_access(space_oauth_api):
application, client = space_oauth_api application, client = space_oauth_api
projected_account = SimpleNamespace( projected_account = SimpleNamespace(
uuid='account-a', uuid='account-a',
@@ -448,8 +589,8 @@ async def test_direct_launch_refreshes_new_workspace_projection_before_rejecting
account_type='space', account_type='space',
status='active', status='active',
) )
application.user_service.get_user_by_uuid = AsyncMock(side_effect=[None, None, projected_account]) application.user_service.get_user_by_uuid = AsyncMock(return_value=projected_account)
application.directory_projection_service = SimpleNamespace(sync_once=AsyncMock()) application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
response = await client.post( response = await client.post(
'/api/v1/user/space/callback', '/api/v1/user/space/callback',
@@ -461,61 +602,5 @@ async def test_direct_launch_refreshes_new_workspace_projection_before_rejecting
assert response.status_code == 200 assert response.status_code == 200
assert (await response.get_json())['data']['workspace_uuid'] == WORKSPACE_UUID assert (await response.get_json())['data']['workspace_uuid'] == WORKSPACE_UUID
assert application.directory_projection_service.sync_once.await_count == 2 application.directory_projection_service.reconcile_workspaces.assert_awaited_once_with((WORKSPACE_UUID,))
assert application.user_service.get_user_by_uuid.await_count == 3 application.user_service.get_user_by_uuid.assert_awaited_once_with('account-a')
@pytest.mark.asyncio
async def test_direct_launch_refreshes_projection_when_account_exists_before_workspace(space_oauth_api):
application, client = space_oauth_api
projected_access = application.workspace_collaboration_service.resolve_account_workspace.return_value
application.workspace_collaboration_service.resolve_account_workspace = AsyncMock(
side_effect=[WorkspaceNotFoundError('Workspace not found'), projected_access]
)
application.directory_projection_service = SimpleNamespace(sync_once=AsyncMock())
response = await client.post(
'/api/v1/user/space/callback',
json={
'workspace_uuid': WORKSPACE_UUID,
'launch_assertion': 'signed-launch-token',
},
)
assert response.status_code == 200
assert (await response.get_json())['data']['workspace_uuid'] == WORKSPACE_UUID
application.directory_projection_service.sync_once.assert_awaited_once_with()
assert application.workspace_collaboration_service.resolve_account_workspace.await_count == 2
@pytest.mark.asyncio
async def test_direct_launch_falls_back_to_snapshot_when_event_backlog_exceeds_page_budget(space_oauth_api):
application, client = space_oauth_api
projected_access = application.workspace_collaboration_service.resolve_account_workspace.return_value
application.workspace_collaboration_service.resolve_account_workspace = AsyncMock(
side_effect=[
WorkspaceNotFoundError('Workspace not found'),
WorkspaceNotFoundError('Workspace not found'),
WorkspaceNotFoundError('Workspace not found'),
WorkspaceNotFoundError('Workspace not found'),
projected_access,
]
)
application.directory_projection_service = SimpleNamespace(
sync_once=AsyncMock(),
refresh_snapshot=AsyncMock(),
)
response = await client.post(
'/api/v1/user/space/callback',
json={
'workspace_uuid': WORKSPACE_UUID,
'launch_assertion': 'signed-launch-token',
},
)
assert response.status_code == 200
assert (await response.get_json())['data']['workspace_uuid'] == WORKSPACE_UUID
assert application.directory_projection_service.sync_once.await_count == 3
application.directory_projection_service.refresh_snapshot.assert_awaited_once_with()
assert application.workspace_collaboration_service.resolve_account_workspace.await_count == 5
@@ -12,6 +12,7 @@ import pytest
from unittest.mock import AsyncMock, MagicMock, Mock, patch from unittest.mock import AsyncMock, MagicMock, Mock, patch
from types import SimpleNamespace from types import SimpleNamespace
import json import json
import sqlalchemy
import uuid import uuid
from langbot.pkg.api.http.service.bot import BotService from langbot.pkg.api.http.service.bot import BotService
@@ -449,10 +450,58 @@ class TestBotServiceCreateBot:
insert_statement = ap.persistence_mgr.execute_async.await_args_list[1].args[0] insert_statement = ap.persistence_mgr.execute_async.await_args_list[1].args[0]
insert_values = insert_statement.compile().params insert_values = insert_statement.compile().params
assert insert_values['workspace_uuid'] == WORKSPACE_UUID assert insert_values['workspace_uuid'] == WORKSPACE_UUID
assert insert_values['use_pipeline_uuid'] == 'default-pipeline-uuid'
assert insert_values['use_pipeline_name'] == 'Default Pipeline'
assert bot_uuid is not None # Verify UUID was returned assert bot_uuid is not None # Verify UUID was returned
async def test_create_bot_rolls_back_insert_when_load_bot_fails(self):
"""Deletes the inserted row when the adapter fails to load.
Regression: a failing adapter constructor (e.g. KeyError on a missing
optional credential key) used to leave a permanently disabled orphan
bot in the DB the insert was already committed and the HTTP layer
surfaced a 500 without any cleanup.
"""
# Setup
ap = SimpleNamespace()
ap.persistence_mgr = SimpleNamespace()
ap.instance_config = SimpleNamespace()
ap.instance_config.data = {'system': {'limitation': {'max_bots': -1}}}
ap.platform_mgr = SimpleNamespace()
ap.platform_mgr.load_bot = AsyncMock(side_effect=KeyError('token'))
pipeline_result = Mock()
pipeline_result.first = Mock(return_value=None)
bot_result = Mock()
bot_result.first = Mock(return_value=_create_mock_bot())
executed_statements = []
async def mock_execute(query):
executed_statements.append(query)
if len(executed_statements) <= 2:
return pipeline_result # 1: limitation bots query, 2: pipeline query
if len(executed_statements) == 3:
return Mock() # insert
return bot_result # get_bot after insert
ap.persistence_mgr.execute_async = AsyncMock(side_effect=mock_execute)
ap.persistence_mgr.serialize_model = Mock(return_value={'uuid': 'new-uuid', 'name': 'New Bot'})
service = BotService(ap)
# Execute & Verify: the adapter error propagates
with pytest.raises(KeyError, match='token'):
await service.create_bot(
WORKSPACE_UUID, {'name': 'New Bot', 'adapter': 'telegram', 'adapter_config': {}}
)
# And the inserted row is rolled back via a DELETE on the new uuid
# (no limitation query runs because max_bots=-1)
assert len(executed_statements) == 4 # pipeline select, insert, bot select, delete
delete_statement = executed_statements[-1]
assert isinstance(delete_statement, sqlalchemy.sql.dml.Delete)
compiled = delete_statement.compile()
assert compiled.params['uuid_1'] is not None
class TestBotServiceUpdateBot: class TestBotServiceUpdateBot:
"""Tests for update_bot method.""" """Tests for update_bot method."""
@@ -95,7 +95,9 @@ class TestSpaceServiceGetOAuthAuthorizeUrl:
result = service.get_oauth_authorize_url('http://localhost/callback') result = service.get_oauth_authorize_url('http://localhost/callback')
# Verify # Verify
assert parse_qs(urlsplit(result).query)['redirect_uri'] == ['http://localhost/callback'] query = parse_qs(urlsplit(result).query)
assert query['redirect_uri'] == ['http://localhost/callback']
assert query['code_contract'] == ['redirect-v1']
assert 'https://space.langbot.app/auth/authorize' in result assert 'https://space.langbot.app/auth/authorize' in result
def test_get_oauth_authorize_url_with_state(self): def test_get_oauth_authorize_url_with_state(self):
@@ -578,12 +580,14 @@ class TestSpaceServiceExchangeOAuthCode:
'auth_code', 'auth_code',
['workspace-1'], ['workspace-1'],
{'workspace-1': 1_700_000_000}, {'workspace-1': 1_700_000_000},
redirect_uri='https://oss.example/auth/space/callback',
) )
# Verify # Verify
assert result['access_token'] == 'new_access_token' assert result['access_token'] == 'new_access_token'
assert mock_session_obj.post.call_args.kwargs['json'] == { assert mock_session_obj.post.call_args.kwargs['json'] == {
'code': 'auth_code', 'code': 'auth_code',
'redirect_uri': 'https://oss.example/auth/space/callback',
'instance_id': constants.instance_id, 'instance_id': constants.instance_id,
'workspace_uuids': ['workspace-1'], 'workspace_uuids': ['workspace-1'],
'workspace_created_ats': {'workspace-1': 1_700_000_000}, 'workspace_created_ats': {'workspace-1': 1_700_000_000},
@@ -846,10 +850,7 @@ class TestSpaceServiceGetModelSelection:
if response_shape == 'models-envelope': if response_shape == 'models-envelope':
data = {'models': models} data = {'models': models}
elif response_shape == 'availability-wrapper': elif response_shape == 'availability-wrapper':
data = [ data = [{'model': model, 'latency_ms': index + 10, 'http_code': 200} for index, model in enumerate(models)]
{'model': model, 'latency_ms': index + 10, 'http_code': 200}
for index, model in enumerate(models)
]
else: else:
data = models data = models
payload = {'code': 0, 'data': data} payload = {'code': 0, 'data': data}
@@ -4,7 +4,7 @@ import asyncio
import datetime import datetime
import logging import logging
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import Mock from unittest.mock import AsyncMock, Mock
import pytest import pytest
import sqlalchemy import sqlalchemy
@@ -215,6 +215,88 @@ async def test_directory_delta_requests_model_catalog_sync_after_commit(projecti
request_sync.assert_called_once_with() request_sync.assert_called_once_with()
async def test_targeted_reconciliation_projects_new_workspace_without_advancing_event_cursor(projection_context):
application, session_factory = projection_context
provider = _Provider(
[_snapshot(7, workspaces=[])],
deltas=[_delta(workspaces=[_workspace(revision=8, name='JIT Workspace')])],
)
service = DirectoryProjectionService(application, provider, INSTANCE_UUID)
await service.initialize()
await service.reconcile_workspaces((WORKSPACE_UUID,))
async with session_factory() as session:
account = await session.scalar(sqlalchemy.select(User).where(User.uuid == ACCOUNT_UUID))
workspace = await session.get(Workspace, WORKSPACE_UUID)
membership = await session.scalar(
sqlalchemy.select(WorkspaceMembership).where(
WorkspaceMembership.workspace_uuid == WORKSPACE_UUID,
WorkspaceMembership.account_uuid == ACCOUNT_UUID,
)
)
state = await session.get(DirectoryProjectionState, INSTANCE_UUID)
assert account is not None
assert workspace is not None and workspace.name == 'JIT Workspace'
assert membership is not None and membership.status == 'active'
assert state is not None and state.cursor == 7
assert provider.delta_calls == 1
assert provider.after_cursors == []
async def test_targeted_reconciliation_preserves_existing_account_until_ordered_event_projection(projection_context):
application, session_factory = projection_context
targeted_workspace = _workspace(revision=8, name='Renamed Workspace').model_copy(
update={
'members': [
_member(revision=8).model_copy(update={'display_name': 'Changed Account Name'})
]
}
)
provider = _Provider(
[_snapshot(7)],
deltas=[_delta(workspaces=[targeted_workspace])],
)
service = DirectoryProjectionService(application, provider, INSTANCE_UUID)
await service.initialize()
await service.reconcile_workspaces((WORKSPACE_UUID,))
async with session_factory() as session:
account = await session.scalar(sqlalchemy.select(User).where(User.uuid == ACCOUNT_UUID))
workspace = await session.get(Workspace, WORKSPACE_UUID)
state = await session.get(DirectoryProjectionState, INSTANCE_UUID)
assert account is not None and account.user == 'Workspace Owner'
assert account.projection_revision == 7
assert workspace is not None and workspace.name == 'Renamed Workspace'
assert state is not None and state.cursor == 7
async def test_targeted_reconciliation_only_updates_requested_workspace_side_effects(projection_context):
application, _session_factory = projection_context
provider = _Provider(
[_snapshot(7, workspaces=[])],
deltas=[_delta(workspaces=[_workspace(revision=8, name='JIT Workspace')])],
)
service = DirectoryProjectionService(application, provider, INSTANCE_UUID)
await service.initialize()
service._reconcile_entitlement_snapshot_set = AsyncMock()
service._update_entitlement_workspace_activity = AsyncMock()
service._publish_runtime_execution_projection = Mock()
await service.reconcile_workspaces((WORKSPACE_UUID,))
service._reconcile_entitlement_snapshot_set.assert_not_awaited()
service._update_entitlement_workspace_activity.assert_awaited_once()
assert service._update_entitlement_workspace_activity.await_args.kwargs == {
'requested_workspace_uuids': {WORKSPACE_UUID},
}
service._publish_runtime_execution_projection.assert_called_once()
assert service._publish_runtime_execution_projection.call_args.kwargs == {
'affected_workspace_uuids': {WORKSPACE_UUID},
}
async def test_initial_snapshot_projects_core_owned_rows(projection_context): async def test_initial_snapshot_projects_core_owned_rows(projection_context):
application, session_factory = projection_context application, session_factory = projection_context
reconcile_execution_projection = Mock() reconcile_execution_projection = Mock()
+88 -1
View File
@@ -55,7 +55,7 @@ finally:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def make_runner(output_key: str = 'response') -> N8nServiceAPIRunner: def make_runner(output_key: str = 'response', response_handling: str = 'reply') -> N8nServiceAPIRunner:
ap = Mock() ap = Mock()
ap.logger = Mock() ap.logger = Mock()
pipeline_config = { pipeline_config = {
@@ -63,6 +63,7 @@ def make_runner(output_key: str = 'response') -> N8nServiceAPIRunner:
'n8n-service-api': { 'n8n-service-api': {
'webhook-url': 'http://test-n8n/webhook', 'webhook-url': 'http://test-n8n/webhook',
'output-key': output_key, 'output-key': output_key,
'response-handling': response_handling,
'auth-type': 'none', 'auth-type': 'none',
} }
} }
@@ -287,6 +288,7 @@ def make_http_session_mock(response_bytes: bytes, status: int = 200):
"""Mock httpclient.get_session() returning a session whose post() yields response_bytes.""" """Mock httpclient.get_session() returning a session whose post() yields response_bytes."""
mock_response = make_mock_response([response_bytes], status=status) mock_response = make_mock_response([response_bytes], status=status)
mock_response.status = status mock_response.status = status
mock_response.headers = {}
mock_cm = AsyncMock() mock_cm = AsyncMock()
mock_cm.__aenter__ = AsyncMock(return_value=mock_response) mock_cm.__aenter__ = AsyncMock(return_value=mock_response)
@@ -314,6 +316,91 @@ async def test_call_webhook_nonstream_adapter_plain_json():
assert results[0].content == 'result text' assert results[0].content == 'result text'
@pytest.mark.asyncio
@pytest.mark.parametrize('status', [200, 201, 202, 204])
@pytest.mark.parametrize(
'response_body',
[
b'{"message":"Workflow was started"}',
b'{"response":"must not be forwarded"}',
b'plain acknowledgement',
],
)
async def test_call_webhook_ignore_response_body(response_body: bytes, status: int):
"""Ignore mode accepts any HTTP 2xx response without emitting chat output."""
runner = make_runner(response_handling='ignore')
query = make_query(is_stream=False)
http_session = make_http_session_mock(response_body, status=status)
with patch('langbot.pkg.provider.runners.n8nsvapi.httpclient.get_session', return_value=http_session):
results = []
async for message in runner._call_webhook(query):
results.append(message)
assert results == []
@pytest.mark.asyncio
async def test_call_webhook_ignore_releases_without_reading_response_body():
"""Ignore mode returns after the success status without waiting for the body."""
runner = make_runner(response_handling='ignore')
query = make_query(is_stream=False)
mock_response = make_mock_response([], status=202)
mock_response.headers = {}
mock_response.release = Mock()
async def fail_if_read(_size):
raise AssertionError('ignore mode must not read the response body')
yield b''
mock_response.content.iter_chunked = fail_if_read
mock_cm = AsyncMock()
mock_cm.__aenter__ = AsyncMock(return_value=mock_response)
mock_cm.__aexit__ = AsyncMock(return_value=False)
mock_session = Mock()
mock_session.post = Mock(return_value=mock_cm)
with patch('langbot.pkg.provider.runners.n8nsvapi.httpclient.get_session', return_value=mock_session):
results = [message async for message in runner._call_webhook(query)]
assert results == []
mock_response.release.assert_called_once_with()
@pytest.mark.asyncio
@pytest.mark.parametrize('status', [201, 202, 204])
async def test_call_webhook_reply_mode_preserves_http_200_contract(status: int):
"""Reply mode remains backward compatible and rejects non-200 statuses."""
runner = make_runner(response_handling='reply')
query = make_query(is_stream=False)
http_session = make_http_session_mock(b'', status=status)
with patch('langbot.pkg.provider.runners.n8nsvapi.httpclient.get_session', return_value=http_session):
with pytest.raises(N8nAPIError, match=f'n8n webhook call failed: {status}'):
async for _ in runner._call_webhook(query):
pass
@pytest.mark.asyncio
async def test_call_webhook_ignore_mode_preserves_http_error():
"""Ignore mode must not swallow a failed n8n webhook response."""
runner = make_runner(response_handling='ignore')
query = make_query(is_stream=False)
http_session = make_http_session_mock(b'{"error":"unavailable"}', status=500)
with patch('langbot.pkg.provider.runners.n8nsvapi.httpclient.get_session', return_value=http_session):
with pytest.raises(N8nAPIError, match='n8n webhook call exception'):
async for _ in runner._call_webhook(query):
pass
@pytest.mark.asyncio
async def test_invalid_response_handling_is_rejected():
"""Configuration errors should fail fast instead of silently changing reply behavior."""
with pytest.raises(ValueError, match='Invalid n8n response-handling'):
make_runner(response_handling='unexpected')
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_call_webhook_stream_adapter_stream_format(): async def test_call_webhook_stream_adapter_stream_format():
"""Stream adapter + stream format → MessageChunks, last is_final.""" """Stream adapter + stream format → MessageChunks, last is_final."""
@@ -0,0 +1,59 @@
"""Tests for WecomAdapter.send_message content-key handling."""
import pytest
import langbot_plugin.api.entities.builtin.platform.message as platform_message
from langbot.pkg.platform.sources.wecom import WecomAdapter
class StubWecomClient:
def __init__(self):
self.calls = []
async def get_media_id(self, msg):
return 'MEDIA_ID_123'
async def send_private_msg(self, user_id, agent_id, text):
self.calls.append(('text', user_id, agent_id, text))
async def send_image(self, user_id, agent_id, media_id):
self.calls.append(('image', user_id, agent_id, media_id))
async def send_voice(self, user_id, agent_id, media_id):
self.calls.append(('voice', user_id, agent_id, media_id))
async def send_file(self, user_id, agent_id, media_id):
self.calls.append(('file', user_id, agent_id, media_id))
def _make_adapter():
adapter = WecomAdapter.model_construct(bot=StubWecomClient())
return adapter
@pytest.mark.asyncio
@pytest.mark.parametrize(
('part', 'expected_type'),
[
(platform_message.Image(url='https://example.com/x.jpg'), 'image'),
(platform_message.Voice(url='https://example.com/x.amr'), 'voice'),
(platform_message.File(url='https://example.com/x.pdf', name='x.pdf'), 'file'),
],
)
async def test_send_message_dispatches_media_by_id(part, expected_type):
adapter = _make_adapter()
chain = platform_message.MessageChain([part])
await adapter.send_message('person', 'USER1|1000001', chain)
assert adapter.bot.calls == [(expected_type, 'USER1', 1000001, 'MEDIA_ID_123')]
@pytest.mark.asyncio
async def test_send_message_text_still_works():
adapter = _make_adapter()
chain = platform_message.MessageChain([platform_message.Plain(text='hello')])
await adapter.send_message('person', 'USER1|1000001', chain)
assert adapter.bot.calls == [('text', 'USER1', 1000001, 'hello')]
@@ -113,7 +113,7 @@ async def test_invitation_email_uses_quiet_brand_lockup_and_compact_fallback_lin
html = service._html("RockChinQ's Workspace", link) html = service._html("RockChinQ's Workspace", link)
assert 'https://docs.langbot.app/langbot-logo.png' in html assert 'https://langbot.app/docs/langbot-logo.png' in html
assert '>LangBot<' in html assert '>LangBot<' in html
assert 'Workspace invitation' in html assert 'Workspace invitation' in html
assert 'Open invitation link' in html assert 'Open invitation link' in html
Generated
+5 -5
View File
@@ -2008,7 +2008,7 @@ wheels = [
[[package]] [[package]]
name = "langbot" name = "langbot"
version = "4.10.9" version = "4.10.10"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiocqhttp" }, { name = "aiocqhttp" },
@@ -2129,7 +2129,7 @@ requires-dist = [
{ name = "ebooklib", specifier = ">=0.18" }, { name = "ebooklib", specifier = ">=0.18" },
{ name = "gewechat-client", specifier = ">=0.1.5" }, { name = "gewechat-client", specifier = ">=0.1.5" },
{ name = "html2text", specifier = ">=2024.2.26" }, { name = "html2text", specifier = ">=2024.2.26" },
{ name = "langbot-plugin", specifier = "==0.5.6" }, { name = "langbot-plugin", specifier = "==0.5.7" },
{ name = "langchain", specifier = ">=1.3.9" }, { name = "langchain", specifier = ">=1.3.9" },
{ name = "langchain-core", specifier = ">=1.3.3" }, { name = "langchain-core", specifier = ">=1.3.3" },
{ name = "langchain-text-splitters", specifier = ">=1.1.2" }, { name = "langchain-text-splitters", specifier = ">=1.1.2" },
@@ -2196,7 +2196,7 @@ dev = [
[[package]] [[package]]
name = "langbot-plugin" name = "langbot-plugin"
version = "0.5.6" version = "0.5.7"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },
@@ -2217,9 +2217,9 @@ dependencies = [
{ name = "watchdog" }, { name = "watchdog" },
{ name = "websockets" }, { name = "websockets" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/0b/1b/0c2e1f457abedf7ce052f47ad193937322b5f25f4e09e35d92bb5bd0346f/langbot_plugin-0.5.6.tar.gz", hash = "sha256:b7d6bb170ceffead6929e8d95ac388dd9a90a6d971ec4fcdaf7f7b46e894fa9e", size = 475814, upload-time = "2026-08-31T16:04:51.604Z" } sdist = { url = "https://files.pythonhosted.org/packages/d2/7d/b024770f1f52c9dc71ddcab79fc07dfb6147ce8e645f0fed170d758e49cb/langbot_plugin-0.5.7.tar.gz", hash = "sha256:faecd566b7ff57dc5f3a5b1be01e2165d25924031c0a65a829c83b51c65255ee", size = 480635, upload-time = "2026-09-04T13:39:22.505Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/40/1bb5d3562f66c88ac45b3b5b6ee77e9f8a6943599aea95731ea4a4e8b005/langbot_plugin-0.5.6-py3-none-any.whl", hash = "sha256:8f35a07be667abeb84147c4299d7afcc394125c73455fc74d9fcc887eae3a7d4", size = 306108, upload-time = "2026-08-31T16:04:50.427Z" }, { url = "https://files.pythonhosted.org/packages/cd/25/416745039cacace6a0ca3f719a2eff41dc74cdb30ef7ffaec1de0142bd2e/langbot_plugin-0.5.7-py3-none-any.whl", hash = "sha256:b1a20bcb6a2d482019eafbfe0ac628c106b8e915c7afe89df057b4d8e2015f05", size = 310463, upload-time = "2026-09-04T13:39:21.18Z" },
] ]
[[package]] [[package]]
+1 -1
View File
@@ -1,6 +1,6 @@
# Debug LangBot Frontend # Debug LangBot Frontend
Please refer to the [Development Guide](https://link.langbot.app/en/docs/dev-config) for more information. Please refer to the [Development Guide](https://langbot.app/docs/en/develop/dev-config) for more information.
## Tests ## Tests
+15 -3
View File
@@ -43,17 +43,24 @@ const pendingSpaceOAuthLogins = new Map<
function getOrCreateSpaceOAuthLoginPromise( function getOrCreateSpaceOAuthLoginPromise(
authCode: string, authCode: string,
state: string, state: string,
redirectUri: string,
workspaceUuid?: string, workspaceUuid?: string,
launchAssertion?: string, launchAssertion?: string,
): Promise<SpaceOAuthLoginResult> { ): Promise<SpaceOAuthLoginResult> {
const requestKey = `${authCode}:${state}:${workspaceUuid ?? ''}:${launchAssertion ?? ''}`; const requestKey = `${authCode}:${state}:${redirectUri}:${workspaceUuid ?? ''}:${launchAssertion ?? ''}`;
const pendingRequest = pendingSpaceOAuthLogins.get(requestKey); const pendingRequest = pendingSpaceOAuthLogins.get(requestKey);
if (pendingRequest) { if (pendingRequest) {
return pendingRequest; return pendingRequest;
} }
const requestPromise = httpClient const requestPromise = httpClient
.exchangeSpaceOAuthCode(authCode, state, workspaceUuid, launchAssertion) .exchangeSpaceOAuthCode(
authCode,
state,
redirectUri,
workspaceUuid,
launchAssertion,
)
.finally(() => { .finally(() => {
pendingSpaceOAuthLogins.delete(requestKey); pendingSpaceOAuthLogins.delete(requestKey);
}); });
@@ -95,6 +102,7 @@ function SpaceOAuthCallbackContent() {
const response = await getOrCreateSpaceOAuthLoginPromise( const response = await getOrCreateSpaceOAuthLoginPromise(
authCode, authCode,
state, state,
`${window.location.origin}/auth/space/callback`,
workspaceUuid, workspaceUuid,
launchAssertion, launchAssertion,
); );
@@ -195,7 +203,11 @@ function SpaceOAuthCallbackContent() {
async (authCode: string, state: string) => { async (authCode: string, state: string) => {
setIsProcessing(true); setIsProcessing(true);
try { try {
const response = await httpClient.bindSpaceAccount(authCode, state); const response = await httpClient.bindSpaceAccount(
authCode,
state,
`${window.location.origin}/auth/space/callback?mode=bind`,
);
if (!isMountedRef.current) { if (!isMountedRef.current) {
return; return;
} }
@@ -0,0 +1,22 @@
const COMMON_N8N_CONFIG_FIELDS = new Set([
'webhook-url',
'auth-type',
'timeout',
'output-key',
'response-handling',
]);
export function shouldShowN8nConfigField(
fieldName: string,
authType: string,
): boolean {
if (COMMON_N8N_CONFIG_FIELDS.has(fieldName)) {
return true;
}
return (
(authType === 'basic' && fieldName.startsWith('basic-')) ||
(authType === 'jwt' && fieldName.startsWith('jwt-')) ||
(authType === 'header' && fieldName.startsWith('header-'))
);
}
@@ -13,6 +13,7 @@ import {
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent'; import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent';
import { normalizeDynamicFormValuesForSave } from '@/app/home/components/dynamic-form/DynamicFormSaveValues'; import { normalizeDynamicFormValuesForSave } from '@/app/home/components/dynamic-form/DynamicFormSaveValues';
import { shouldShowN8nConfigField } from '@/app/home/components/dynamic-form/N8nAuthFieldVisibility';
import { extractI18nObject } from '@/i18n/I18nProvider'; import { extractI18nObject } from '@/i18n/I18nProvider';
/** /**
@@ -181,29 +182,9 @@ export default function N8nAuthFormComponent({
}, [form, itemConfigList]); }, [form, itemConfigList]);
// 根据认证类型过滤表单项 // 根据认证类型过滤表单项
const filteredConfigList = itemConfigList.filter((config) => { const filteredConfigList = itemConfigList.filter((config) =>
// 始终显示webhook-url、auth-type、timeout和output-key shouldShowN8nConfigField(config.name, authType),
if ( );
['webhook-url', 'auth-type', 'timeout', 'output-key'].includes(
config.name,
)
) {
return true;
}
// 根据认证类型显示相应的表单项
if (authType === 'basic' && config.name.startsWith('basic-')) {
return true;
}
if (authType === 'jwt' && config.name.startsWith('jwt-')) {
return true;
}
if (authType === 'header' && config.name.startsWith('header-')) {
return true;
}
return false;
});
return ( return (
<Form {...form}> <Form {...form}>
@@ -2195,12 +2195,12 @@ export default function HomeSidebar({
localStorage.getItem('langbot_language'); localStorage.getItem('langbot_language');
if (language === 'zh-Hans' || language === 'zh-Hant') { if (language === 'zh-Hans' || language === 'zh-Hant') {
window.open( window.open(
'https://link.langbot.app/zh/docs/guide', 'https://langbot.app/docs/zh/insight/guide',
'_blank', '_blank',
); );
} else { } else {
window.open( window.open(
'https://link.langbot.app/en/docs/guide', 'https://langbot.app/docs/en/insight/guide',
'_blank', '_blank',
); );
} }
@@ -49,9 +49,9 @@ export const sidebarConfigList = [
route: '/home/bots', route: '/home/bots',
description: t('bots.description'), description: t('bots.description'),
helpLink: { helpLink: {
en_US: 'https://link.langbot.app/en/docs/platforms', en_US: 'https://langbot.app/docs/en/usage/platforms/readme',
zh_Hans: 'https://link.langbot.app/zh/docs/platforms', zh_Hans: 'https://langbot.app/docs/zh/usage/platforms/readme',
ja_JP: 'https://link.langbot.app/ja/docs/platforms', ja_JP: 'https://langbot.app/docs/ja/usage/platforms/readme',
}, },
section: 'home', section: 'home',
}), }),
@@ -62,9 +62,9 @@ export const sidebarConfigList = [
route: '/home/pipelines', route: '/home/pipelines',
description: t('pipelines.description'), description: t('pipelines.description'),
helpLink: { helpLink: {
en_US: 'https://link.langbot.app/en/docs/pipelines', en_US: 'https://langbot.app/docs/en/usage/pipelines/readme',
zh_Hans: 'https://link.langbot.app/zh/docs/pipelines', zh_Hans: 'https://langbot.app/docs/zh/usage/pipelines/readme',
ja_JP: 'https://link.langbot.app/ja/docs/pipelines', ja_JP: 'https://langbot.app/docs/ja/usage/pipelines/readme',
}, },
section: 'home', section: 'home',
}), }),
@@ -75,9 +75,9 @@ export const sidebarConfigList = [
route: '/home/knowledge', route: '/home/knowledge',
description: t('knowledge.description'), description: t('knowledge.description'),
helpLink: { helpLink: {
en_US: 'https://link.langbot.app/en/docs/knowledge', en_US: 'https://langbot.app/docs/en/usage/knowledge/readme',
zh_Hans: 'https://link.langbot.app/zh/docs/knowledge', zh_Hans: 'https://langbot.app/docs/zh/usage/knowledge/readme',
ja_JP: 'https://link.langbot.app/ja/docs/knowledge', ja_JP: 'https://langbot.app/docs/ja/usage/knowledge/readme',
}, },
section: 'home', section: 'home',
}), }),
@@ -89,9 +89,9 @@ export const sidebarConfigList = [
route: '/home/extensions', route: '/home/extensions',
description: t('plugins.description'), description: t('plugins.description'),
helpLink: { helpLink: {
en_US: 'https://docs.langbot.app/en/plugin/plugin-intro', en_US: 'https://langbot.app/docs/en/plugin/plugin-intro',
zh_Hans: 'https://docs.langbot.app/zh/plugin/plugin-intro', zh_Hans: 'https://langbot.app/docs/zh/plugin/plugin-intro',
ja_JP: 'https://docs.langbot.app/ja/plugin/plugin-intro', ja_JP: 'https://langbot.app/docs/ja/plugin/plugin-intro',
}, },
section: 'extensions', section: 'extensions',
}), }),
@@ -102,9 +102,9 @@ export const sidebarConfigList = [
route: '/home/add-extension', route: '/home/add-extension',
description: t('plugins.description'), description: t('plugins.description'),
helpLink: { helpLink: {
en_US: 'https://docs.langbot.app/en/plugin/plugin-intro', en_US: 'https://langbot.app/docs/en/plugin/plugin-intro',
zh_Hans: 'https://docs.langbot.app/zh/plugin/plugin-intro', zh_Hans: 'https://langbot.app/docs/zh/plugin/plugin-intro',
ja_JP: 'https://docs.langbot.app/ja/plugin/plugin-intro', ja_JP: 'https://langbot.app/docs/ja/plugin/plugin-intro',
}, },
section: 'extensions', section: 'extensions',
}), }),
@@ -34,11 +34,11 @@ export default function NewVersionDialog({
const getUpdateDocsUrl = () => { const getUpdateDocsUrl = () => {
const language = i18n.language; const language = i18n.language;
if (language === 'zh-Hans' || language === 'zh-Hant') { if (language === 'zh-Hans' || language === 'zh-Hant') {
return 'https://link.langbot.app/zh/docs/update'; return 'https://langbot.app/docs/zh/deploy/update';
} else if (language === 'ja-JP') { } else if (language === 'ja-JP') {
return 'https://link.langbot.app/ja/docs/update'; return 'https://langbot.app/docs/ja/deploy/update';
} else { } else {
return 'https://link.langbot.app/en/docs/update'; return 'https://langbot.app/docs/en/deploy/update';
} }
}; };
+5 -4
View File
@@ -1365,6 +1365,7 @@ export class BackendClient extends BaseHttpClient {
public async bindSpaceAccount( public async bindSpaceAccount(
code: string, code: string,
state: string, state: string,
redirectUri: string,
): Promise<{ ): Promise<{
token: string; token: string;
user: string; user: string;
@@ -1372,7 +1373,7 @@ export class BackendClient extends BaseHttpClient {
}> { }> {
const response = await this.instance.post( const response = await this.instance.post(
'/api/v1/user/bind-space', '/api/v1/user/bind-space',
{ code, state }, { code, state, redirect_uri: redirectUri },
{ skipWorkspace: true } as RequestConfig, { skipWorkspace: true } as RequestConfig,
); );
if (response.data.code !== 0) { if (response.data.code !== 0) {
@@ -1390,9 +1391,7 @@ export class BackendClient extends BaseHttpClient {
}> { }> {
return this.get( return this.get(
'/api/v1/user/space/authorize-url', '/api/v1/user/space/authorize-url',
{ { redirect_uri: redirectUri },
redirect_uri: redirectUri,
},
{ skipWorkspace: true }, { skipWorkspace: true },
); );
} }
@@ -1410,6 +1409,7 @@ export class BackendClient extends BaseHttpClient {
public async exchangeSpaceOAuthCode( public async exchangeSpaceOAuthCode(
code: string, code: string,
state: string, state: string,
redirectUri: string,
workspaceUuid?: string, workspaceUuid?: string,
launchAssertion?: string, launchAssertion?: string,
): Promise<{ ): Promise<{
@@ -1424,6 +1424,7 @@ export class BackendClient extends BaseHttpClient {
{ {
code, code,
state, state,
redirect_uri: redirectUri,
workspace_uuid: workspaceUuid, workspace_uuid: workspaceUuid,
launch_assertion: launchAssertion, launch_assertion: launchAssertion,
}, },
+2 -1
View File
@@ -76,7 +76,8 @@ const enUS = {
privacyPolicy: 'Privacy Policy', privacyPolicy: 'Privacy Policy',
and: 'and', and: 'and',
dataCollectionPolicy: 'Data Collection Policy', dataCollectionPolicy: 'Data Collection Policy',
dataCollectionPolicyUrl: 'https://link.langbot.app/en/docs/data-policy', dataCollectionPolicyUrl:
'https://langbot.app/docs/en/insight/data-collection-policy',
loading: 'Loading...', loading: 'Loading...',
fieldRequired: 'This field is required', fieldRequired: 'This field is required',
or: 'or', or: 'or',
+2 -1
View File
@@ -79,7 +79,8 @@ const esES = {
privacyPolicy: 'Política de privacidad', privacyPolicy: 'Política de privacidad',
and: 'y', and: 'y',
dataCollectionPolicy: 'Política de recopilación de datos', dataCollectionPolicy: 'Política de recopilación de datos',
dataCollectionPolicyUrl: 'https://link.langbot.app/en/docs/data-policy', dataCollectionPolicyUrl:
'https://langbot.app/docs/en/insight/data-collection-policy',
loading: 'Cargando...', loading: 'Cargando...',
fieldRequired: 'Este campo es obligatorio', fieldRequired: 'Este campo es obligatorio',
or: 'o', or: 'o',
+2 -1
View File
@@ -77,7 +77,8 @@ const jaJP = {
privacyPolicy: 'プライバシーポリシー', privacyPolicy: 'プライバシーポリシー',
and: 'および', and: 'および',
dataCollectionPolicy: 'データ収集ポリシー', dataCollectionPolicy: 'データ収集ポリシー',
dataCollectionPolicyUrl: 'https://link.langbot.app/ja/docs/data-policy', dataCollectionPolicyUrl:
'https://langbot.app/docs/ja/insight/data-collection-policy',
loading: '読み込み中...', loading: '読み込み中...',
fieldRequired: 'この項目は必須です', fieldRequired: 'この項目は必須です',
or: 'または', or: 'または',
+2 -1
View File
@@ -76,7 +76,8 @@ const ruRU = {
privacyPolicy: 'Политикой конфиденциальности', privacyPolicy: 'Политикой конфиденциальности',
and: 'и', and: 'и',
dataCollectionPolicy: 'Политикой сбора данных', dataCollectionPolicy: 'Политикой сбора данных',
dataCollectionPolicyUrl: 'https://link.langbot.app/en/docs/data-policy', dataCollectionPolicyUrl:
'https://langbot.app/docs/en/insight/data-collection-policy',
loading: 'Загрузка...', loading: 'Загрузка...',
fieldRequired: 'Это поле обязательно для заполнения', fieldRequired: 'Это поле обязательно для заполнения',
or: 'или', or: 'или',
+2 -1
View File
@@ -76,7 +76,8 @@ const thTH = {
privacyPolicy: 'นโยบายความเป็นส่วนตัว', privacyPolicy: 'นโยบายความเป็นส่วนตัว',
and: 'และ', and: 'และ',
dataCollectionPolicy: 'นโยบายการเก็บรวบรวมข้อมูล', dataCollectionPolicy: 'นโยบายการเก็บรวบรวมข้อมูล',
dataCollectionPolicyUrl: 'https://link.langbot.app/en/docs/data-policy', dataCollectionPolicyUrl:
'https://langbot.app/docs/en/insight/data-collection-policy',
loading: 'กำลังโหลด...', loading: 'กำลังโหลด...',
fieldRequired: 'ช่องนี้จำเป็นต้องกรอก', fieldRequired: 'ช่องนี้จำเป็นต้องกรอก',
or: 'หรือ', or: 'หรือ',
+2 -1
View File
@@ -77,7 +77,8 @@ const viVN = {
privacyPolicy: 'Chính sách bảo mật', privacyPolicy: 'Chính sách bảo mật',
and: 'và', and: 'và',
dataCollectionPolicy: 'Chính sách thu thập dữ liệu', dataCollectionPolicy: 'Chính sách thu thập dữ liệu',
dataCollectionPolicyUrl: 'https://link.langbot.app/en/docs/data-policy', dataCollectionPolicyUrl:
'https://langbot.app/docs/en/insight/data-collection-policy',
loading: 'Đang tải...', loading: 'Đang tải...',
fieldRequired: 'Trường này là bắt buộc', fieldRequired: 'Trường này là bắt buộc',
or: 'hoặc', or: 'hoặc',
+2 -1
View File
@@ -75,7 +75,8 @@ const zhHans = {
privacyPolicy: '隐私政策', privacyPolicy: '隐私政策',
and: '和', and: '和',
dataCollectionPolicy: '数据收集政策', dataCollectionPolicy: '数据收集政策',
dataCollectionPolicyUrl: 'https://link.langbot.app/zh/docs/data-policy', dataCollectionPolicyUrl:
'https://langbot.app/docs/zh/insight/data-collection-policy',
loading: '加载中...', loading: '加载中...',
fieldRequired: '此字段为必填项', fieldRequired: '此字段为必填项',
or: '或', or: '或',
+2 -1
View File
@@ -75,7 +75,8 @@ const zhHant = {
privacyPolicy: '隱私政策', privacyPolicy: '隱私政策',
and: '和', and: '和',
dataCollectionPolicy: '數據收集政策', dataCollectionPolicy: '數據收集政策',
dataCollectionPolicyUrl: 'https://link.langbot.app/zh/docs/data-policy', dataCollectionPolicyUrl:
'https://langbot.app/docs/zh/insight/data-collection-policy',
loading: '載入中...', loading: '載入中...',
fieldRequired: '此欄位為必填', fieldRequired: '此欄位為必填',
or: '或', or: '或',
@@ -7,12 +7,13 @@ const source = fs.readFileSync(
'utf8', 'utf8',
); );
test('normal Cloud login starts stateful Space OAuth', () => { test('normal Cloud login uses the standard Space OAuth callback path', () => {
assert.match(source, /getSpaceAuthorizeUrl\(redirectUri\)/);
assert.doesNotMatch(source, /cloudEntry/); assert.doesNotMatch(source, /cloudEntry/);
assert.match(source, /getSpaceAuthorizeUrl\(redirectUri\)/);
}); });
test('invitation login remains on the OAuth callback path', () => { test('invitation login uses the same OAuth callback before accepting the invitation', () => {
assert.match(source, /getPendingInvitationToken\(\)/); assert.doesNotMatch(source, /cloudEntry/);
assert.match(source, /getSpaceAuthorizeUrl\(redirectUri\)/); assert.match(source, /const invitationToken = getPendingInvitationToken\(\)/);
assert.match(source, /acceptWorkspaceInvitation\(invitationToken\)/);
}); });
@@ -0,0 +1,52 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import ts from 'typescript';
import { fileURLToPath } from 'node:url';
const currentDirectory = path.dirname(fileURLToPath(import.meta.url));
const sourcePath = path.resolve(
currentDirectory,
'../../src/app/home/components/dynamic-form/N8nAuthFieldVisibility.ts',
);
function loadVisibilityPolicy() {
const source = fs.readFileSync(sourcePath, 'utf8');
const compiled = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.CommonJS },
}).outputText;
const loadedModule = { exports: {} };
new Function('require', 'module', 'exports', compiled)(
() => {
throw new Error('N8nAuthFieldVisibility must not have runtime imports');
},
loadedModule,
loadedModule.exports,
);
return loadedModule.exports;
}
test('shows response handling with the other common n8n fields', () => {
const { shouldShowN8nConfigField } = loadVisibilityPolicy();
for (const field of [
'webhook-url',
'auth-type',
'timeout',
'output-key',
'response-handling',
]) {
assert.equal(shouldShowN8nConfigField(field, 'none'), true, field);
}
});
test('shows only fields for the selected n8n authentication method', () => {
const { shouldShowN8nConfigField } = loadVisibilityPolicy();
assert.equal(shouldShowN8nConfigField('basic-username', 'basic'), true);
assert.equal(shouldShowN8nConfigField('basic-password', 'jwt'), false);
assert.equal(shouldShowN8nConfigField('jwt-secret', 'jwt'), true);
assert.equal(shouldShowN8nConfigField('header-name', 'header'), true);
assert.equal(shouldShowN8nConfigField('unrelated-field', 'none'), false);
});