Compare commits

..

291 Commits

Author SHA1 Message Date
RockChinQ
662e6a4815 fix(longtext): avoid split interfering with multi-chain agent responses
Use query variable '_longtext_split_extra_chains' to pass extra split
segments instead of appending to resp_message_chain directly. This
prevents agent tool-call multi-round responses from being misidentified
as split results and sent repeatedly.

respback.py reverts to original single-chain logic and appends split
extra chains after the main response.
2026-03-12 09:52:59 -04:00
Dong_master
c92d3d7ad7 feat(longtext): implement long text splitting strategy with Markdown awareness 2026-03-09 01:39:25 +08:00
youhuanghe
40c7b0f731 fix(web): display document_name instead of file_id in retrieval results
The getTitle fallback order was reversed, always showing the UUID
(file_id) since it's always truthy. Swap priority to document_name
first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 04:24:41 +00:00
huanghuoguoguo
cadcf10047 Feat/rag plugin (#1995)
* [issue:1933] RAG engine plugin architecture (#1967)

* refactor: migrate RAG knowledge services to a plugin-oriented host service architecture.

* feat(rag): phase 2 core refactor with RPC Action handlers

* feat: 为 RAG 插件添加知识库创建和删除事件通知,并优化了 RAG 动作的参数传递和枚举使用。

* feat: 统一知识库管理为RAG引擎,支持动态配置并移除旧的外部知识库组件。

* refactor(rag): remove plugin_adapter, inline logic into RuntimeKnowledgeBase

BREAKING CHANGE: RAGPluginAdapter has been removed. All plugin
communication is now handled directly by RuntimeKnowledgeBase.

Architecture change:
- Before: RuntimeKnowledgeBase → RAGPluginAdapter → plugin_connector
- After:  RuntimeKnowledgeBase → plugin_connector (direct)

Changes to kbmgr.py (RuntimeKnowledgeBase):
- Remove RAGPluginAdapter import and usage
- Inline plugin communication methods:
  - _on_kb_create(): Notify plugin when KB is created
  - _on_kb_delete(): Notify plugin when KB is deleted
  - _ingest_document(): Call plugin for document ingestion
  - _retrieve(): Call plugin for retrieval
  - _delete_document(): Call plugin to delete document
- Simplify dispose(): Only notify plugin, no built-in VDB assumption

Changes to base.py (KnowledgeBaseInterface):
- Remove get_type() abstract method (outdated internal/external concept)
- Add get_rag_engine_plugin_id() abstract method

Changes to localagent.py:
- Remove get_type() call
- Simplify top_k retrieval from KB entity

Deleted files:
- pkg/rag/knowledge/plugin_adapter.py

Benefits:
- Reduced abstraction layer, simpler code
- Plugin communication logic centralized in RuntimeKnowledgeBase
- Easier to understand and maintain

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(api): remove ExternalKnowledgeBase infrastructure

BREAKING CHANGE: ExternalKnowledgeBase has been completely removed.
All knowledge bases are now unified under the single KnowledgeBase model,
differentiated by their rag_engine_plugin_id.

Deleted files:
- pkg/api/http/controller/groups/knowledge/external.py
  (ExternalKBController with /external-bases routes)
- pkg/api/http/service/external_kb.py
  (ExternalKnowledgeBaseService)
- pkg/rag/knowledge/external.py
  (ExternalKnowledgeBase implementation)

Modified files:
- pkg/entity/persistence/rag.py:
  Remove ExternalKnowledgeBase SQLAlchemy table definition
- pkg/core/app.py:
  Remove external_kb_service attribute from LangBotApplication
- pkg/core/stages/build_app.py:
  Remove external_kb_service initialization

Migration notes:
- Existing external knowledge base data should be migrated manually
- API consumers should use /api/v1/knowledge/bases for all KB operations
- Use /api/v1/knowledge/engines to discover available RAG engines

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(plugin): remove list_knowledge_retrievers from connector

Remove deprecated list_knowledge_retrievers functionality from the
plugin communication layer. This aligns with the SDK change that
removed the LIST_KNOWLEDGE_RETRIEVERS action.

Changes:
- connector.py: Remove list_knowledge_retrievers() method
- handler.py: Remove list_knowledge_retrievers() handler

The functionality is replaced by the new /api/v1/knowledge/engines
endpoint which lists available RAGEngine components with their
capabilities and configuration schemas.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(service): update knowledge service with capability-based checks

Replace type-based checks with capability-based checks for file
operations, aligning with the unified knowledge base architecture.

Changes to knowledge.py:
- store_file(): Replace get_type() check with doc_ingestion capability check
- delete_file(): Replace get_type() check with doc_ingestion capability check
- list_rag_engines(): Remove list_knowledge_retrievers call, simplify to
  only list RAGEngine components (KnowledgeRetriever type removed)

Changes to pipelines.py:
- Minor cleanup related to knowledge base references

The capability-based approach allows RAG engines to declare their
supported features (doc_ingestion, chunking_config, rerank, hybrid_search)
and the system responds accordingly, rather than hardcoding behavior
based on internal/external type distinction.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(web): unify knowledge base UI, remove external KB components

BREAKING CHANGE: The internal/external knowledge base distinction
has been removed from the frontend. All knowledge bases are now
displayed in a unified list, differentiated by their RAG engine.

Changes to page.tsx:
- Remove Tab component (内置/外置 tabs)
- Remove selectedKbType state
- Unified knowledge base list display
- Single "Create Knowledge Base" button for all types

Changes to KBDetailDialog.tsx:
- Remove kbType prop
- Simplify dialog logic for unified KB handling
- Documents menu item conditionally shown based on doc_ingestion capability

Changes to KBForm.tsx:
- Remove retriever type handling code
- Simplify form for unified KB creation
- Dynamic form rendering based on RAG engine's creation_schema

Changes to KBCardVO.ts:
- Remove 'type' field from KBCardVO interface

Changes to BackendClient.ts:
- Remove all external KB related methods:
  - getExternalKnowledgeBases()
  - getExternalKnowledgeBase()
  - createExternalKnowledgeBase()
  - updateExternalKnowledgeBase()
  - deleteExternalKnowledgeBase()
  - retrieveFromExternalKnowledgeBase()

Changes to api/index.ts:
- Remove ExternalKnowledgeBase interface definition

UI/UX improvements:
- Users no longer need to understand internal vs external distinction
- RAG engine selection is now the primary differentiator
- Documents panel visibility is capability-driven (doc_ingestion)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(plugin): code review improvements for RAG handlers

- Unify embed_model field naming to embedding_model_uuid only
- Add structured error responses with error_type for RAG actions
- Fix file_size and mime_type detection in _store_file_task
- Improve error handling with detailed error context (error_type, original_error)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(rag): refactor KB dynamic form and vector manager

- Frontend: Refactor Knowledge Base form using DynamicForm components.
- Frontend: Remove obsolete jsonSchemaConverter utility.
- Backend: Update VectorManager and PluginHandler to support new RAG architecture.
- Chore: Update dependencies in pyproject.toml.

* fix: code review fixes for RAG refactor

- Remove DEBUG stderr outputs in handler.py
- Move repeated `import json` to file top
- Add warning log for unimplemented delete_by_filter

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(rag): consolidate valid_fields into entity constants

Define MUTABLE_FIELDS, CREATE_FIELDS, ALL_DB_FIELDS as class
constants in KnowledgeBase entity to eliminate duplication.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: 将知识库获取和RAG引擎信息丰富逻辑移至知识库管理器。

* refactor(rag): introduce RAGRuntimeService and clean up plugin handler

- Create RAGRuntimeService to encapsulate RAG capability implementation (Embedding, VectorOps).
- Refactor PluginHandler to delegate RAG actions to RAGRuntimeService.
- Move KnowledgeService enrichment and creation logic to RAGManager.
- Register RAGRuntimeService in Application and BuildAppStage.
- Clean up legacy code in KnowledgeService.

* refactor(rag): standardize logger and fix type hints

- Use self.ap.logger consistently in kbmgr.py and runtime.py, removing module-level loggers.
- Fix type hints for retrieve_knowledge in handler.py and connector.py to match implementation returning dict.

* refactor: 将引擎徽章的样式从 Tailwind CSS 类迁移到 CSS 模块。

* fix(web): resolve React rendering errors in plugins page

- Fix missing key prop in PluginComponentList by using ternary instead of Fragment
- Fix RAGEngine.name type to I18nObject and use extractI18nObject() for rendering
- Preserves multi-language support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(rag): update runtime service and web components

* refactor: 优化知识库设置结构并增强前端距离显示健壮性。

* fix: 处理前端距离显示中的空值。

* fix(rag): document retrieve ui and kbmgr top_k validation

* 更新 uv.lock 中的 PyPI 镜像源为官方地址。

* fix: address code review issues for RAG engine plugin architecture

P0 fixes:
- Fix ALL_DB_FIELDS missing collection_id and emoji fields
- Move rag_engine_plugin_id to CREATE_FIELDS (immutable after creation)
- Fix creation_settings mutable default value (dict -> None)
- Rename vector delete method to delete_by_file_id for correct semantics
- Fix delete_by_filter to raise NotImplementedError instead of silent no-op
- Add database migration script (dbm019) for new columns and table cleanup

P1 fixes:
- Clean up design-hesitation comments in connector.py
- Add _parse_plugin_id() with format validation for all RAG methods
- Make _retrieve() raise exceptions instead of silently returning empty results
- Extract _make_rag_error_response() helper for clean error formatting
- Remove unused imports from handler.py

P2 fixes:
- Fix runtime.py indentation inconsistencies
- Simplify get_file_stream to use storage abstraction uniformly
- Reduce redundant DB queries in knowledge service (extract _check_doc_capability)
- Fix engines.py URL encoding: use <path:plugin_id> instead of __ replacement
- Add read-only mode for engine settings in KBForm edit mode
- Simplify page.tsx handleKBCardClick to pass only kbId string

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: address code review findings for RAG plugin architecture

- Frontend: add retrieval_settings param to retrieveKnowledgeBase API call
- Backend: return {uuid} from PUT knowledge base to match frontend expectation
- Backend: validate query is non-empty in retrieve endpoint (400 on empty)
- Backend: rename vector_delete ids→file_ids for semantic clarity, keep
  backward compat by accepting both 'file_ids' and 'ids' in RPC handler
- Backend: ensure rag_engine.name fallback is always I18nObject-compatible
  dict, preventing frontend extractI18nObject from receiving plain strings
- Migration: fix misleading docstring about external_kb data migration

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update langbot-plugin version to 0.2.6

* chore: update required database version from 18 to 19

* refactor: remove unused polymorphic component framework

* chore: fix lint and format issues for python and frontend

* fix(plugin): remove legacy `ids` fallback in rag_vector_delete handler

SDK now sends `file_ids` directly, the `ids` backward-compat fallback
is no longer needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rag): deep review fixes for critical bugs, security and quality

Critical:
- Fix StorageMgr.load() -> storage_provider.load() (C1, AttributeError)
- Update required_database_version 18 -> 19 (C2, migration never runs)

Security:
- Add path traversal validation in get_file_stream (C11)
- Add vectors/ids/metadata length validation in rag_vector_upsert (C12)

Logic fixes:
- Legacy KBs: set capabilities to [] instead of ['doc_ingestion'] (C4)
- Fix store_file return type int -> str (C5)
- Fix retrieve_knowledge return [] -> {'results': []} when disabled (C6)
- Re-raise exception in _on_kb_create instead of silently swallowing (C7)
- Log warning when KB not found in memory during delete (C8)

API fixes:
- Catch ValueError as 400 in create_knowledge_base endpoint (C15)
- Validate plugin_id format in engines endpoints (C16)

Quality:
- Remove dead if/else in migration with identical branches (C17)
- Fix variable shadowing: rag_context -> rag_context_text (C18)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove unused os import to fix ruff lint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(plugin): remove PolymorphicComponent sync from LangBot side

Remove sync_polymorphic_component_instances() from connector and handler,
and the post-connection sync call in initialize(). This dead code synced
an always-empty list of polymorphic instances that were never created.

Companion change to langbot-plugin-sdk PolymorphicComponent removal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rag): fix vector_delete count bug and remove vestigial instance_id parameter

1. vector_delete: assign return value from delete_by_filter to count
   instead of silently returning 0 for filter-based deletion.

2. Remove instance_id parameter from the entire retrieve_knowledge
   call chain (kbmgr → connector → handler → runtime). This parameter
   was a remnant of the PolymorphicComponent mechanism and is no longer
   used — RAGEngine operates as a stateless singleton.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(web): 支持 creation_schema 字段级别的 editable 属性控制编辑模式可修改性

- IDynamicFormItemSchema 添加 editable 可选属性
- DynamicFormItemConfig 透传 editable 属性
- DynamicFormComponent 接收 isEditing prop,按字段 editable 值控制禁用
- KBForm 解析 editable 并传递 isEditing 给动态表单组件
- editable 未指定时默认可编辑,editable: false 时编辑模式下禁用该字段

* feat(storage): 添加 size() 抽象方法及 LocalStorage/S3 实现

支持获取存储对象大小,S3 使用 head_object 避免下载整个文件

* fix(migration): 删除 external_knowledge_bases 表前记录日志警告

- 迁移时如果表中存在数据,先 warning 日志记录避免无感数据丢失
- 添加 chunk 清理注释说明:仅对旧版非插件架构 KB 有效

* fix(web): 修复检索结果长文本撑大容器导致查询按钮不可见

KBDetailDialog 的 main 容器添加 min-w-0 overflow-x-hidden,
限制 flex-1 子容器宽度,防止 Dify RAG 长文本撑出 Dialog 边界

* fix(rag): address code review issues for plugin architecture PR

- Fix SQL injection in migration helpers by using bind parameters
- Move numpy import to module level in vector/mgr.py
- Improve path traversal validation using posixpath.normpath
- Add call_rag_retrieve to connector, eliminating duplicate plugin_id
  parsing in kbmgr.py _retrieve
- Normalize typing style to modern dict/list/None syntax

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(web): fix prettier formatting errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(rag): update embedding handling in RuntimeConnectionHandler

- Renamed RAG_EMBED_DOCUMENTS and RAG_EMBED_QUERY actions to INVOKE_EMBEDDING for clarity.
- Removed embed_documents and embed_query methods from RuntimeEmbeddingModel and RAGRuntimeService.
- Integrated embedding model retrieval directly in the invoke_embedding method, improving error handling for missing models.
- Updated the embedding invocation logic to streamline the process and enhance error reporting.

* refactor(web): replace KnowledgeRetriever with RAGEngine across frontend and tests

KnowledgeRetriever component type has been removed in favor of the new
RAGEngine architecture. Update all remaining references in i18n locales,
plugin component icon mappings, marketplace filter, and unit tests.

Addresses reviewer notes from RockChinQ on PR #1967.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rag): address critical bugs found in deep review

- Fix path traversal bypass in runtime.py (check all path components for '..')
- Use normalized path for file loading instead of raw user input
- Change knowledge_bases from list to dict for O(1) lookup and race safety
- Add rollback on KB creation failure (clean up DB + runtime on plugin error)
- Add null check after KB update in knowledge service
- Fix file extension parsing to use os.path.splitext instead of split('.')
  (handles multi-dot filenames like 'report.v2.pdf' correctly)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rag): address remaining review issues across frontend and backend

Frontend:
- Fix KB delete: use async/await with error handling instead of fire-and-forget
- Fix capabilities null check: add optional chaining to prevent crash
- Add toast.error on KB info load failure instead of silent console.error
- Replace hard-coded Chinese validation message with i18n key
- Replace hard-coded English error messages in DynamicFormItemComponent with i18n
- Optimize document polling: stop when all documents reach terminal state
- Add i18n keys (fieldRequired, loadKnowledgeBaseFailed,
  deleteKnowledgeBaseFailed, getKnowledgeBaseListError) to all 4 locales

Backend:
- Fix KB delete atomicity: delete from DB first, then notify plugin
- Add RAG engine plugin existence validation before creating KB

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(rag): fix ruff formatting in kbmgr.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>

* chore: bump langbot-plugin to 0.3.0 (#1992)

* chore: correct sdk version to 0.3.0a1

* feat: normalize rag related actions' names

* refactor(rag): align IngestionContext fields with SDK changes

Remove redundant `chunking_strategy` field and rename `custom_settings`
to `creation_settings` to match the updated SDK entity definitions
(langbot-plugin-sdk#36).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix ruff formatting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rag): enforce immutability of embedding_model_uuid and non-editable creation_settings fields

Remove embedding_model_uuid from MUTABLE_FIELDS to prevent post-creation
modification via API. Add backend validation for creation_settings to
preserve fields marked editable:false in the plugin's creation schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(rag): fix ruff formatting in knowledge service

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(rag): split settings into immutable creation_settings and mutable retrieval_settings

- Remove standalone embedding_model_uuid and top_k columns from KB entity
- Add retrieval_settings column; update MUTABLE_FIELDS/CREATE_FIELDS accordingly
- Merge migration logic into dbm019 (add retrieval_settings, migrate top_k
  and embedding_model_uuid into JSON settings, drop old columns on PostgreSQL)
- Remove _filter_creation_settings and per-field editable concept
- Frontend: creation_settings fields are all disabled when editing,
  retrieval_settings fields are always editable via a second DynamicFormComponent
- Remove editable from IDynamicFormItemSchema, DynamicFormItemConfig
- Clean up KBCardVO, KnowledgeBase API type, and localagent runner

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* bugfix: if ingest_document failed,not raise exep

* fix: ruff lint

* refactor(rag): remove unused _get_kb_entity method from RAGRuntimeService

* feat(vector): implement metadata filters for vector_search and vector_delete (#1997)

Add functional metadata filter support across all 5 VDB backends using
Chroma-style where syntax as the canonical format. Previously the filters
parameter existed throughout the stack but was entirely ignored.

- Add filter_utils.py with normalize_filter() and strip_unsupported_fields()
- Implement filter in search() and add delete_by_filter() for all backends:
  Chroma/SeekDB (native passthrough), Qdrant (translated to models.Filter),
  Milvus (translated to expr string), pgvector (translated to SQLAlchemy conditions)
- Milvus/pgvector limited to {text, file_id, chunk_uuid}; other fields logged and ignored
- Replace delete_by_filter() NotImplementedError with backend delegation in mgr.py
- Populate retrieval_context['filters'] from settings in kbmgr._retrieve()
- Pass search_type/query_text/documents through handler and runtime service

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(vector): fix ruff formatting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(vector): remove numpy dependency and fix SeekDB search modes

- Remove numpy array conversion for query vectors; all VDB backends
  accept list[float] directly
- Remove redundant get_or_create_collection call from upsert; backends
  handle collection creation internally in add_embeddings
- Fix SeekDB to raise ValueError when vector dimension is unknown
  instead of defaulting to 384
- Use hybrid_search() for full-text and hybrid search modes in SeekDB,
  since pyseekdb's query() always requires embeddings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(vector): escape single quotes in SeekDB documents and metadata

Document text containing apostrophes (e.g. "don't", "it's") causes
SQL syntax errors in OceanBase because single quotes were not in the
escape table. Add single-quote escaping and apply the escape table to
the documents parameter in add_embeddings(), not just metadata.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(vector): use standard SQL escaping for single quotes in SeekDB

Change single quote escaping from MySQL-style \' to standard SQL ''
(doubled quote). The backslash escape is not recognized by OceanBase
in NO_BACKSLASH_ESCAPES mode, causing SQL syntax errors when metadata
text contains apostrophes (e.g. O'Shea in academic citations).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rag): persist retrieval_settings on knowledge base creation

retrieval_settings was not being passed from the service layer to
RAGManager.create_knowledge_base(), causing retrieval schema fields
(e.g. query_rewrite) to be lost on initial KB creation. They only
took effect after a subsequent edit/update.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(web): add show_if conditional rendering for dynamic forms

Support conditional field visibility in plugin-defined forms via
show_if rules (eq, neq, in operators). Fields can depend on values
from the same form or cross-reference between creation and retrieval
settings via externalDependentValues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rag): replace base64 with chunked file transfer for get_rag_file_stream

Use send_file() instead of base64 encoding for returning file content
in the GET_RAG_FILE_STREAM handler, avoiding memory issues with large files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(parser): add parser plugin integration and capability-aware upload UI (#2000)

* feat(parser): add parser plugin integration and capability-aware upload UI

Backend: add parser plugin API endpoints (list/invoke), connector and
handler support for parser actions, and KB manager passthrough.

Frontend: thread ragEngineCapabilities prop to FileUploadZone and use
doc_parsing capability to conditionally show the RAG engine option in
the parser selector. When no parser is available, show a warning
prompting users to install a parser plugin.

Update i18n: rename builtInParser to "Provided by RAG engine" and add
noParserAvailable warning message in all 4 locales.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(parser): replace base64 with chunked file transfer and remove stale cache

- Remove @alru_cache from list_parsers() and list_rag_engines()
- Replace inline base64 file content with send_file/read_local_file
  chunked transfer pattern in parse_document and invoke_parser flows
- Remove unused base64 import from kbmgr.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat(web): add Parser component kind to plugin market UI and i18n

Add Parser to kindIconMap, market filter toggle, and all 4 locale files
so parser plugins are properly displayed and filterable in the plugin
market, matching the existing RAGEngine treatment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(web): fix prettier formatting from merge

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: rename RAGEngine to KnowledgeEngine across frontend and backend

* fix(web): fix I18nObject import path in FileUploadZone and KBDoc

* chore: format files involved in RAGEngine to KnowledgeEngine refactor

* refactor: change rag engine to knowledge engine

* fix: update langbot-plugin version to 0.3.0rc1

* chore: disable migration 20 for now

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2026-03-06 21:54:38 +08:00
fdc310
3e8f47fd97 feat: judge and send runner category (local or cloud) for telemetry
* feat(chat): add runner_url to payload for telemetry tracking

* feat(telemetry): add runner_url to sanitized fields in telemetry payload

* feat(telemetry): replace runner_url with runner_category in telemetry payload and add runner utility functions

* fix:ruff
2026-03-06 00:44:09 +08:00
youhuanghe
b11ae55c6e fix: update web/lint src 2026-03-05 15:02:03 +00:00
marun
2d63d528c6 refactor(dify): Optimize the Dify API output parsing and workflow processing logic (#2027)
- Add the _extract_dify_text_output method to uniformly handle the parsing of Dify output content

- Modify the content extraction method for the answer node in workflow mode

- Add workflow mode detection logic to support the workflow_started event

- Handle error state checks upon completion of the workflow

- Improve the message chunking logic for both basic and workflow modes

- Add a mechanism to capture answer content upon completion of a workflow node
2026-03-05 15:15:40 +08:00
fdc310
10f253015d Fix/tg send msg chunk (#2021)
* feat(telegram): enhance message handling with markdown support and draft messages

* fix(telegram): update draft message ID generation to use current timestamp
2026-03-04 20:42:33 +08:00
RockChinQ
b34ebf85a6 fix: update version to 4.8.7 in pyproject.toml, __init__.py, and uv.lock 2026-03-04 18:30:53 +08:00
RockChinQ
06d3298cde fix: update pnpm-lock.yaml for rehype-sanitize 2026-03-01 04:12:27 -05:00
Junyan Chin
614621ab7b Merge commit from fork
Add rehype-sanitize after rehypeRaw in all ReactMarkdown usages:
- PluginReadme.tsx (plugin README rendering)
- DebugDialog.tsx (debug chat message rendering)
- NewVersionDialog.tsx (release notes rendering)

This prevents injection of raw HTML (e.g. <iframe srcdoc>) that
could steal session tokens and API credentials from localStorage.

Fixes GHSA-w8gq-g4pc-xh3h
2026-03-01 17:01:23 +08:00
Junyan Qin
8600d0a8e7 chore: add botocore dependency to pyproject.toml and uv.lock
- Included botocore>=1.42.39 in dependencies to ensure compatibility with boto3.
- Updated lock file to reflect the new botocore dependency.
2026-02-28 19:26:50 +08:00
RockChinQ
b83e6a53be fix(storage): lazy import s3storage to avoid boto3 dependency for local storage
Fixes #2014

When using default local storage, the s3storage module was imported
at the top level, which triggered boto3/botocore import and caused
ModuleNotFoundError if those packages weren't installed.

Now s3storage is only imported when S3 storage is actually configured.
2026-02-28 06:02:41 -05:00
Junyan Chin
88132dff8a perf: reduce memory usage by ~200MB+ at startup (#2013)
* perf: reduce memory usage by ~200MB+ at startup

Two key optimizations:

1. Use importlib.util.find_spec() instead of __import__() in dependency
   checking. find_spec() only locates modules without executing them,
   avoiding loading all 36 dependencies (~222MB) into memory at startup.

2. Introduce shared aiohttp.ClientSession via httpclient module.
   Previously, every HTTP request created a new ClientSession, which
   creates a new TCPConnector and SSL context, loading system root
   certificates each time (~270MB total allocations observed via memray).
   Now all HTTP client code reuses shared sessions.

   - satori.py and coze_server_api/client.py are left unchanged as they
     create one session per adapter lifecycle (not per-request).

Profiling data (memray):
- Peak memory: 403MB
- SSL context creation: 270MB / 6.7M allocations (67% of total)
- Dependency import: 222MB (55% of peak)
- Expected reduction: 150-350MB at startup

* fix: remove unused aiohttp imports (ruff F401)

* style: ruff format
2026-02-27 20:09:03 +08:00
Junyan Qin
2dc5999583 fix: handle undefined values in DynamicFormItemComponent
- Updated BOOLEAN case to default to false when field.value is undefined.
- Updated SELECT case to default to an empty string when field.value is undefined.
2026-02-27 10:55:28 +08:00
Junyan Qin
73461814c9 fix: prevent infinite re-render loop in BotForm and DynamicFormComponent
- Updated BotForm to serialize adapter_config for stable useEffect dependency.
- Refactored DynamicFormComponent to track last emitted values, avoiding unnecessary re-renders when form values remain unchanged.
2026-02-27 10:52:19 +08:00
Guanchao Wang
210e5e50d3 fix: telegram send messsage (#2010) 2026-02-27 00:40:19 +08:00
Junyan Qin
4fd488b97a chore: Bump version to 4.8.6 in pyproject.toml, uv.lock, and __init__.py 2026-02-26 22:54:13 +08:00
Junyan Qin
422a34ead4 fix: plugins in recommendation cannot be installed 2026-02-26 22:53:29 +08:00
Junyan Qin
02a1036d63 chore: Bump version to 4.8.5 in pyproject.toml and __init__.py 2026-02-26 14:34:23 +08:00
Junyan Chin
2d837c9cb4 feat: add in-product survey system (#2008)
* feat: add in-product survey system

- SurveyManager: event-based trigger, Space API communication
- Trigger on first successful non-WebSocket response
- Backend API: /api/v1/survey/{pending,respond,dismiss}
- Frontend: floating survey widget with progressive questions
- Flat radio/checkbox style (not dropdown Select)

* fix: persist triggered survey events to disk across restarts

Store triggered events in data/survey_triggered_events.json so that
restarting the process doesn't re-query Space for already-triggered events.

* fix: use metadata table for survey event persistence instead of file

Store triggered events in the existing metadata KV table
(key='survey_triggered_events') instead of a standalone JSON file.

* fix: ruff format and prettier fixes
2026-02-26 13:50:14 +08:00
Junyan Chin
2ded774747 docs: add LangBot Cloud references to all READMEs (#2007) 2026-02-25 22:18:22 +08:00
Junyan Chin
d9a630b8c1 feat: add session message monitoring tab to bot detail dialog (#2005)
* feat: add session message monitoring tab to bot detail dialog

Add a new "Sessions" tab in the bot detail dialog that displays
sent & received messages grouped by sessions. Users can select
any session to view its messages in a chat-bubble style layout.

Backend changes:
- Add sessionId filter to monitoring messages endpoint
- Add role column to MonitoringMessage (user/assistant)
- Record bot responses in monitoring via record_query_response()
- Add DB migration (dbm019) for the new role column

Frontend changes:
- New BotSessionMonitor component with session list + message viewer
- Add Sessions sidebar tab to BotDetailDialog
- Add getBotSessions/getSessionMessages API methods to BackendClient
- Add i18n translations (en-US, zh-Hans, zh-Hant, ja-JP)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* refactor: remove outdated version comment from PipelineManager class

* fix: bump required_database_version to 19 to trigger monitoring_messages.role migration

* fix: prevent session message auto-scroll from pushing dialog content out of view

Replace scrollIntoView (which scrolls all ancestor containers) with
direct scrollTop manipulation on the ScrollArea viewport. This keeps
the scroll contained within the messages panel only.

* ui: redesign BotSessionMonitor with polished chat UI

- Wider session list (w-72) with avatar circles and cleaner layout
- Richer chat header with avatar, platform info, and active indicator
- User messages now use blue-500 (solid) instead of blue-100 for
  clear visual distinction
- Metadata (time, runner) shown on hover below bubbles, not inside
- Proper empty state illustrations for both panels
- Better spacing, rounded corners, and shadow treatment
- Consistent dark mode styling

* fix: infinite re-render loop in DynamicFormComponent

The useEffect depended on onSubmit which was a new closure every
parent render. Calling onSubmit inside the effect triggered parent
state update → re-render → new onSubmit ref → effect re-runs → loop.

Fix: use useRef to hold a stable reference to onSubmit, removing it
from the useEffect dependency array.

Also add DialogDescription to BotDetailDialog to suppress Radix
aria-describedby warning.

* fix: remove .html suffix from docs.langbot.app links (Mintlify migration)

* style: fix prettier and ruff formatting

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
2026-02-25 21:56:24 +08:00
Guanchao Wang
b8df0dbd7f feat: message aggregator (#1985)
* feat: aggregator

* fix: resolve deadlock, mutation, and safety issues in message aggregator

- Fix deadlock: don't await cancelled timer tasks inside the lock;
  _flush_buffer acquires the same lock, causing a deadlock cycle
- Fix message_event mutation: keep original message_event unmodified
  to preserve message_id/metadata for reply/quote; only pass merged
  message_chain separately
- Fix Plain positional arg: Plain('\n') → Plain(text='\n')
- Fix float() ValueError: wrap delay cast in try/except
- Add MAX_BUFFER_MESSAGES (10) cap to prevent unbounded buffer growth
- Default enabled to false to avoid surprising latency on upgrade
- Fix flush_all: cancel all timers under one lock acquisition, then
  flush outside the lock to avoid deadlock

---------

Co-authored-by: RockChinQ <rockchinq@gmail.com>
2026-02-25 14:20:34 +08:00
Dongze Yang
298437f352 feat(platform): add Forward message support for aiocqhttp adapter (#2003)
* feat(platform): add Forward message support for aiocqhttp adapter

- Add _send_forward_message method to send merged forward cards via OneBot API
- Support NapCat's send_forward_msg API with fallback to send_group_forward_msg
- Fix MessageChain deserialization for Forward messages in handler.py
- Properly deserialize nested ForwardMessageNode.message_chain to preserve data

This enables plugins to send QQ merged forward cards through the standard
LangBot send_message API using the Forward message component.

* style: fix ruff lint and format issues

- Remove f-string prefix from log message without placeholders
- Apply ruff format to aiocqhttp.py and handler.py

* refactor: remove custom deserializer, rely on SDK for Forward deserialization

- Remove _deserialize_message_chain from handler.py; use standard
  MessageChain.model_validate() (Forward handling fixed in SDK via
  langbot-app/langbot-plugin-sdk#38)
- Fix group_id type: use int instead of str for OneBot compatibility
- Add warning log when Forward message is used with non-group target

* chore: bump langbot-plugin to 0.2.7 (Forward deserialization fix)

---------

Co-authored-by: RockChinQ <rockchinq@gmail.com>
2026-02-25 14:03:17 +08:00
Dongze Yang
94d72c378c fix(web): emit initial form values on mount to prevent saving empty config (#2004)
DynamicFormComponent uses form.watch(callback) to notify parent of form
values, but react-hook-form's watch callback only fires on subsequent
changes, not on mount. This causes PluginForm's currentFormValues to
remain as {} if the user saves without modifying any field, overwriting
the existing plugin config with an empty object in the database.
2026-02-25 13:34:52 +08:00
fdc310
f09ba6a0e3 fix: Add the file upload function and optimize the media message proc… (#2002)
* fix: Add the file upload function and optimize the media message processing

* fix: Optimize the message processing logic, improve the concatenation of text elements and the sending of media messages

* fix: Simplify the file request construction and message processing logic to enhance code readability
2026-02-25 12:24:16 +08:00
Junyan Chin
1eda076b93 feat: add plugin recommendation lists to market page (#2001) 2026-02-24 21:24:36 +08:00
Junyan Qin
d6c10763a8 chore: Bump version to 4.8.4 and update langbot-plugin dependency to 0.2.6 2026-02-23 23:32:43 +08:00
Junyan Qin
9df50d2cab chore: Standardize section headers in multiple language README files 2026-02-23 17:16:18 +08:00
Junyan Qin
6c6b510a0a chore: Update logo in README files to new resource location 2026-02-23 17:01:37 +08:00
Junyan Qin
063dc6fe97 feat: Add unsaved changes tracking to PipelineFormComponent 2026-02-23 14:36:04 +08:00
Junyan Chin
42caae1bcf feat: Implement extension and bot limitations across services and UI (#1991)
- Added checks for maximum allowed extensions, bots, and pipelines in the backend services (PluginsRouterGroup, BotService, MCPService, PipelineService).
- Updated system configuration to include limitation settings for max_bots, max_pipelines, and max_extensions.
- Enhanced frontend components to handle limitations, providing user feedback when limits are reached.
- Added internationalization support for limitation messages in English, Japanese, Simplified Chinese, and Traditional Chinese.
2026-02-22 17:25:45 +08:00
Typer_Body
aa09a27a63 Merge pull request #1975 from TyperBody/master
Add new platform named satori
2026-02-21 23:30:28 +08:00
Typer_Body
96e32a10e2 Update satori.py 2026-02-21 23:18:47 +08:00
Typer_Body
9a9f0eaa7d Update satori.py 2026-02-21 23:14:07 +08:00
Typer_Body
f5dea3c64c Update satori.py 2026-02-21 03:15:21 +08:00
Copilot
e213046302 fix: correct license declaration in OpenAPI spec from AGPL-3.0 to Apache-2.0 (#1988)
* Initial plan

* fix: update license from AGPL-3.0 to Apache-2.0 in service-api-openapi.json

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
2026-02-19 21:10:03 +08:00
Typer_Body
41d31d77d8 Change type from int to integer in satori.yaml 2026-02-18 18:07:57 +08:00
Typer_Body
6fb7fc80cc Add files via upload 2026-02-18 17:58:56 +08:00
Typer_Body
7bee5ff2f8 ruff 2026-02-18 17:43:41 +08:00
Typer_Body
afe82ebdfd Update print statement from 'Hello' to 'Goodbye' 2026-02-18 17:25:29 +08:00
Typer_Body
65c10ea54b Update fmt.Println message from 'Hello' to 'Goodbye' 2026-02-18 17:12:20 +08:00
Typer_Body
ff0023c6c2 Merge branch 'master' into master 2026-02-18 17:02:16 +08:00
Typer_Body
0e17d869ab Update README_RU.md 2026-02-18 16:53:56 +08:00
Typer_Body
7ec41bb91a Add Satori support to the README_KO.md 2026-02-18 16:51:16 +08:00
Typer_Body
da164c214e Update README_VI.md 2026-02-18 16:50:29 +08:00
Typer_Body
32a5de9bbb Add Satori support to README_TW.md 2026-02-18 16:49:53 +08:00
Typer_Body
1b12b1fc35 Update README.md 2026-02-18 16:49:02 +08:00
Typer_Body
caa1ed9d6a Delete README_EN.md 2026-02-18 16:47:59 +08:00
Typer_Body
05f40e72ff Add files via upload 2026-02-18 16:46:53 +08:00
Guanchao Wang
27fb22d7be Merge pull request #1966 from langbot-app/feat/export-history
feat: support export message history
2026-02-17 22:33:07 +08:00
wangcham
ca504384d2 Merge branch 'feat/export-history' of https://github.com/langbot-app/LangBot into feat/export-history 2026-02-17 22:22:33 +08:00
wangcham
b7e1e43fbd fix: some errors 2026-02-17 22:21:53 +08:00
Junyan Chin
deabb19389 Update src/langbot/pkg/platform/sources/satori.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-17 22:20:27 +08:00
Junyan Chin
809035daac Update src/langbot/pkg/platform/sources/satori.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-17 22:19:51 +08:00
RockChinQ
1eac87b89f Update README files across multiple languages to reflect new platform capabilities and improve clarity. Enhanced descriptions for AI bot development and deployment, and added links for further documentation. 2026-02-17 15:52:13 +08:00
RockChinQ
70a2d137f0 Replace English README with Chinese version and update language links across all README files 2026-02-17 15:42:33 +08:00
Junyan Chin
c72b785c1f Update bug-report_en.yml 2026-02-16 14:07:50 +08:00
Junyan Chin
8588199640 Revise bug report instructions for clarity
Updated bug report template to request export files for external platforms.
2026-02-16 14:07:28 +08:00
dependabot[bot]
2e42cd2faf chore(deps): bump axios from 1.13.4 to 1.13.5 in /web (#1979)
Bumps [axios](https://github.com/axios/axios) from 1.13.4 to 1.13.5.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.4...v1.13.5)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.13.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-15 16:18:02 +08:00
dependabot[bot]
7b3555af45 chore(deps): bump cryptography from 46.0.4 to 46.0.5 (#1978)
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.4 to 46.0.5.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/46.0.4...46.0.5)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-15 16:16:47 +08:00
dependabot[bot]
e12a77ca05 chore(deps): bump pillow from 12.1.0 to 12.1.1 (#1977)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 12.1.0 to 12.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/12.1.0...12.1.1)

---
updated-dependencies:
- dependency-name: pillow
  dependency-version: 12.1.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-15 16:15:17 +08:00
Junyan Qin
9ce3ad8300 fix: update JSX setting in TypeScript configuration to use react-jsx 2026-02-15 15:07:35 +08:00
Typer_Body
1f60d9c3d6 Add files via upload 2026-02-12 22:27:51 +08:00
Typer_Body
d855d29c15 Add files via upload 2026-02-12 22:25:14 +08:00
Typer_Body
18083e9160 Update README_TW.md 2026-02-12 22:12:53 +08:00
Typer_Body
7f9e8ecac1 Add files via upload 2026-02-12 22:12:28 +08:00
Typer_Body
995c852f0a Add Satori to the supported platforms list 2026-02-12 02:52:26 +08:00
Typer_Body
682962cc47 Add Satori to supported platforms list 2026-02-12 02:51:54 +08:00
Typer_Body
24e90a7f9b Add Satori to the supported platforms list 2026-02-12 02:51:37 +08:00
Typer_Body
6a5a7182db Add Satori to the supported LLMs list 2026-02-12 02:51:15 +08:00
Typer_Body
c581c8e809 Add Satori to supported platforms list 2026-02-12 02:50:59 +08:00
Typer_Body
ffd2423920 Add Satori to communication tools list 2026-02-12 02:50:42 +08:00
Typer_Body
c388339bd5 Update README_TW.md 2026-02-12 02:49:21 +08:00
Typer_Body
28492a62bb Update README_EN.md 2026-02-12 02:48:58 +08:00
Typer_Body
6a687ebeeb Update README.md 2026-02-12 02:48:31 +08:00
Typer_Body
29dfae1518 Add files via upload 2026-02-12 02:44:47 +08:00
Typer_Body
791877d391 Merge branch 'langbot-app:master' into master 2026-02-12 02:40:57 +08:00
Copilot
8fd0c3cc18 fix(web): Handle null/undefined starCount and installCount (#1970)
* Initial plan

* fix(web): Handle null/undefined values for starCount and installCount

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* fix(web): Hide star count badge when API fails instead of showing '0'

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
2026-02-11 16:55:32 +08:00
wangcham
10dd8c86d0 fix: frontend lint 2026-02-09 10:48:22 +08:00
wangcham
c2574bdd3a fix: lint error 2026-02-09 01:01:20 +08:00
wangcham
d2d7892325 fix: lint 2026-02-09 00:41:34 +08:00
WangCham
6d858475d7 feat: support export message history 2026-02-08 10:19:27 +08:00
Junyan Qin
59d55b382d chore: bump version to 4.8.3 in pyproject.toml and uv.lock 2026-02-02 01:07:46 +08:00
Copilot
8c17e55913 feat: Add Telegram voice message receiving support (#1948)
* Initial plan

* feat: add Telegram voice message receiving support

- Add filters.VOICE to Telegram message handler to capture voice messages
- Implement voice message processing in target2yiri converter
- Download voice files from Telegram API and convert to base64
- Create platform_message.Voice component with proper mime type and duration
- Maintain compatibility with existing text, photo, and command messages

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* chore: format code

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2026-02-02 00:51:49 +08:00
RockChinQ
af509fe61f chore: sync deps 2026-02-01 23:02:09 +08:00
Copilot
87e2a2099a fix: display loading animation in content area only (#1955)
* Initial plan

* fix: change loading animation to display only in content area instead of full screen

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
2026-02-01 22:51:10 +08:00
Copilot
3f22f62332 feat: add monitoring tab to pipeline dialog for in-context error debugging (#1953)
* Initial plan

* Add monitoring tab to pipeline dialog with i18n support

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Fix prettier formatting for monitoring tab component

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Fix code review issues: use functional state updates and add comment for delay

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Update dependencies and enhance monitoring tab functionality

- Updated various package versions in pnpm-lock.yaml for improved compatibility and performance.
- Refactored PipelineDetailDialog to streamline WebSocket connection status display.
- Enhanced PipelineMonitoringTab to support navigation to detailed logs and improved UI elements.
- Added i18n support for 'Detailed Logs' in English, Japanese, Simplified Chinese, and Traditional Chinese locales.

* Fix lint errors: remove unused Button import and format en-US.ts

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
Co-authored-by: RockChinQ <rockchinq@gmail.com>
2026-01-31 22:00:37 +08:00
fdc310
d1ee5f931a chore(deps): update dashscope version to 1.25.10 in pyproject.toml (#1951)
feat: enable thinking feature in DashScopeAPIRunner for improved conversation handling
2026-01-31 20:31:37 +08:00
fdc310
35506dd2bb feat: add card auto layout configuration for DingTalk adapter (#1952)
* feat: add card auto layout configuration for DingTalk adapter

* fix: correct card auto layout configuration key and improve related logic

* fix: simplify card auto layout configuration logic in create_and_card method

* fix: correct card auto layout key in DingTalk migration configuration

* fix: correct migration class name for DingTalk card auto layout

* fix: update migration version for DingTalk card auto layout

* fix: correct key name for card auto layout in DingTalk configuration

* fix: improve formatting and consistency in DingTalk card auto layout methods
2026-01-31 20:31:01 +08:00
fdc310
2f06321ebf fix: Fix the file URL processing logic to support complete URLs (#1950) 2026-01-31 20:30:46 +08:00
Junyan Qin
023281ae56 fix: ensure content extraction from messages includes only valid text entries 2026-01-31 13:51:17 +08:00
Junyan Qin
50dff55217 feat: enhance LLM model creation with optional default pipeline setting
- Updated create_llm_model method to include auto_set_to_default_pipeline parameter.
- Adjusted ModelManager to set auto_set_to_default_pipeline to False when creating models.
- Improved logic for setting the default pipeline model based on the new parameter.
2026-01-31 13:24:33 +08:00
Junyan Qin
3204292360 chore: bump version to 4.8.2 and update langbot-plugin and pyseekdb versions in uv.lock 2026-01-31 12:54:05 +08:00
Junyan Qin
e0d72969e3 chore(deps): update langbot-plugin version to 0.2.5 in pyproject.toml 2026-01-30 17:31:21 +08:00
Junyan Qin
a65b7ad413 chore(deps): update pyseekdb version to 1.0.0b7 in pyproject.toml 2026-01-30 13:39:36 +08:00
Junyan Qin
45df44e01b chore: update uv.lock 2026-01-30 12:42:21 +08:00
Junyan Qin
d8addb105a chore: update .gitignore and add uv.lock for dependency management 2026-01-30 12:32:39 +08:00
Junyan Qin
f17ccad665 chore: update TypeScript configuration for improved compatibility and structure 2026-01-30 12:15:19 +08:00
Junyan Qin
120ceb0b55 chore: update linting configuration to use eslint directly 2026-01-30 12:03:43 +08:00
dependabot[bot]
8a6f80a181 chore(deps): bump lodash from 4.17.21 to 4.17.23 in /web (#1944)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-30 11:25:16 +08:00
dependabot[bot]
b19e468668 chore(deps): bump next from 15.5.9 to 16.1.5 in /web (#1943)
Bumps [next](https://github.com/vercel/next.js) from 15.5.9 to 16.1.5.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.9...v16.1.5)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.1.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-30 11:20:08 +08:00
Junyan Qin
aeac79e1b3 feat: add tag filtering functionality to Plugin Market
- Introduced TagsFilter component for selecting and filtering plugins by tags.
- Updated PluginMarketComponent to handle tag selection and display.
- Enhanced PluginMarketCardComponent to show selected tags.
- Modified CloudServiceClient to fetch available tags from the API.
- Updated localization files to support new tag-related strings.
2026-01-29 16:08:05 +08:00
Junyan Qin
b89a240250 feat: implement LoadingSpinner component and replace existing loaders across the application 2026-01-29 15:24:23 +08:00
Junyan Qin
13f42857f5 perf: detailed control of models service displaying 2026-01-27 22:44:58 +08:00
Junyan Qin
61f3f31edc chore: bump version to 4.8.1 2026-01-27 20:33:55 +08:00
Junyan Qin
3663d9dc10 style: adjust margin in PipelineDetailDialog for improved button alignment 2026-01-27 20:33:17 +08:00
Guanchao Wang
89ec86c530 fix: issue 1936 (#1937) 2026-01-27 20:28:19 +08:00
Junyan Qin
d9ba2a17ff chore: bump version to 4.8.0 2026-01-26 21:12:56 +08:00
Junyan Qin
c4ea6188f9 chore: update layout description to reflect production-grade capabilities for IM bot integration 2026-01-26 21:09:59 +08:00
Guanchao Wang
5d9f6ec763 Feat/monitor (#1928)
* feat: add monitor

* feat: fix tab

* feat: work

* feat: not reliable monitor

* feat: enhance monitoring page layout with integrated filters and refresh button

* feat: add support for runner recording

* feat: add jump button & alignment

* feat: new

* fix: not show query variables in local agent

* fix: pnpm lint and python ruff check

* fix: ruff fromat

* chore: remove unnecessary migration

* style: optimize monitoring page layout and fix sticky filter issues

- Enhanced metric cards with gradient backgrounds and hover effects
- Increased traffic chart height from 200px to 300px
- Adjusted grid layout and spacing for better visual appeal
- Fixed sticky filter area to properly cover parent padding without transparent gaps
- Used negative margins and positioning to eliminate scrolling artifacts
- Matched padding/margins with other pages (pipelines, bots) for consistency
- Removed duplicate title/subtitle from page content
- Added cursor-pointer styling to tab triggers
- Removed border between tab list and tab content

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: apply prettier formatting to monitoring components

- Fixed indentation and spacing in MetricCard.tsx
- Fixed formatting in TrafficChart.tsx
- Applied prettier formatting to page.tsx

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: update HomeSidebar to trigger action on child selection and localize monitoring titles

* refactor: streamline LLM and embedding invocation methods

* feat: add embedding model monitor

* fix: database version

* chore: simplify pnpm-lock.yaml formatting

---------

Co-authored-by: Junyan Qin <rockchinq@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-26 21:08:23 +08:00
Junyan Qin (Chin)
b73847f1a6 feat: add emoji support to knowledge bases and pipelines (#1935)
* feat: add emoji support to knowledge bases and pipelines

* feat: add optional emoji property to ExternalKBCardVO for enhanced knowledge base representation
2026-01-26 17:37:35 +08:00
Typer_Body
d6e1e79f07 fix: potential copy action bug on windows (#1931)
* fix a bag updata

* Update page.tsx

* Update page.tsx

* Append text area to body for selection

* Update page.tsx

* Update mcp.py
2026-01-25 15:40:11 +08:00
Junyan Qin
525008b8b2 docs: update feature descriptions in multiple language READMEs to include Langflow integration and enhance clarity on production-grade features 2026-01-25 15:28:15 +08:00
Junyan Qin (Chin)
bbf77bac4c feat(user): update Space model provider API keys in UserService (#1932) 2026-01-25 14:15:25 +08:00
Typer_Body
f4ae829f59 Update mcp.py 2026-01-25 01:49:53 +08:00
Typer_Body
3af8c13fab Update page.tsx 2026-01-25 01:38:17 +08:00
Typer_Body
a8f7924867 Append text area to body for selection 2026-01-25 01:37:41 +08:00
Typer_Body
77047e87d6 Update page.tsx 2026-01-25 01:37:15 +08:00
Typer_Body
24d865bcd3 Update page.tsx 2026-01-25 01:36:51 +08:00
Typer_Body
81ec7c201c Merge branch 'langbot-app:master' into master 2026-01-25 01:30:21 +08:00
Junyan Qin (Chin)
fc6e414be4 feat: add GitHub Actions workflow for linting with Ruff (#1929)
* feat: add GitHub Actions workflow for linting with Ruff

* refactor: rename lint job and add formatting step to Ruff workflow

* chore: run ruff format

* chore: rename Ruff lint job to 'Lint' and add frontend linting workflow
2026-01-23 13:43:12 +08:00
Junyan Qin
e60cb6ad0e fix: ruff check errors 2026-01-23 13:30:44 +08:00
Junyan Qin
c90f2d6a12 chore: update mcp dependency version to 1.25.0 2026-01-20 01:59:19 +08:00
Junyan Qin
fe8a738cd7 fix(i18n): update apiKeyCreatedMessage for clarity across multiple languages 2026-01-20 01:53:49 +08:00
Tiankai Ma
604cc53973 fix(localagent): allow empty func arg (#1921) 2026-01-19 23:42:47 +08:00
Tiankai Ma
195b694ecc feat(telegram): threaded mode support (#1920)
* feat(telegram): reply in threaded mode

* feat(telegram): thread-level isolation
2026-01-19 23:42:17 +08:00
Typer_Body
ee2d4e3ab9 fix a bag updata 2026-01-19 00:05:21 +08:00
Tiankai Ma
d21f23beee fix(telegram): set reply_to_message_id correctly (#1918) 2026-01-15 18:09:57 +08:00
Junyan Qin
558587883b chore: update project version to 4.7.2 2026-01-13 14:02:00 +08:00
Junyan Qin
2e6a1daf4f feat(mcp): extend mode options in MCPCardVO to include 'http' 2026-01-13 13:59:59 +08:00
Tiankai Ma
1fc5e75f93 feat(mcp): add streamable HTTP and stdio (#1911)
* feat(mcp): add streamable HTTP

alongside with frontend UI change, w/ support for stdio

* fix(mcp): address copilot reviews

* Update src/langbot/pkg/provider/tools/loaders/mcp.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: resolve copilot reviews

* fix: Message -> MessageChunk

* feat: upgrade mcp module

* feat: add i18n

* feat(mcp): enhance MCPCardComponent with mode badge and reorder select items in MCPFormDialog

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: WangCham <651122857@qq.com>
Co-authored-by: Junyan Qin (Chin) <rockchinq@gmail.com>
2026-01-13 13:50:06 +08:00
fdc310
a332206ba3 fix: When the deletion of the thinking chain is activated, since the "continue" is triggered as soon as the thinking begins, it causes a bug in the subsequent judgment that breaks out of the loop impression. (#1913) 2026-01-12 00:14:39 +08:00
Junyan Qin
8e620dc635 fix: remove unreachable assertion in ChatMessageHandler to improve error handling 2026-01-09 23:46:43 +08:00
Junyan Qin
c9a21ebace fix: improve error handling in ChatMessageHandler 2026-01-09 23:23:53 +08:00
Junyan Qin
a05cdcac50 chore: update project version to 4.7.1 2026-01-09 21:52:08 +08:00
Junyan Qin
ecfb2bfb34 chore: add type hints for ap in telemetry.py 2026-01-09 21:50:43 +08:00
Guanchao Wang
e17dba0a98 fix: testing mcp server (#1912) 2026-01-09 18:39:40 +08:00
Hadong
6b138943ce feat(milvus): milvus related updates (#1908)
- Add Milvus db_name configuration and client parameter support.
- change kb_data uuid for Milvus. 3. add MAX_BATCH_SIZE for openai.
- support more vector_size.
2026-01-09 16:03:43 +08:00
fdc310
eb0e6aff68 feat: add telemetry support for query execution tracking and configur… (#1900)
* feat: add telemetry support for query execution tracking and configuration

* feat: integrate telemetry manager and enable telemetry data sending

* feat: integrate telemetry manager and enhance error handling for telemetry sending

* feat: update telemetry configuration to use 'space' instead of 'telemetry' and adjust related parameters

* feat: integrate telemetry manager and enable telemetry data sending

* feat: integrate telemetry manager and enhance error handling for telemetry sending

* feat: add instance id

* feat: enhance telemetry management with asynchronous task handling and improve model retrieval caching

---------

Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2026-01-09 15:50:44 +08:00
Junyan Qin
4d0095626a fix: update docker-compose command to include --no-sync option for improved runtime behavior 2026-01-08 11:30:25 +08:00
Junyan Qin
aa0a501ade fix: bug in bind space account in models dialog 2026-01-05 20:53:35 +08:00
Junyan Qin
68ef7bd2c4 chore: update project version to 4.7.0 and revise description for clarity 2026-01-05 20:06:01 +08:00
Junyan Qin
61dc5de085 fix: update help links in sidebar configuration to reflect new usage paths and add Japanese translations 2026-01-05 18:45:35 +08:00
Junyan Qin
63bdd71e22 fix: update models_gateway_api_url to include version in cloud service configuration 2026-01-05 17:58:50 +08:00
Junyan Qin
9ea5b50802 refactor: enhance layout and styling of ModelsDialog component for improved usability 2026-01-05 17:58:01 +08:00
Jinzhe Zeng
1cd586634d fix: split Wecom messages exceeding 2048-byte limit (#1901)
Co-authored-by: Oracle Public Cloud User <opc@arm1.subnet.vcn.oraclevcn.com>
2026-01-05 15:04:46 +08:00
Junyan Qin
45bedbe70e fix: update QQ Group link in README to the new group ID 2026-01-05 10:20:42 +08:00
Junyan Qin (Chin)
f7f1dde7b5 Merge pull request #1894 from langbot-app/feat/maas-support
refactor: model config dialog and introduce LangBot Models service integration
2026-01-03 15:47:23 +08:00
Junyan Qin
ba06555078 refactor: remove SQLite compatibility check for column cleanup in DB migration script 2026-01-03 15:43:40 +08:00
Junyan Qin
840fa39979 feat: add informational popover to registration page with tips on using Space for account authentication 2026-01-03 15:26:24 +08:00
Junyan Qin
b295416e6c fix: adjust ModelsDialog component to set a maximum width for better layout consistency 2026-01-03 01:06:17 +08:00
Junyan Qin
914f77ff37 refactor: standardize error handling across components by utilizing CustomApiError for improved error messaging 2026-01-03 00:56:25 +08:00
Junyan Qin
b0b7b914d8 feat: update README files to include new links for API integration, plugin market, and roadmap across multiple languages 2026-01-01 22:11:43 +08:00
Junyan Qin
12713aad45 feat: migrate cloud service URL configuration and update database version to 17 2026-01-01 21:40:55 +08:00
Junyan Qin
02e12cc1e4 feat: implement account email mismatch error handling and improve user feedback in authentication flows 2026-01-01 17:01:32 +08:00
Junyan Qin
61f08f3218 feat: add disable_models_service configuration to manage model service availability and update related components 2026-01-01 15:40:39 +08:00
Junyan Qin
75c2a063cc refactor: remove providerUuid prop from model components and enhance provider deletion confirmation UI 2026-01-01 15:07:37 +08:00
Junyan Qin
b4773c4e48 refactor: update model management components and enhance provider functionality 2026-01-01 14:58:06 +08:00
Junyan Qin (Chin)
fb73da8735 Merge branch 'master' into feat/maas-support 2026-01-01 13:07:45 +08:00
Junyan Qin
679e549b1d feat: implement loading states in SpaceOAuthCallback and HomeSidebar components using Suspense 2026-01-01 13:06:04 +08:00
Junyan Qin
898144e9f4 fix: remove unused HoverCard imports from DynamicFormItemComponent and clean up ModelsDialog constants 2026-01-01 12:53:39 +08:00
Junyan Qin
b99c5561fc fix: update cloud service URL retrieval and enhance model synchronization error handling 2026-01-01 12:50:26 +08:00
Copilot
b2f4b91979 perf: replace copy button toast notifications with checkmark feedback (#1898)
* Initial plan

* Replace copy button toast notifications with checkmark visual feedback

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Complete copy button checkmark feedback implementation

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* revert pnpm-lock.yaml

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2026-01-01 11:53:13 +08:00
Junyan Qin
4528000fc4 refactor: model management 2026-01-01 02:00:24 +08:00
Junyan Qin
96e40eaf25 feat: enhance model creation with UUID preservation option and implement Space model synchronization in ModelManager 2025-12-31 22:25:07 +08:00
Junyan Qin
197258ae91 feat: add LangBot Space ChatCompletions requester and integrate with ModelsDialog and EmbeddingForm components 2025-12-30 21:52:52 +08:00
Junyan Qin
19f417174c feat: implement SpaceService for OAuth handling and user management, refactor UserService to utilize new service methods 2025-12-29 22:43:19 +08:00
Junyan Qin
9c82eeddeb feat: add endpoint for retrieving user space credits and implement caching mechanism in UserService 2025-12-29 22:23:11 +08:00
Junyan Qin
f11e01b549 refactor: rename 'allow_change_password' to 'allow_modify_login_info' and update related logic across the application 2025-12-29 21:14:05 +08:00
Junyan Qin
863b26c3fa refactor: update column drop logic in DBMigrateModelProviderRefactor for PostgreSQL compatibility 2025-12-29 20:42:06 +08:00
Junyan Qin
b788858f9e fix: handle case of empty token list in TokenManager to prevent errors 2025-12-29 12:18:45 +08:00
Junyan Qin
de8a7df6c2 feat: implement instance ID management and integrate with OAuth token exchange 2025-12-29 00:35:31 +08:00
Junyan Qin
ba5b481617 refactor: simplify theme toggle implementation in HomeSidebar and ThemeToggle components 2025-12-28 22:43:05 +08:00
Junyan Qin
07ad846e96 feat: update dependencies and enhance account settings dialog with password management and improved UI elements 2025-12-28 22:38:11 +08:00
Copilot
30945aafdd feat: support configurable WeCom API base URL for reverse proxy deployment (#1890)
* Initial plan

* Add api_base_url support to WeCom API libraries and adapters

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Add api_base_url parameter to OAClient and adapters for Official Account and WeCom APIs

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-12-28 21:04:55 +08:00
Junyan Qin
24c15b4479 feat: implement account settings dialog for managing user passwords and binding Space accounts 2025-12-26 23:20:51 +08:00
Junyan Qin
1d4c5bbdf1 feat: enhance model abilities display in DynamicFormItem and ModelsDialog components with icons for vision and function call 2025-12-26 20:57:12 +08:00
Junyan Qin
57fcec011d feat: refactor model management to introduce provider structure, enhancing model organization and retrieval 2025-12-26 20:27:33 +08:00
Junyan Qin
455e3db28d feat: add Radix UI collapsible component for enhanced UI interactions 2025-12-26 00:49:35 +08:00
Junyan Qin
8caab43b00 feat: add Space integration for user authentication and model management with OAuth support 2025-12-26 00:35:47 +08:00
Junyan Qin
7479545339 feat: implement models dialog for managing LLM and embedding models with dynamic URL handling 2025-12-25 20:54:00 +08:00
Junyan Qin
10ee30695a feat: add error handling and alert display for model testing in EmbeddingForm and LLMForm 2025-12-24 16:12:41 +08:00
Junyan Qin
a9a262eaae feat: add new version notification dialog and version comparison logic 2025-12-24 12:43:52 +08:00
Junyan Qin
a8594b76cd fix: enable extra_args in LLMModelsService for model testing 2025-12-23 21:03:45 +08:00
Junyan Qin
11ee0fef5d chore: update Python versions in CI workflow 2025-12-23 14:27:09 +08:00
Junyan Qin
9a9ba34717 chore: bump version v4.6.5 2025-12-23 14:26:52 +08:00
Junyan Qin
312e47bf46 chore: bump langbot-plugin to 0.2.4 2025-12-23 14:22:13 +08:00
Junyan Qin
628865fd06 fix: add timeout to image fetching in get_qq_image_bytes function (#1859) 2025-12-23 14:17:16 +08:00
Junyan Qin
806a03cd53 fix: dingtalk adapter lifecycle mgm issues (#1844, #1853) 2025-12-23 14:00:41 +08:00
Junyan Qin
24bd90fcf6 fix: alter_user_message typing issues 2025-12-23 13:24:52 +08:00
Junyan Qin
d2765577c8 chore: provide '--no-sync' arg in dockerfile 2025-12-23 12:39:42 +08:00
fdc310
60ca688bcb Fix/Incomplete JSON data returned by N8N streaming data causes the loss of chunks. (#1880)
* fix: Incomplete JSON data returned by N8N streaming data causes the loss of chunks.
2025-12-23 09:42:26 +08:00
ICE
76d8eea41d fix: group bot at rule (#1882) 2025-12-22 20:20:41 +08:00
Junyan Qin
635c3a04d8 perf: ja-JP translation for New 2025-12-22 18:46:15 +08:00
Junyan Qin
dde97abe38 feat: enhance HomeSidebar with new integration options and updated translations 2025-12-22 18:43:19 +08:00
Copilot
90a22d894d fix: prevent memory overflow from excessive logging in streaming and query processing (#1879)
* Initial plan

* fix: reduce excessive logging to prevent memory overflow

- Add log file rotation (10MB max per file, 5 backups)
- Reduce streaming response logging (every 10th chunk instead of every chunk)
- Remove debug logging from controller tight loop
- Add summary logging after streaming completes

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* refactor: address code review feedback

- Extract log rotation config to module-level constants
- Keep first streaming chunk at INFO level for connection debugging
- Use DEBUG level for subsequent chunks

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* style: fix code formatting whitespace

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
2025-12-22 18:25:24 +08:00
Junyan Qin
88ef9cd6ae chore: remove platform field from docker-compose.yaml 2025-12-21 20:31:09 +08:00
fdc310
e3595b5c57 Feat/lark file and audio (#1874)
* fix: n8n streaming no sequence bug

* feat:add lark file and audio
fix: webhook

* feat:add lark file and audio
fix: webhook

* 更新 n8nsvapi.py

* del : print and log
2025-12-21 01:30:05 +08:00
Junyan Qin (Chin)
ce82f87e43 feat: add SeekDB vector database support for knowledge bases (#1814)
* feat: add SeekDB vector database support for knowledge bases

This commit adds complete integration of OceanBase's SeekDB as a vector
database option for LangBot's knowledge base feature.

## Changes

### Core Implementation
- Add SeekDB adapter implementing VectorDatabase interface
  - Support both embedded and server deployment modes
  - HNSW indexing with cosine similarity
  - Async operations with error handling
  - Comprehensive logging

### System Integration
- Register SeekDB in VectorDBManager
- Add pyseekdb>=0.1.0 dependency
- Add SeekDB configuration template
- Update README with vector database section

### Documentation
- Complete integration guide with platform compatibility warnings
- Configuration examples for all deployment modes
- Troubleshooting guide for common issues
- Code examples demonstrating usage patterns
- Comprehensive test reports and status documentation

## Testing

Architecture validated end-to-end using ChromaDB:
- File upload → parsing → chunking → embedding → storage
- 828 bytes → 3 chunks → 3 vectors stored successfully
- BGE-M3 model (384 dimensions)
- Status: Completed 

## Platform Compatibility

### Embedded Mode
-  Linux: Fully supported
-  macOS: Not supported (pylibseekdb is Linux-only)
-  Windows: Not supported (pylibseekdb is Linux-only)

### Server Mode
-  Linux: Fully supported
- ⚠️ macOS: Known issue (oceanbase/seekdb#36)
- ⚠️ Windows: Untested

### Remote Connection
-  All platforms supported

## Known Issues

macOS Docker server mode affected by upstream bug:
https://github.com/oceanbase/seekdb/issues/36

Workaround: Use ChromaDB/Qdrant or connect to remote SeekDB server.

## Files Added
- src/langbot/pkg/vector/vdbs/seekdb.py
- docs/SEEKDB_INTEGRATION.md
- examples/seekdb_example.py
- SEEKDB_INTEGRATION_SUMMARY.md
- SEEKDB_INTEGRATION_COMPLETE.md
- SEEKDB_TEST_STATUS.md
- SEEKDB_FINAL_SUMMARY.md
- SEEKDB_INTEGRATION_DONE.md
- GITHUB_ISSUE_36_COMMENT.md

## Files Modified
- src/langbot/pkg/vector/mgr.py
- src/langbot/pkg/vector/vdbs/__init__.py
- pyproject.toml
- src/langbot/templates/config.yaml
- README.md
- README_EN.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* chore: remove unused docs

* feature: minimal seekdb change (#1866)

* feat: add SeekDB embedding requester and configuration

This commit introduces a new SeekDB embedding requester, which utilizes the local embedding function from pyseekdb. It includes the necessary Python implementation and a corresponding YAML configuration file for integration. Additionally, a new SVG icon for SeekDB is added to enhance the visual representation in the UI.

* fix: update EmbeddingForm to conditionally render URL field based on model provider

This commit modifies the EmbeddingForm component to conditionally display the URL input field only when the current model provider is not 'seekdb-embedding'. Additionally, it updates the condition for rendering the API key field to exclude both 'ollama-chat' and 'seekdb-embedding' providers.

* chore: update Python version requirement in pyproject.toml to support Python 3.11

* fix: add config default value, when it makes fronted not show spec

* fix: seekdb.py clean metadata. change api

* fix: enhance error handling in SeekDB embedding initialization

This commit adds improved error handling to the SeekDB embedding function. It ensures that a RuntimeError is raised if the embedding function fails to initialize, and wraps the embedding call in a try-except block to catch and raise a RequesterError with a descriptive message in case of failure.

* refactor: update SeekDB database management to use AdminClient

This commit refactors the SeekDB database management logic to utilize the AdminClient for database operations. It replaces the previous temp_client with admin_client for listing and creating databases, ensuring a more robust interaction with the SeekDB API.

* refactor: update SeekDB embedding model initialization to use task manager

This commit refactors the SeekDB embedding model initialization by replacing the direct asyncio task creation with the task manager's create_task method. This change enhances task management and provides a clearer naming convention for the embedding model initialization task.

* perf: integration

* chore: remove unnecessary files

* fix: linter errors

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
Co-authored-by: 名为a的全局变量 <1051233107@qq.com>
2025-12-20 23:40:30 +08:00
fdc310
854b291c5a fix: n8n streaming no sequence bug (#1873) 2025-12-20 00:03:05 +08:00
Junyan Qin
9780fd059c chore: add back arm64 docker image (#1871) 2025-12-19 23:44:28 +08:00
Junyan Qin
adc65f66eb fix: pipeline duplication bug 2025-12-19 23:27:18 +08:00
Copilot
ae772074a1 feat: Add configurable password change toggle via system.allow_change_password (#1869)
* Initial plan

* Add password change toggle feature with config flag

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Feature implementation complete and validated

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* chore: remove lock

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-12-18 15:14:03 +08:00
dependabot[bot]
16c1e9edd1 chore(deps): bump next from 15.5.7 to 15.5.9 in /web (#1868)
Bumps [next](https://github.com/vercel/next.js) from 15.5.7 to 15.5.9.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.7...v15.5.9)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.5.9
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 12:21:02 +08:00
sheetung
3ab9ffb7b7 feat(plugins): add plugin new version detection (#1865)
* feat(plugins): 添加插件更新检测功能

* perf: card style

---------

Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-12-18 12:17:25 +08:00
Copilot
82e2123fe7 Fix Dify v1.11.0 conversation_id UUID validation error (#1860)
* Initial plan

* Fix Dify v1.11.0 conversation_id UUID validation error

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
2025-12-12 18:35:47 +08:00
Junyan Qin
7a65f3d2f4 chore: update AGENTS.md 2025-12-12 17:35:02 +08:00
Junyan Qin
b5b5d499e5 feat: add back streaming switch for web chat 2025-12-11 18:54:16 +08:00
Hadong
173f9e9c30 feat(lark): 支持商店应用机器人 (#1855)
* feat(lark): 支持商店应用机器人

* feat(lark): app_type改成select模式,修复select配置无效,按照copilot建议隐藏log敏感信息

* fix: KeyError for backward compatibility

---------

Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-12-11 16:54:28 +08:00
Junyan Qin
a610c72067 chore: bump version 4.6.4 2025-12-10 14:22:57 +08:00
Junyan Qin
d210a49fae fix: react cve 2025-12-10 14:21:41 +08:00
Junyan Qin
b015c248ea chore: bump langbot-plugin to 0.2.3 2025-12-10 14:02:23 +08:00
Hadong
4a559ea770 feat: 飞书适配器加入“机器人进群欢迎语”配置 (#1852)
* feat(lark): 支持机器人进群发送欢迎消息

* perf: existence check and indent

---------

Co-authored-by: donghao <donghao@patsnap.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-12-09 16:37:03 +08:00
fdc310
e306751863 feat:add lark ubified_webhook and The configuration for the front-end regarding whether to enable webhooks for Lark is displayed. (#1850) 2025-12-09 13:30:45 +08:00
Junyan Qin
2f51f5f33e docs: apply README changes to all languages 2025-12-06 22:34:48 +08:00
Junyan Qin (Chin)
74a2a61fc1 Update README with new features and headings
Added a new heading and additional features to the README.
2025-12-06 22:21:49 +08:00
Junyan Qin
b6c0345b3e chore: bump version 4.6.3 2025-12-06 21:29:28 +08:00
Junyan Qin (Chin)
6421a6f5cb Feat/complete adapter features (#1849)
* feat: add voice and file supports for wecom

* feat: add   and  in query variables

* feat: supports for lark recv file message

* feat: kook recv voice msg

* feat: supports for Voice and File in discord

* chore: remove debug msg

* perf: remove unnecessary bot logs

* feat: implement bot log filtering and per label color (#1839)

* feat: add sender_name and group_name in query variables
2025-12-06 21:11:01 +08:00
Junyan Qin
daf56e5dc2 fix: test failed 2025-12-05 22:54:13 +08:00
Yaguang.Wang
cb7c9af25c feat: Expanded WeCom message parsing to capture msgtype, inline voice/video… (#1843)
* Expanded WeCom message parsing to capture msgtype, inline voice/video/file/link data, bounded base64 downloads, and richer mixed-message attachments (src/langbot/libs/wecom_ai_bot_api/api.py); added event accessors for new fields (src/langbot/libs/wecom_ai_bot_api/wecombotevent.py).
Converter now maps richer WeCom payloads (text, images, files, voice, video, links) into platform message chain with fallbacks when nothing parsable is present (src/langbot/pkg/platform/sources/wecombot.py).
Preprocessor now turns voice inputs into file URLs for downstream runners (src/langbot/pkg/pipeline/preproc/preproc.py).
Dify runner uploads all incoming files (images/audio/video/docs) after downloading or decoding data URLs, infers MIME types, and passes typed file descriptors into chat/workflow calls (src/langbot/pkg/provider/runners/difysvapi.py).

* Update src/langbot/pkg/platform/sources/wecombot.py

Fixed the issue of duplicate text in the comments.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/langbot/libs/wecom_ai_bot_api/api.py

Modify the way you approach challenges.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/langbot/pkg/platform/sources/wecombot.py

Changing the variable names makes more sense.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feat: use from_base64 for the voice file converting

---------

Co-authored-by: tabriswang <tabriswang@finecomn.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-12-05 22:33:15 +08:00
Junyan Qin
45e61befac fix: test failed 2025-12-05 22:30:44 +08:00
Junyan Qin
ea50ba10e6 perf: add en name in the wecom manifest 2025-12-05 21:28:56 +08:00
Junyan Qin
5c4a727e74 feat: make all db migrations SQL-only 2025-12-05 21:00:04 +08:00
Junyan Qin
867f05c4ad perf: make the timeout of emit_event 180s 2025-12-05 20:59:37 +08:00
Junyan Qin
b06b32306f feat: remove all unnecessary fields in GroupMember and implement MessageEvent field for pipeline events 2025-12-05 17:24:58 +08:00
Junyan Qin
dbfcb70f8d fix: sender_id not presented to Session 2025-12-05 17:13:30 +08:00
Junyan Qin
e64d56c4ac fix: bad protocol of default plugin debug url 2025-12-05 16:06:56 +08:00
Bruce
8f0da7943c Remove plugins volume from docker-compose (#1842) 2025-12-05 11:28:04 +08:00
Junyan Qin
e62ff7e520 fix: deps issues 2025-12-04 23:07:55 +08:00
Junyan Qin (Chin)
86e951916e feat: add milvus and pgvector as vector db (#1840)
* feat: add milvus and pgvector as vector db

* chore: update config.yaml template delete comments
2025-12-04 22:34:49 +08:00
Junyan Qin
6bf08466de chore: bump version 4.6.2 2025-12-04 20:30:02 +08:00
Junyan Qin
5e36dd480d docs: add KOOK in README 2025-12-04 13:56:56 +08:00
Junyan Qin (Chin)
0e2cd8c018 Feat/kook (#1834)
* feat: add adapter file

* fix: style for bot log

* fix: kook bugs
2025-12-04 13:40:38 +08:00
Junyan Qin (Chin)
b4f92eba38 feat(platform): add skip_pipeline parameter for webhook responses (#1837)
* feat(platform): add skip_pipeline parameter for webhook responses

Add support for skip_pipeline parameter in webhook responses, allowing
webhook targets to instruct LangBot to skip pipeline processing for
specific messages. When a webhook responds with skip_pipeline=true,
the message is treated as a notification only and bypasses the query pool.

Changes:
- webhook_pusher.py: Parse JSON responses and return skip_pipeline flag
- botmgr.py: Check skip_pipeline before adding messages to query pool
- docker-compose.yaml: Add DNS configuration to fix container networking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: webhook crud bug

* chore: revert docker-compose.yaml

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 13:40:26 +08:00
dependabot[bot]
905e48c8ed chore(deps): bump next from 15.4.7 to 15.4.8 in /web (#1836)
Bumps [next](https://github.com/vercel/next.js) from 15.4.7 to 15.4.8.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.4.7...v15.4.8)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.4.8
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 11:33:55 +08:00
Junyan Qin
10ec79312e chore: bump version 4.6.1 2025-12-02 17:43:38 +08:00
Junyan Qin
24f779ff95 fix: websocket connect failed in prod env 2025-12-02 17:41:31 +08:00
Junyan Qin
08c0677de9 chore: bump version 4.6.0 2025-12-02 13:58:08 +08:00
Junyan Qin
cc5d32cf8a chore: bump langbot-plugin to 0.2.0 2025-12-01 22:15:38 +08:00
Junyan Qin
01a5133396 chore: update docker-compose.yaml 2025-12-01 22:14:38 +08:00
Guanchao Wang
0aa5188b29 Feat/unified webhook (#1793)
* fix: wecombot id

* feat: add unified webhook for wecom

* feat: add support for wecombot,wxoa,slack and qqo

* fix: slack adapter

* feat: qqo

* fix: errors when npm lint

* fix: qqo webhook

* feat: add wecomcs

* fix: modify wecomcs

* fix: import errors

* feat: add configurable webhook display prefix (#1797)

* Initial plan

* Add webhook_display_prefix configuration option

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* perf: change config field name

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>

* feat: finish the fxxking line adapter

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
2025-12-01 22:09:20 +08:00
Junyan Qin (Chin)
e49a161d0a feat: displaying plugin debug info (#1828) 2025-12-01 17:59:49 +08:00
Junyan Qin
0ddc3d60e7 fix: incorrect update date in kb card 2025-12-01 14:35:41 +08:00
Junyan Qin
51794176af perf: add comment for installing KB retriever plugins 2025-12-01 14:04:32 +08:00
Copilot
b634aa48dc feat(web): Add markdown rendering support to pipeline chat messages with toggle (#1826)
* Initial plan

* Add markdown rendering support to pipeline debug dialog messages with toggle button

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Fix code review feedback: remove conflicting styles and imports

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* perf: styles

* fix: websocket message broadcasting cross-contamination between person and group channels

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-12-01 13:44:01 +08:00
Junyan Qin
16ae8ac546 chore: remove NotFound component from the application 2025-11-30 21:58:28 +08:00
Copilot
1ecb0735cb perf: Filter plugins by component types in pipeline extensions (#1821)
* Initial plan

* Add component-kind filtering to list_plugins and filter pipeline extensions to only show plugins with Command, EventListener, or Tool components

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* fix: testing path

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-11-30 20:55:48 +08:00
Junyan Qin
c368d828c9 fix: linter error 2025-11-30 14:27:53 +08:00
Junyan Qin
019ae9c216 refactor: remove debug plugin success message from PluginForm and localization files 2025-11-30 14:20:40 +08:00
Copilot
580d9441a4 fix: increase execute_command timeout from 60s to 180s (#1813)
* Initial plan

* fix: increase execute_command timeout from 60s to 180s

Co-authored-by: the-lazy-me <52873503+the-lazy-me@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: the-lazy-me <52873503+the-lazy-me@users.noreply.github.com>
2025-11-28 23:07:55 +08:00
Junyan Qin (Chin)
b5d192425e perf: advanced web chat (#1811)
* perf: supports for quoting message

* feat: add supports for Voice and File

* perf: reply button
2025-11-28 22:25:06 +08:00
Copilot
58312deb8c fix: command return value image_url handling for DingTalk, Slack, LINE, and Lark adapters (#1810)
* Initial plan

* Fix command return value image_url handling for DingTalk, Slack, and LINE adapters

Co-authored-by: the-lazy-me <52873503+the-lazy-me@users.noreply.github.com>

* Refactor DingTalk image handling into helper method and add clarifying comment

Co-authored-by: the-lazy-me <52873503+the-lazy-me@users.noreply.github.com>

* Fix Lark adapter to not append empty paragraph before images

Co-authored-by: the-lazy-me <52873503+the-lazy-me@users.noreply.github.com>

* Improve Lark adapter image handling with better error logging

Co-authored-by: the-lazy-me <52873503+the-lazy-me@users.noreply.github.com>

* Fix Lark adapter to send images as separate image messages instead of embedded in post

Co-authored-by: the-lazy-me <52873503+the-lazy-me@users.noreply.github.com>

* Parse Markdown image syntax in Lark adapter and render as separate image messages

Co-authored-by: the-lazy-me <52873503+the-lazy-me@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: the-lazy-me <52873503+the-lazy-me@users.noreply.github.com>
2025-11-28 22:24:42 +08:00
Junyan Qin
cf646752c5 feat: add more service api supports 2025-11-28 20:13:58 +08:00
Junyan Qin
b53750fde4 feat: add KnowledgeRetriever supports in plugin market 2025-11-28 16:47:55 +08:00
Junyan Qin
52e6135ae8 chore: i18n for knowledge retriever component name 2025-11-28 15:48:27 +08:00
Junyan Qin
f4eb59e2ad fix: deleted external kb not destoryed 2025-11-28 15:37:55 +08:00
Junyan Qin
34d84590e2 chore: tidy files 2025-11-28 15:01:54 +08:00
Junyan Qin (Chin)
d09b823c49 refactor: switch webchat from sse to websocket (#1808)
* refactor: switch webchat from sse to websocket

* perf: image preview dialog

* chore: remove console.log
2025-11-28 14:54:01 +08:00
Junyan Qin
348620ac0a chore: remove unused code 2025-11-27 23:39:01 +08:00
Copilot
a8481e43f0 feat: external knowledge bases (#1783)
* Initial plan

* Add backend support for external knowledge bases

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Add frontend support for external knowledge bases with tabs UI

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Add i18n translations for all languages (Traditional Chinese and Japanese)

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Update knowledge base tab list styling to match plugins page

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* perf: margin-top for kb page

* refactor: switch RetrievalResultEntry to langbot_plugin pkg ones

* feat: knowledge retriever listing and creating

* stash

* refactor: unify sync mechanism for polymorphic components

* feat: use unified retireval result struct in retrieval test page

* chore: remove unused methods

* feat: retriever icon displaying

* feat: localagent retrieval with external kbs

* chore: bump version of langbot-plugin to 0.2.0b1

* fix: i18n

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-11-27 23:19:43 +08:00
Copilot
3c04eeaff9 perf: API integration dialog height and enable table scrolling (#1805)
* Initial plan

* Fix API integration dialog height and make tables scrollable

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Update API integration dialog height for improved layout

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-11-27 11:59:46 +08:00
Junyan Qin (Chin)
87131cf03b Feat/pipeline enable all extensions (#1807)
* feat: 添加流水线扩展集成"启用所有"选项

为流水线的扩展集成配置添加独立的"启用所有插件"和"启用所有MCP服务器"选项。

主要变更:
- 数据模型:在 extensions_preferences 中添加 enable_all_plugins 和 enable_all_mcp_servers 字段
- 后端逻辑:修改 RuntimePipeline 以支持独立的启用所有选项,当启用时设置为 None 表示使用所有可用资源
- API 接口:更新 GET/PUT /api/v1/pipelines/{uuid}/extensions 以支持新字段
- 前端 UI:为插件和 MCP 服务器分别添加独立的开关控件
- 国际化:添加对应的中文翻译文本
- 默认行为:新创建的流水线默认启用所有插件和 MCP 服务器

🤖 Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* fix(i18n): add missing translations for pipeline extensions

Added translations for enable all plugins/MCP servers feature:
- en-US: English translations
- ja-JP: Japanese translations
- zh-Hant: Traditional Chinese translations

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* chore: add migration for enable all extensions config

* fix: bad renaming

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
2025-11-27 11:52:15 +08:00
Junyan Qin
7d51293594 chore: adjust star component style 2025-11-25 10:08:11 +08:00
Junyan Qin
b78b0e50bb perf: plugin list padding bottom 2025-11-25 09:50:51 +08:00
Copilot
6b4c1a7dee fix: plugin card source badge blocked by hover overlay (#1802)
* Initial plan

* Add View Source menu item and remove clickable source badges

- Add "viewSource" translation key to all language files
- Add View Source menu item to plugin card dropdown (only for GitHub/marketplace plugins)
- Remove onClick handlers and ExternalLink icons from source badges
- Keep the badges themselves for visual indication of plugin source

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Fix prettier formatting issue in PluginCardComponent

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
2025-11-25 09:46:37 +08:00
Xiaoyu Su
2e1f16d7b4 feat: improvements for installed plugin card
* feat:Add README display to installed plugins

* chore: Increase the timeout of call_tool

* perf: smaller animation

* fix: add endpiont for readme

* feat: supports for multilingual READMEs

* feat: supports for getting readme img

* chore: bump langbot-plugin to 0.1.13b1

* perf: plugin card layout

* fix: import useTranslation linter error

---------

Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-11-25 00:12:03 +08:00
Tigrex Dai
50c33c5213 Fix typo for variable and comment 'Quote' (#1800) 2025-11-24 23:09:31 +08:00
Copilot
ace6d62d76 perf: Sort installed plugins: debug plugins first, then by installation time (#1798)
* Initial plan

* Implement plugin list sorting: debug plugins first, then by installation time

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Apply ruff formatting

* Add unit tests for plugin list sorting functionality

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Optimize database query to avoid N+1 problem and update tests

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Remove redundant assertion in test

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* perf: plugin list sorting

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-11-23 13:46:45 +08:00
Junyan Qin
b7c4c21796 feat: add message_chain field to *NormalMessageReceived events 2025-11-22 14:59:12 +08:00
Copilot
66602da9cb feat: add model_config parameter support for Dify assistant type apps (#1796)
* Initial plan

* feat: add model_config parameter support for Dify assistant type

- Add model_config parameter to AsyncDifyServiceClient.chat_messages method
- Add _get_model_config helper method to DifyServiceAPIRunner
- Pass model_config from pipeline configuration to all chat_messages calls
- Add model-config configuration field to dify-service-api schema in ai.yaml
- Support optional model configuration for assistant type apps in open-source Dify

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* refactor: improve model_config implementation based on code review

- Simplify _get_model_config method logic
- Add more descriptive comment about model_config usage
- Clarify when model_config is used (assistant type apps)

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* feat: only modify client.py

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-11-22 14:38:26 +08:00
wrj97
31b483509c fix: fix n8n streaming support issue (#1787)
* fix: fix n8n streaming support issue

Add streaming support detection and proper message type handling for
n8n service API runner. Previously, when streaming was enabled, n8n
integration would fail due to incorrect message type usage.

1. Added streaming capability detection by checking adapter's
is_stream_output_supported method
2. Implemented conditional message generation using MessageChunk for
streaming mode and Message for non-streaming mode
3. Added proper error handling for adapters that don't support streaming
detection

* fix: add n8n webhook streaming model ,Optimized the streaming output when calling n8n.

---------

Co-authored-by: Dong_master <2213070223@qq.com>
2025-11-22 14:17:46 +08:00
Junyan Qin
ba7cf69c9d doc: update READMEs 2025-11-22 00:20:39 +08:00
Junyan Qin
37296be67e feat: refactor plugin market interaction and migrate to LangBot Space
- Replace plugin detail dialog with hover buttons interaction
- Add "Install" and "View Details" hover buttons on plugin cards
- Remove PluginDetailDialog component
- Update plugin marketplace URL format to /market/{author}/{plugin}
- Redirect all plugin detail views to LangBot Space
- Add i18n support for 4 languages (zh-Hans, en-US, zh-Hant, ja-JP)
- Optimize hover overlay styles for light/dark theme

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 14:23:22 +08:00
Junyan Qin
6c03a1dd31 perf: add supports for showing multilingual plugin README 2025-11-21 12:14:04 +08:00
Junyan Qin
b75ec9e989 doc: add product hunt badges 2025-11-21 10:31:57 +08:00
Copilot
5c8523e4ef docs: Add multilingual README files (Spanish, French, Korean, Russian, Vietnamese) (#1794)
* Initial plan

* Add multilingual README files (ES, FR, KO, RU, VI)

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
2025-11-21 00:36:35 +08:00
Junyan Qin
9802a42a9e perf: add request plugin button to marketplace 2025-11-20 23:41:45 +08:00
Junyan Qin
99e3abec72 chore: bump version 4.5.4 2025-11-20 23:19:37 +08:00
Junyan Qin
fc2efdf994 chore: bump langbot-plugin 0.1.12 2025-11-20 21:51:44 +08:00
Junyan Qin
6ed672d996 perf: tips msg for tool call 2025-11-20 21:45:22 +08:00
Junyan Qin
2bf593fa6b feat: pass session and query_id to tool call 2025-11-20 21:17:47 +08:00
Junyan Qin
3182214663 fix: linter errors 2025-11-20 19:48:34 +08:00
Junyan Qin
20614b20b7 feat: add component filter to marketplace page 2025-11-20 19:46:33 +08:00
Junyan Qin
da323817f7 feat: add plugin components displaying in marketplace page 2025-11-20 18:50:00 +08:00
Junyan Qin
763c1a885c perf: url display in webhook dialog 2025-11-20 16:48:06 +08:00
Junyan Qin
dbc09f46f4 perf: provider icon rounded in hovercard 2025-11-20 10:25:29 +08:00
Junyan Qin
cf43f09aff perf: auto refresh logic in market 2025-11-20 10:18:28 +08:00
Copilot
c3c51b0fbf perf: Add "Select All" checkbox to Plugin and MCP Server selection dialogs (#1790)
* Initial plan

* Add "Select All" checkbox to Plugin and MCP Server selection dialogs

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Make "Select All" text clickable by adding onClick handler to container

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
2025-11-18 17:00:05 +08:00
Duke
8a42daa63f Fix wecom image message send fail issue (#1789)
* Fix wecom image upload issue

* Fix log
2025-11-18 16:02:13 +08:00
350 changed files with 52387 additions and 9893 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
.github
.venv
.vscode
.data
.temp
web/.next
web/node_modules
web/.env

View File

@@ -19,7 +19,7 @@ body:
- type: textarea
attributes:
label: 复现步骤
description: 提供越多信息,我们会越快解决问题,建议多提供配置截图;**如果你不认真填写(只一两句话概括),我们会很生气并且立即关闭 issue 或两年后才回复你**
description: 提供越多信息,我们会越快解决问题,建议多提供配置截图;**如果涉及 Dify、n8n、Langflow 等外部平台,请提供应用的导出文件(如 Dify 应用的 DSL我们将更快回复您。**
validations:
required: false
- type: textarea

View File

@@ -19,7 +19,7 @@ body:
- type: textarea
attributes:
label: Reproduction steps
description: How to reproduce this problem, the more detailed the better; the more information you provide, the faster we will solve the problem. 【注意】请务必认真填写此部分,若不提供完整信息(如只有一两句话的概括),我们将不会回复!
description: How to reproduce this problem, the more detailed the better; the more information you provide, the faster we will solve the problem.
validations:
required: false
- type: textarea

View File

@@ -3,7 +3,6 @@ on:
## 发布release的时候会自动构建
release:
types: [published]
workflow_dispatch:
jobs:
publish-docker-image:
runs-on: ubuntu-latest
@@ -42,7 +41,7 @@ jobs:
run: docker buildx create --name mybuilder --use
- name: Build for Release # only relase, exlude pre-release
if: ${{ github.event.release.prerelease == false }}
run: docker buildx build --platform linux/amd64 -t rockchin/langbot:${{ steps.check_version.outputs.version }} -t rockchin/langbot:latest . --push
run: docker buildx build --platform linux/arm64,linux/amd64 -t rockchin/langbot:${{ steps.check_version.outputs.version }} -t rockchin/langbot:latest . --push
- name: Build for Pre-release # no update for latest tag
if: ${{ github.event.release.prerelease == true }}
run: docker buildx build --platform linux/amd64 -t rockchin/langbot:${{ steps.check_version.outputs.version }} . --push
run: docker buildx build --platform linux/arm64,linux/amd64 -t rockchin/langbot:${{ steps.check_version.outputs.version }} . --push

60
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Lint
on:
push:
branches:
- main
- master
- dev
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
jobs:
ruff:
name: Ruff Lint & Format
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Install dependencies
run: uv sync --dev
- name: Run ruff check
run: uv run ruff check src
- name: Run ruff format
run: uv run ruff format src --check
frontend:
name: Frontend Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '25'
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Install dependencies
working-directory: web
run: pnpm install
- name: Run lint
working-directory: web
run: pnpm lint

View File

@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
python-version: ['3.11', '3.12', '3.13']
fail-fast: false
steps:

1
.gitignore vendored
View File

@@ -42,7 +42,6 @@ botpy.log*
test.py
/web_ui
.venv/
uv.lock
/test
plugins.bak
coverage.xml

37
.mcp.json Normal file
View File

@@ -0,0 +1,37 @@
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": [
"shadcn@latest",
"mcp"
]
},
"sequential-thinking": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"],
"env": {}
},
"github": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
}
},
"fetch": {
"type": "stdio",
"command": "uvx",
"args": ["mcp-server-fetch"],
"env": {}
},
"playwright": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@playwright/mcp@latest"],
"env": {}
}
}
}

View File

@@ -8,16 +8,17 @@ LangBot is a open-source LLM native instant messaging bot development platform,
LangBot has a comprehensive frontend, all operations can be performed through the frontend. The project splited into these major parts:
- `./pkg`: The core python package of the project backend.
- `./pkg/platform`: The platform module of the project, containing the logic of message platform adapters, bot managers, message session managers, etc.
- `./pkg/provider`: The provider module of the project, containing the logic of LLM providers, tool providers, etc.
- `./pkg/pipeline`: The pipeline module of the project, containing the logic of pipelines, stages, query pool, etc.
- `./pkg/api`: The api module of the project, containing the http api controllers and services.
- `./pkg/plugin`: LangBot bridge for connecting with plugin system.
- `./libs`: Some SDKs we previously developed for the project, such as `qq_official_api`, `wecom_api`, etc.
- `./templates`: Templates of config files, components, etc.
- `./web`: Frontend codebase, built with Next.js + **shadcn** + **Tailwind CSS**.
- `./docker`: docker-compose deployment files.
- `./src/langbot`: The main python package of the project, below are the main modules in this package:
- `./pkg`: The core python package of the project backend.
- `./pkg/platform`: The platform module of the project, containing the logic of message platform adapters, bot managers, message session managers, etc.
- `./pkg/provider`: The provider module of the project, containing the logic of LLM providers, tool providers, etc.
- `./pkg/pipeline`: The pipeline module of the project, containing the logic of pipelines, stages, query pool, etc.
- `./pkg/api`: The api module of the project, containing the http api controllers and services.
- `./pkg/plugin`: LangBot bridge for connecting with plugin system.
- `./libs`: Some SDKs we previously developed for the project, such as `qq_official_api`, `wecom_api`, etc.
- `./templates`: Templates of config files, components, etc.
- `./web`: Frontend codebase, built with Next.js + **shadcn** + **Tailwind CSS**.
- `./docker`: docker-compose deployment files.
## Backend Development
@@ -69,6 +70,7 @@ Plugin Runtime automatically starts each installed plugin and interacts through
- type: must be a specific type, such as feat (new feature), fix (bug fix), docs (documentation), style (code style), refactor (refactoring), perf (performance optimization), etc.
- scope: the scope of the commit, such as the package name, the file name, the function name, the class name, the module name, etc.
- subject: the subject of the commit, such as the description of the commit, the reason for the commit, the impact of the commit, etc.
- If you changed the definition of database entities, please update the migration file in `src/langbot/pkg/persistence/migrations/` and update the constants.py file in `src/langbot/pkg/utils/constants.py` with the new migration number.
## Some Principles

View File

@@ -20,4 +20,4 @@ RUN apt update \
&& uv sync \
&& touch /.dockerenv
CMD [ "uv", "run", "main.py" ]
CMD [ "uv", "run", "--no-sync", "main.py" ]

230
README.md
View File

@@ -1,47 +1,69 @@
<p align="center">
<a href="https://langbot.app">
<img src="https://docs.langbot.app/social_zh.png" alt="LangBot"/>
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
</a>
<div align="center">
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production&#0045;grade&#0032;IM&#0032;bot&#0032;made&#0032;easy&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
[English](README_EN.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / (PR for your language)
<h3>Production-grade platform for building agentic IM bots.</h3>
<h4>Quickly build, debug, and ship AI bots to Slack, Discord, Telegram, WeChat, and more.</h4>
English / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-966235608-blue)](https://qm.qq.com/q/JLi38whHum)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langbot-app/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/langbot-app/LangBot)](https://github.com/langbot-app/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot)
<a href="https://langbot.app">项目主页</a>
<a href="https://docs.langbot.app/zh/insight/guide.html">部署文档</a>
<a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">插件介绍</a>
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a>
[![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://docs.langbot.app/en/insight/features">Features</a>
<a href="https://docs.langbot.app/en/insight/guide">Docs</a>
<a href="https://docs.langbot.app/en/tags/readme">API</a>
<a href="https://space.langbot.app/cloud">Cloud</a>
<a href="https://space.langbot.app">Plugin Market</a>
<a href="https://langbot.featurebase.app/roadmap">Roadmap</a>
</div>
</p>
LangBot 是一个开源的大语言模型原生即时通信机器人开发平台,旨在提供开箱即用的 IM 机器人开发体验,具有 Agent、RAG、MCP 等多种 LLM 应用功能,适配全球主流即时通信平台,并提供丰富的 API 接口,支持自定义开发。
---
## 📦 开始使用
## What is LangBot?
#### 快速部署
LangBot is an **open-source, production-grade platform** for building AI-powered instant messaging bots. It connects Large Language Models (LLMs) to any chat platform, enabling you to create intelligent agents that can converse, execute tasks, and integrate with your existing workflows.
使用 `uvx` 一键启动(需要先安装 [uv](https://docs.astral.sh/uv/getting-started/installation/)
### Key Capabilities
- **AI Conversations & Agents** — Multi-turn dialogues, tool calling, multi-modal support, streaming output. Built-in RAG (knowledge base) with deep integration to [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
- **Universal IM Platform Support** — One codebase for Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
- **Production-Ready** — Access control, rate limiting, sensitive word filtering, comprehensive monitoring, and exception handling. Trusted by enterprises.
- **Plugin Ecosystem** — Hundreds of plugins, event-driven architecture, component extensions, and [MCP protocol](https://modelcontextprotocol.io/) support.
- **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.
[→ Learn more about all features](https://docs.langbot.app/en/insight/features)
---
## Quick Start
### ☁️ LangBot Cloud (Recommended)
**[LangBot Cloud](https://space.langbot.app/cloud)** — Zero deployment, ready to use.
### One-Line Launch
```bash
uvx langbot
```
访问 http://localhost:5300 即可开始使用。
> Requires [uv](https://docs.astral.sh/uv/getting-started/installation/). Visit http://localhost:5300 — done.
#### Docker Compose 部署
### Docker Compose
```bash
git clone https://github.com/langbot-app/LangBot
@@ -49,122 +71,102 @@ cd LangBot/docker
docker compose up -d
```
访问 http://localhost:5300 即可开始使用。
详细文档[Docker 部署](https://docs.langbot.app/zh/deploy/langbot/docker.html)。
#### 宝塔面板部署
已上架宝塔面板,若您已安装宝塔面板,可以根据[文档](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html)使用。
#### Zeabur 云部署
社区贡献的 Zeabur 模板。
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH)
#### Railway 云部署
### One-Click Cloud Deploy
[![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)
#### 手动部署
**More options:** [Docker](https://docs.langbot.app/en/deploy/langbot/docker) · [Manual](https://docs.langbot.app/en/deploy/langbot/manual) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt) · [Kubernetes](./docker/README_K8S.md)
直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
---
#### Kubernetes 部署
## Supported Platforms
参考 [Kubernetes 部署](./docker/README_K8S.md) 文档。
## 😎 保持更新
点击仓库右上角 Star 和 Watch 按钮,获取最新动态。
![star gif](https://docs.langbot.app/star.gif)
## ✨ 特性
- 💬 大模型对话、Agent支持多种大模型适配群聊和私聊具有多轮对话、工具调用、多模态、流式输出能力自带 RAG知识库实现并深度适配 [Dify](https://dify.ai)。
- 🤖 多平台支持:目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
- 😻 Web 管理面板:支持通过浏览器管理 LangBot 实例,不再需要手动编写配置文件。
详细规格特性请访问[文档](https://docs.langbot.app/zh/insight/features.html)。
或访问 demo 环境https://demo.langbot.dev/
- 登录信息:邮箱:`demo@langbot.app` 密码:`langbot123456`
- 注意:仅展示 WebUI 效果,公开环境,请不要在其中填入您的任何敏感信息。
### 消息平台
| 平台 | 状态 | 备注 |
| --- | --- | --- |
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
| 企业微信 | ✅ | |
| 企微对外客服 | ✅ | |
| 企微智能机器人 | ✅ | |
| 个人微信 | ✅ | |
| 微信公众号 | ✅ | |
| 飞书 | ✅ | |
| 钉钉 | ✅ | |
| Platform | Status | Notes |
|----------|--------|-------|
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | ✅ | |
| QQ | ✅ | Personal & Official API |
| WeCom | ✅ | Enterprise WeChat, External CS, AI Bot |
| WeChat | ✅ | Personal & Official Account |
| Lark | ✅ | |
| DingTalk | ✅ | |
| KOOK | ✅ | |
| Satori | ✅ | |
### 大模型能力
---
| 模型 | 状态 | 备注 |
| --- | --- | --- |
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
| [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | |
| [智谱AI](https://open.bigmodel.cn/) | ✅ | |
| [胜算云](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | 全球大模型都可调用(友情推荐) |
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 资源平台 |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 |
| [接口 AI](https://jiekou.ai/) | ✅ | 大模型聚合平台,专注全球大模型接入 |
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型接口聚合平台 |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
| [小马算力](https://www.tokenpony.cn/453z1) | ✅ | 大模型聚合平台 |
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支持通过 MCP 协议获取工具 |
| [百宝箱Tbox](https://www.tbox.cn/open) | ✅ | 蚂蚁百宝箱智能体平台每月免费10亿大模型Token |
## Supported LLMs & Integrations
### TTS
| Provider | Type | Status |
|----------|------|--------|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
| [xAI](https://x.ai/) | LLM | ✅ |
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
| [Ollama](https://ollama.com/) | Local LLM | ✅ |
| [LM Studio](https://lmstudio.ai/) | Local LLM | ✅ |
| [Dify](https://dify.ai) | LLMOps | ✅ |
| [MCP](https://modelcontextprotocol.io/) | Protocol | ✅ |
| [SiliconFlow](https://siliconflow.cn/) | Gateway | ✅ |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Gateway | ✅ |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Gateway | ✅ |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Gateway | ✅ |
| [GiteeAI](https://ai.gitee.com/) | Gateway | ✅ |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU Platform | ✅ |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU Platform | ✅ |
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU Platform | ✅ |
| [接口 AI](https://jiekou.ai/) | Gateway | ✅ |
| [302.AI](https://share.302.ai/SuTG99) | Gateway | ✅ |
| 平台/模型 | 备注 |
| --- | --- |
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
[→ View all integrations](https://docs.langbot.app/en/insight/features)
### 文生图
---
| 平台/模型 | 备注 |
| --- | --- |
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
## Why LangBot?
## 😘 社区贡献
| Use Case | How LangBot Helps |
|----------|-------------------|
| **Customer Support** | Deploy AI agents to Slack/Discord/Telegram that answer questions using your knowledge base |
| **Internal Tools** | Connect n8n/Dify workflows to WeCom/DingTalk for automated business processes |
| **Community Management** | Moderate QQ/Discord groups with AI-powered content filtering and interaction |
| **Multi-Platform Presence** | One bot, all platforms. Manage from a single dashboard |
感谢以下[代码贡献者](https://github.com/langbot-app/LangBot/graphs/contributors)和社区里其他成员对 LangBot 的贡献:
---
## Live Demo
**Try it now:** https://demo.langbot.dev/
- Email: `demo@langbot.app`
- Password: `langbot123456`
*Note: Public demo environment. Do not enter sensitive information.*
---
## Community
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&label=Discord)](https://discord.gg/wdNEHETs87)
- [Discord Community](https://discord.gg/wdNEHETs87)
---
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=langbot-app/LangBot&type=Date)](https://star-history.com/#langbot-app/LangBot&Date)
---
## Contributors
Thanks to all [contributors](https://github.com/langbot-app/LangBot/graphs/contributors) who have helped make LangBot better:
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a>
<!--
## For Code Agents
If you're a code agent based on LLM, you should read [AGENTS.md](AGENTS.md) to understand the project structure and development process before you start any development.
-->

197
README_CN.md Normal file
View File

@@ -0,0 +1,197 @@
<p align="center">
<a href="https://langbot.app">
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
</a>
<div align="center">
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<h3>生产级 AI 即时通信机器人开发平台。</h3>
<h4>快速构建、调试和部署 AI 机器人到微信、QQ、飞书、Slack、Discord、Telegram 等平台。</h4>
[English](README.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-1030838208-blue)](https://qm.qq.com/q/DxZZcNxM1W)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langbot-app/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/langbot-app/LangBot)](https://github.com/langbot-app/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers)
[![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot)
<a href="https://langbot.app">官网</a>
<a href="https://docs.langbot.app/zh/insight/features.html">特性</a>
<a href="https://docs.langbot.app/zh/insight/guide.html">文档</a>
<a href="https://docs.langbot.app/zh/tags/readme.html">API</a>
<a href="https://space.langbot.app/cloud">Cloud</a>
<a href="https://space.langbot.app">插件市场</a>
<a href="https://langbot.featurebase.app/roadmap">路线图</a>
</div>
</p>
---
## 什么是 LangBot
LangBot 是一个**开源的生产级平台**,用于构建 AI 驱动的即时通信机器人。它将大语言模型LLM连接到各种聊天平台帮助你创建能够对话、执行任务、并集成到现有工作流程中的智能 Agent。
### 核心能力
- **AI 对话与 Agent** — 多轮对话、工具调用、多模态、流式输出。自带 RAG知识库深度集成 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org) 等 LLMOps 平台。
- **全平台支持** — 一套代码,覆盖 QQ、微信、企业微信、飞书、钉钉、Discord、Telegram、Slack、LINE、KOOK 等平台。
- **生产就绪** — 访问控制、限速、敏感词过滤、全面监控与异常处理,已被多家企业采用。
- **插件生态** — 数百个插件,事件驱动架构,组件扩展,适配 [MCP 协议](https://modelcontextprotocol.io/)。
- **Web 管理面板** — 通过浏览器直观地配置、管理和监控机器人,无需手动编辑配置文件。
- **多流水线架构** — 不同机器人用于不同场景,具备全面的监控和异常处理能力。
[→ 了解更多功能特性](https://docs.langbot.app/zh/insight/features.html)
---
## 快速开始
### ☁️ LangBot Cloud推荐
**[LangBot Cloud](https://space.langbot.app/cloud)** — 免部署,开箱即用。
### 一键启动
```bash
uvx langbot
```
> 需要安装 [uv](https://docs.astral.sh/uv/getting-started/installation/)。访问 http://localhost:5300 即可使用。
### Docker Compose
```bash
git clone https://github.com/langbot-app/LangBot
cd LangBot/docker
docker compose up -d
```
### 一键云部署
[![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)
**更多方式:** [Docker](https://docs.langbot.app/zh/deploy/langbot/docker.html) · [手动部署](https://docs.langbot.app/zh/deploy/langbot/manual.html) · [宝塔面板](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
---
## 支持的平台
| 平台 | 状态 | 备注 |
|------|------|------|
| QQ | ✅ | 个人号、官方机器人(频道、私聊、群聊) |
| 微信 | ✅ | 个人微信、微信公众号 |
| 企业微信 | ✅ | 应用消息、对外客服、智能机器人 |
| 飞书 | ✅ | |
| 钉钉 | ✅ | |
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | ✅ | |
| KOOK | ✅ | |
---
## 支持的大模型与集成
| 提供商 | 类型 | 状态 |
|--------|------|------|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
| [xAI](https://x.ai/) | LLM | ✅ |
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
| [智谱AI](https://open.bigmodel.cn/) | LLM | ✅ |
| [Ollama](https://ollama.com/) | 本地 LLM | ✅ |
| [LM Studio](https://lmstudio.ai/) | 本地 LLM | ✅ |
| [Dify](https://dify.ai) | LLMOps | ✅ |
| [MCP](https://modelcontextprotocol.io/) | 协议 | ✅ |
| [SiliconFlow](https://siliconflow.cn/) | 聚合平台 | ✅ |
| [阿里云百炼](https://bailian.console.aliyun.com/) | 聚合平台 | ✅ |
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | 聚合平台 | ✅ |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | 聚合平台 | ✅ |
| [GiteeAI](https://ai.gitee.com/) | 聚合平台 | ✅ |
| [胜算云](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU 平台 | ✅ |
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU 平台 | ✅ |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU 平台 | ✅ |
| [接口 AI](https://jiekou.ai/) | 聚合平台 | ✅ |
| [302.AI](https://share.302.ai/SuTG99) | 聚合平台 | ✅ |
| [小马算力](https://www.tokenpony.cn/453z1) | 聚合平台 | ✅ |
| [百宝箱Tbox](https://www.tbox.cn/open) | 智能体平台 | ✅ |
[→ 查看完整集成列表](https://docs.langbot.app/zh/insight/features.html)
### TTS语音合成
| 平台/模型 | 备注 |
|-----------|------|
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
### 文生图
| 平台/模型 | 备注 |
|-----------|------|
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin) |
---
## 为什么选择 LangBot
| 使用场景 | LangBot 如何帮助 |
|----------|------------------|
| **客户服务** | 将 AI Agent 部署到微信/企微/钉钉/飞书,基于知识库自动回答用户问题 |
| **内部工具** | 将 n8n/Dify 工作流接入企微/钉钉,实现业务流程自动化 |
| **社群运营** | 在 QQ/Discord 群中使用 AI 驱动的内容审核与智能互动 |
| **多平台触达** | 一个机器人,覆盖所有平台。通过统一面板集中管理 |
---
## 在线演示
**立即体验:** https://demo.langbot.dev/
- 邮箱:`demo@langbot.app`
- 密码:`langbot123456`
*注意:公开演示环境,请不要在其中填入任何敏感信息。*
---
## 社区
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&label=Discord)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-1030838208-blue)](https://qm.qq.com/q/DxZZcNxM1W)
- [Discord 社区](https://discord.gg/wdNEHETs87)
- [QQ 社区群](https://qm.qq.com/q/DxZZcNxM1W)
---
## Star 趋势
[![Star History Chart](https://api.star-history.com/svg?repos=langbot-app/LangBot&type=Date)](https://star-history.com/#langbot-app/LangBot&Date)
---
## 贡献者
感谢所有[贡献者](https://github.com/langbot-app/LangBot/graphs/contributors)对 LangBot 的帮助:
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a>
<!--
## For Code Agents
If you're a code agent based on LLM, you should read [AGENTS.md](AGENTS.md) to understand the project structure and development process before you start any development.
-->

View File

@@ -1,141 +0,0 @@
<p align="center">
<a href="https://langbot.app">
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
</a>
<div align="center">
English / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / (PR for your language)
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langbot-app/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/langbot-app/LangBot)](https://github.com/langbot-app/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
<a href="https://langbot.app">Home</a>
<a href="https://docs.langbot.app/en/insight/guide.html">Deployment</a>
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a>
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">Submit Plugin</a>
</div>
</p>
LangBot is an open-source LLM native instant messaging robot development platform, aiming to provide out-of-the-box IM robot development experience, with Agent, RAG, MCP and other LLM application functions, adapting to global instant messaging platforms, and providing rich API interfaces, supporting custom development.
## 📦 Getting Started
#### Quick Start
Use `uvx` to start with one command (need to install [uv](https://docs.astral.sh/uv/getting-started/installation/)):
```bash
uvx langbot
```
Visit http://localhost:5300 to start using it.
#### Docker Compose Deployment
```bash
git clone https://github.com/langbot-app/LangBot
cd LangBot/docker
docker compose up -d
```
Visit http://localhost:5300 to start using it.
Detailed documentation [Docker Deployment](https://docs.langbot.app/en/deploy/langbot/docker.html).
#### One-click Deployment on BTPanel
LangBot has been listed on the BTPanel, if you have installed the BTPanel, you can use the [document](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) to use it.
#### Zeabur Cloud Deployment
Community contributed Zeabur template.
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH)
#### Railway Cloud Deployment
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
#### Other Deployment Methods
Directly use the released version to run, see the [Manual Deployment](https://docs.langbot.app/en/deploy/langbot/manual.html) documentation.
#### Kubernetes Deployment
Refer to the [Kubernetes Deployment](./docker/README_K8S.md) documentation.
## 😎 Stay Ahead
Click the Star and Watch button in the upper right corner of the repository to get the latest updates.
![star gif](https://docs.langbot.app/star.gif)
## ✨ Features
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, multi-modal, and streaming output capabilities. Built-in RAG (knowledge base) implementation, and deeply integrates with [Dify](https://dify.ai).
- 🤖 Multi-platform Support: Currently supports QQ, QQ Channel, WeCom, personal WeChat, Lark, DingTalk, Discord, Telegram, etc.
- 🛠️ High Stability, Feature-rich: Native access control, rate limiting, sensitive word filtering, etc. mechanisms; Easy to use, supports multiple deployment methods. Supports multiple pipeline configurations, different bots can be used for different scenarios.
- 🧩 Plugin Extension, Active Community: Support event-driven, component extension, etc. plugin mechanisms; Integrate Anthropic [MCP protocol](https://modelcontextprotocol.io/); Currently has hundreds of plugins.
- 😻 Web UI: Support management LangBot instance through the browser. No need to manually write configuration files.
For more detailed specifications, please refer to the [documentation](https://docs.langbot.app/en/insight/features.html).
Or visit the demo environment: https://demo.langbot.dev/
- Login information: Email: `demo@langbot.app` Password: `langbot123456`
- Note: For WebUI demo only, please do not fill in any sensitive information in the public environment.
### Message Platform
| Platform | Status | Remarks |
| --- | --- | --- |
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | ✅ | |
| Personal QQ | ✅ | |
| QQ Official API | ✅ | |
| WeCom | ✅ | |
| WeComCS | ✅ | |
| WeCom AI Bot | ✅ | |
| Personal WeChat | ✅ | |
| Lark | ✅ | |
| DingTalk | ✅ | |
### LLMs
| LLM | Status | Remarks |
| --- | --- | --- |
| [OpenAI](https://platform.openai.com/) | ✅ | Available for any OpenAI interface format model |
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
| [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | |
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | LLM and GPU resource platform |
| [Dify](https://dify.ai) | ✅ | LLMOps platform |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform |
| [接口 AI](https://jiekou.ai/) | ✅ | LLM aggregation platform, dedicated to global LLMs |
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLM and GPU resource platform |
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLM gateway(MaaS) |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
| [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform |
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM gateway(MaaS) |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM gateway(MaaS), LLMOps platform |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLM gateway(MaaS), LLMOps platform |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLM gateway(MaaS) |
| [MCP](https://modelcontextprotocol.io/) | ✅ | Support tool access through MCP protocol |
## 🤝 Community Contribution
Thank you for the following [code contributors](https://github.com/langbot-app/LangBot/graphs/contributors) and other members in the community for their contributions to LangBot:
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a>

171
README_ES.md Normal file
View File

@@ -0,0 +1,171 @@
<p align="center">
<a href="https://langbot.app">
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
</a>
<div align="center">
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production&#0045;grade&#0032;IM&#0032;bot&#0032;made&#0032;easy&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<h3>Plataforma de grado de producción para construir bots de mensajería instantánea con agentes de IA.</h3>
<h4>Construya, depure y despliegue bots de IA rápidamente en Slack, Discord, Telegram, WeChat y más.</h4>
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / Español / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langbot-app/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/langbot-app/LangBot)](https://github.com/langbot-app/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![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://docs.langbot.app/en/insight/features.html">Características</a>
<a href="https://docs.langbot.app/en/insight/guide.html">Documentación</a>
<a href="https://docs.langbot.app/en/tags/readme.html">API</a>
<a href="https://space.langbot.app">Mercado de Plugins</a>
<a href="https://langbot.featurebase.app/roadmap">Hoja de Ruta</a>
</div>
</p>
---
## ¿Qué es LangBot?
LangBot es una **plataforma de código abierto y grado de producción** para construir bots de mensajería instantánea impulsados por IA. Conecta modelos de lenguaje de gran escala (LLMs) con cualquier plataforma de chat, permitiéndole crear agentes inteligentes que pueden conversar, ejecutar tareas e integrarse con sus flujos de trabajo existentes.
### Capacidades Clave
- **Conversaciones e Agentes IA** — Diálogos de múltiples turnos, llamadas a herramientas, soporte multimodal, salida en streaming. RAG (base de conocimientos) incorporado con integración profunda con [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
- **Soporte Universal de Plataformas de MI** — Un solo código base para Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
- **Listo para Producción** — Control de acceso, limitación de velocidad, filtrado de palabras sensibles, monitoreo completo y manejo de excepciones. De confianza para empresas.
- **Ecosistema de Plugins** — Cientos de plugins, arquitectura basada en eventos, extensiones de componentes y soporte del [protocolo MCP](https://modelcontextprotocol.io/).
- **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.
[→ Conocer más sobre todas las funcionalidades](https://docs.langbot.app/en/insight/features.html)
---
## Inicio Rápido
### ☁️ LangBot Cloud (Recomendado)
**[LangBot Cloud](https://space.langbot.app/cloud)** — Sin despliegue, listo para usar.
### Lanzamiento en una línea
```bash
uvx langbot
```
> Requiere [uv](https://docs.astral.sh/uv/getting-started/installation/). Visite http://localhost:5300 — listo.
### Docker Compose
```bash
git clone https://github.com/langbot-app/LangBot
cd LangBot/docker
docker compose up -d
```
### Despliegue en la Nube con un Clic
[![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)
**Más opciones:** [Docker](https://docs.langbot.app/en/deploy/langbot/docker.html) · [Manual](https://docs.langbot.app/en/deploy/langbot/manual.html) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
---
## Plataformas Soportadas
| Plataforma | Estado | Notas |
|----------|--------|-------|
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | ✅ | |
| QQ | ✅ | Personal y API Oficial |
| WeCom | ✅ | WeChat Empresarial, CS Externo, AI Bot |
| WeChat | ✅ | Personal y Cuenta Oficial |
| Lark | ✅ | |
| DingTalk | ✅ | |
| KOOK | ✅ | |
| Satori | ✅ | |
---
## LLMs e Integraciones Soportadas
| Proveedor | Tipo | Estado |
|----------|------|--------|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
| [xAI](https://x.ai/) | LLM | ✅ |
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
| [Ollama](https://ollama.com/) | LLM Local | ✅ |
| [LM Studio](https://lmstudio.ai/) | LLM Local | ✅ |
| [Dify](https://dify.ai) | LLMOps | ✅ |
| [MCP](https://modelcontextprotocol.io/) | Protocolo | ✅ |
| [SiliconFlow](https://siliconflow.cn/) | Pasarela | ✅ |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Pasarela | ✅ |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Pasarela | ✅ |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Pasarela | ✅ |
| [GiteeAI](https://ai.gitee.com/) | Pasarela | ✅ |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Plataforma GPU | ✅ |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Plataforma GPU | ✅ |
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Plataforma GPU | ✅ |
| [接口 AI](https://jiekou.ai/) | Pasarela | ✅ |
| [302.AI](https://share.302.ai/SuTG99) | Pasarela | ✅ |
[→ Ver todas las integraciones](https://docs.langbot.app/en/insight/features.html)
---
## ¿Por qué LangBot?
| Caso de Uso | Cómo Ayuda LangBot |
|----------|-------------------|
| **Atención al cliente** | Despliegue agentes de IA en Slack/Discord/Telegram que respondan preguntas usando su base de conocimientos |
| **Herramientas internas** | Conecte flujos de trabajo de n8n/Dify a WeCom/DingTalk para procesos empresariales automatizados |
| **Gestión de comunidades** | Modere grupos de QQ/Discord con filtrado de contenido e interacción impulsados por IA |
| **Presencia multiplataforma** | Un solo bot, todas las plataformas. Gestione desde un único panel de control |
---
## Demo en Vivo
**Pruébelo ahora:** https://demo.langbot.dev/
- Correo electrónico: `demo@langbot.app`
- Contraseña: `langbot123456`
*Nota: Entorno de demostración público. No ingrese información confidencial.*
---
## Comunidad
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&label=Discord)](https://discord.gg/wdNEHETs87)
- [Comunidad de Discord](https://discord.gg/wdNEHETs87)
---
## Historial de Stars
[![Star History Chart](https://api.star-history.com/svg?repos=langbot-app/LangBot&type=Date)](https://star-history.com/#langbot-app/LangBot&Date)
---
## Colaboradores
Gracias a todos los [colaboradores](https://github.com/langbot-app/LangBot/graphs/contributors) que han ayudado a mejorar LangBot:
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a>

171
README_FR.md Normal file
View File

@@ -0,0 +1,171 @@
<p align="center">
<a href="https://langbot.app">
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
</a>
<div align="center">
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production&#0045;grade&#0032;IM&#0032;bot&#0032;made&#0032;easy&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<h3>Plateforme de niveau production pour construire des bots de messagerie instantanée avec agents IA.</h3>
<h4>Créez, déboguez et déployez rapidement des bots IA sur Slack, Discord, Telegram, WeChat et plus.</h4>
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / Français / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langbot-app/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/langbot-app/LangBot)](https://github.com/langbot-app/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![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://docs.langbot.app/en/insight/features.html">Fonctionnalités</a>
<a href="https://docs.langbot.app/en/insight/guide.html">Documentation</a>
<a href="https://docs.langbot.app/en/tags/readme.html">API</a>
<a href="https://space.langbot.app">Marché des Plugins</a>
<a href="https://langbot.featurebase.app/roadmap">Feuille de Route</a>
</div>
</p>
---
## Qu'est-ce que LangBot ?
LangBot est une **plateforme open-source de niveau production** pour créer des bots de messagerie instantanée alimentés par l'IA. Elle connecte les grands modèles de langage (LLMs) à n'importe quelle plateforme de chat, vous permettant de créer des agents intelligents capables de converser, d'exécuter des tâches et de s'intégrer à vos workflows existants.
### Capacités Clés
- **Conversations IA & Agents** — Dialogues multi-tours, appels d'outils, support multimodal, sortie en streaming. RAG (base de connaissances) intégré avec intégration profonde de [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
- **Support Universel des Plateformes de MI** — Un seul code pour Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
- **Prêt pour la Production** — Contrôle d'accès, limitation de débit, filtrage de mots sensibles, surveillance complète et gestion des exceptions. Approuvé par les entreprises.
- **Écosystème de Plugins** — Des centaines de plugins, architecture événementielle, extensions de composants, et support du [protocole MCP](https://modelcontextprotocol.io/).
- **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.
[→ En savoir plus sur toutes les fonctionnalités](https://docs.langbot.app/en/insight/features.html)
---
## Démarrage Rapide
### ☁️ LangBot Cloud (Recommandé)
**[LangBot Cloud](https://space.langbot.app/cloud)** — Sans déploiement, prêt à utiliser.
### Lancement en une ligne
```bash
uvx langbot
```
> Nécessite [uv](https://docs.astral.sh/uv/getting-started/installation/). Visitez http://localhost:5300 — c'est prêt.
### Docker Compose
```bash
git clone https://github.com/langbot-app/LangBot
cd LangBot/docker
docker compose up -d
```
### Déploiement Cloud en un Clic
[![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)
**Plus d'options :** [Docker](https://docs.langbot.app/en/deploy/langbot/docker.html) · [Manuel](https://docs.langbot.app/en/deploy/langbot/manual.html) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
---
## Plateformes Supportées
| Plateforme | Statut | Notes |
|----------|--------|-------|
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | ✅ | |
| QQ | ✅ | Personnel & API Officielle |
| WeCom | ✅ | WeChat Entreprise, CS Externe, AI Bot |
| WeChat | ✅ | Personnel & Compte Officiel |
| Lark | ✅ | |
| DingTalk | ✅ | |
| KOOK | ✅ | |
| Satori | ✅ | |
---
## LLMs et Intégrations Supportés
| Fournisseur | Type | Statut |
|----------|------|--------|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
| [xAI](https://x.ai/) | LLM | ✅ |
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
| [Ollama](https://ollama.com/) | LLM Local | ✅ |
| [LM Studio](https://lmstudio.ai/) | LLM Local | ✅ |
| [Dify](https://dify.ai) | LLMOps | ✅ |
| [MCP](https://modelcontextprotocol.io/) | Protocole | ✅ |
| [SiliconFlow](https://siliconflow.cn/) | Passerelle | ✅ |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Passerelle | ✅ |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Passerelle | ✅ |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Passerelle | ✅ |
| [GiteeAI](https://ai.gitee.com/) | Passerelle | ✅ |
| [接口 AI](https://jiekou.ai/) | Passerelle | ✅ |
| [302.AI](https://share.302.ai/SuTG99) | Passerelle | ✅ |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Plateforme GPU | ✅ |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Plateforme GPU | ✅ |
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Plateforme GPU | ✅ |
[→ Voir toutes les intégrations](https://docs.langbot.app/en/insight/features.html)
---
## Pourquoi LangBot ?
| Cas d'Usage | Comment LangBot Aide |
|----------|-------------------|
| **Support Client** | Déployez des agents IA sur Slack/Discord/Telegram qui répondent aux questions en utilisant votre base de connaissances |
| **Outils Internes** | Connectez les workflows n8n/Dify à WeCom/DingTalk pour automatiser vos processus métier |
| **Gestion de Communauté** | Modérez les groupes QQ/Discord avec un filtrage de contenu et des interactions alimentés par l'IA |
| **Présence Multi-plateforme** | Un seul bot, toutes les plateformes. Gérez tout depuis un tableau de bord unique |
---
## Démo en Ligne
**Essayez maintenant :** https://demo.langbot.dev/
- Email : `demo@langbot.app`
- Mot de passe : `langbot123456`
*Note : Environnement de démonstration public. Ne saisissez pas d'informations sensibles.*
---
## Communauté
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&label=Discord)](https://discord.gg/wdNEHETs87)
- [Communauté Discord](https://discord.gg/wdNEHETs87)
---
## Historique des Stars
[![Star History Chart](https://api.star-history.com/svg?repos=langbot-app/LangBot&type=Date)](https://star-history.com/#langbot-app/LangBot&Date)
---
## Contributeurs
Merci à tous les [contributeurs](https://github.com/langbot-app/LangBot/graphs/contributors) qui ont aidé à améliorer LangBot :
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a>

View File

@@ -1,41 +1,68 @@
<p align="center">
<a href="https://langbot.app">
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
</a>
<div align="center">
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / 日本語 / (PR for your language)
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production&#0045;grade&#0032;IM&#0032;bot&#0032;made&#0032;easy&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<h3>AIエージェント搭載IMボットを構築するための本番グレードプラットフォーム。</h3>
<h4>Slack、Discord、Telegram、WeChat などに AI ボットを素早く構築、デバッグ、デプロイ。</h4>
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / 日本語 / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langbot-app/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/langbot-app/LangBot)](https://github.com/langbot-app/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![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://docs.langbot.app/en/insight/guide.html">デプロイ</a>
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">プラグイン</a>
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">プラグインの提出</a>
<a href="https://docs.langbot.app/ja/insight/features.html">機能</a>
<a href="https://docs.langbot.app/ja/insight/guide.html">ドキュメント</a>
<a href="https://docs.langbot.app/ja/tags/readme.html">API</a>
<a href="https://space.langbot.app">プラグインマーケット</a>
<a href="https://langbot.featurebase.app/roadmap">ロードマップ</a>
</div>
</p>
LangBot は、エージェント、RAG、MCP などの LLM アプリケーション機能を備えた、オープンソースの LLM ネイティブのインスタントメッセージングロボット開発プラットフォームです。世界中のインスタントメッセージングプラットフォームに適応し、豊富な API インターフェースを提供し、カスタム開発をサポートします。
---
## 📦 始め方
## LangBot とは?
#### クイックスタート
LangBot は、AI搭載のインスタントメッセージングボットを構築するための**オープンソースの本番グレードプラットフォーム**です。大規模言語モデルLLMをあらゆるチャットプラットフォームに接続し、会話、タスク実行、既存のワークフローとの統合が可能なインテリジェントエージェントを作成できます。
`uvx` を使用した迅速なデプロイ([uv](https://docs.astral.sh/uv/getting-started/installation/) が必要です):
### 主な機能
- **AI対話とエージェント** — マルチターン対話、ツール呼び出し、マルチモーダル対応、ストリーミング出力。RAGナレッジベースを内蔵し、[Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org) と深く統合。
- **ユニバーサルIMプラットフォーム対応** — 単一のコードベースで Discord、Telegram、Slack、LINE、QQ、WeChat、WeCom、Lark、DingTalk、KOOK に対応。
- **本番環境対応** — アクセス制御、レート制限、センシティブワードフィルタリング、包括的な監視、例外処理を搭載。エンタープライズの信頼に応える品質。
- **プラグインエコシステム** — 数百のプラグイン、イベント駆動アーキテクチャ、コンポーネント拡張、[MCPプロトコル](https://modelcontextprotocol.io/)対応。
- **Web管理パネル** — 直感的なブラウザインターフェースからボットの設定、管理、監視が可能。YAML編集は不要。
- **マルチパイプラインアーキテクチャ** — 異なるシナリオに異なるボットを配置し、包括的な監視と例外処理を実現。
[→ すべての機能について詳しく見る](https://docs.langbot.app/ja/insight/features.html)
---
## クイックスタート
### ☁️ LangBot Cloud推奨
**[LangBot Cloud](https://space.langbot.app/cloud)** — デプロイ不要、すぐに使えます。
### ワンライン起動
```bash
uvx langbot
```
http://localhost:5300 にアクセスして使用を開始します
> [uv](https://docs.astral.sh/uv/getting-started/installation/) が必要です。http://localhost:5300 にアクセスして完了
#### Docker Compose デプロイ
### Docker Compose
```bash
git clone https://github.com/langbot-app/LangBot
@@ -43,98 +70,101 @@ cd LangBot/docker
docker compose up -d
```
http://localhost:5300 にアクセスして使用を開始します。
詳細なドキュメントは[Dockerデプロイ](https://docs.langbot.app/en/deploy/langbot/docker.html)を参照してください。
#### Panelでのワンクリックデプロイ
LangBotはBTPanelにリストされています。BTPanelをインストールしている場合は、[ドキュメント](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html)を使用して使用できます。
#### Zeaburクラウドデプロイ
コミュニティが提供するZeaburテンプレート。
### ワンクリッククラウドデプロイ
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH)
#### Railwayクラウドデプロイ
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
#### その他のデプロイ方法
**その他:** [Docker](https://docs.langbot.app/en/deploy/langbot/docker.html) · [手動デプロイ](https://docs.langbot.app/en/deploy/langbot/manual.html) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
リリースバージョンを直接使用して実行します。[手動デプロイ](https://docs.langbot.app/en/deploy/langbot/manual.html)のドキュメントを参照してください。
---
#### Kubernetes デプロイ
[Kubernetes デプロイ](./docker/README_K8S.md) ドキュメントを参照してください。
## 😎 最新情報を入手
リポジトリの右上にある Star と Watch ボタンをクリックして、最新の更新を取得してください。
![star gif](https://docs.langbot.app/star.gif)
## ✨ 機能
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル、ストリーミング出力機能をサポート、RAG知識ベースを組み込み、[Dify](https://dify.ai) と深く統合。
- 🤖 多プラットフォーム対応: 現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。複数のパイプライン設定をサポートし、異なるボットを異なる用途に使用できます。
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
- 😻 Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。
詳細な仕様については、[ドキュメント](https://docs.langbot.app/en/insight/features.html)を参照してください。
または、デモ環境にアクセスしてください: https://demo.langbot.dev/
- ログイン情報: メール: `demo@langbot.app` パスワード: `langbot123456`
- 注意: WebUI のデモンストレーションのみの場合、公開環境では機密情報を入力しないでください。
### メッセージプラットフォーム
## 対応プラットフォーム
| プラットフォーム | ステータス | 備考 |
| --- | --- | --- |
|----------|--------|-------|
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | ✅ | |
| 個人QQ | ✅ | |
| QQ公式API | ✅ | |
| WeCom | ✅ | |
| WeComCS | ✅ | |
| WeCom AI Bot | ✅ | |
| 個人WeChat | ✅ | |
| QQ | ✅ | 個人 & 公式API |
| WeCom | ✅ | 企業WeChat、外部CS、AIボット |
| WeChat | ✅ | 個人 & 公式アカウント |
| Lark | ✅ | |
| DingTalk | ✅ | |
| KOOK | ✅ | |
| Satori | ✅ | |
### LLMs
---
| LLM | ステータス | 備考 |
| --- | --- | --- |
| [OpenAI](https://platform.openai.com/) | ✅ | 任意のOpenAIインターフェース形式モデルに対応 |
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
| [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | |
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
| [接口 AI](https://jiekou.ai/) | ✅ | LLMゲートウェイ(MaaS) |
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLMとGPUリソースプラットフォーム |
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLMゲートウェイ(MaaS) |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム |
| [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム |
| [LMStudio](https://lmstudio.ai/) | ✅ | ローカルLLM実行プラットフォーム |
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLMインターフェースゲートウェイ(MaaS) |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLMゲートウェイ(MaaS) |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLMゲートウェイ(MaaS) |
| [MCP](https://modelcontextprotocol.io/) | ✅ | MCPプロトコルをサポート |
## 対応LLMと統合
## 🤝 コミュニティ貢献
| プロバイダー | タイプ | ステータス |
|----------|------|--------|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
| [xAI](https://x.ai/) | LLM | ✅ |
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
| [Ollama](https://ollama.com/) | ローカルLLM | ✅ |
| [LM Studio](https://lmstudio.ai/) | ローカルLLM | ✅ |
| [Dify](https://dify.ai) | LLMOps | ✅ |
| [MCP](https://modelcontextprotocol.io/) | プロトコル | ✅ |
| [SiliconFlow](https://siliconflow.cn/) | ゲートウェイ | ✅ |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ゲートウェイ | ✅ |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ゲートウェイ | ✅ |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ゲートウェイ | ✅ |
| [GiteeAI](https://ai.gitee.com/) | ゲートウェイ | ✅ |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPUプラットフォーム | ✅ |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPUプラットフォーム | ✅ |
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPUプラットフォーム | ✅ |
| [接口 AI](https://jiekou.ai/) | ゲートウェイ | ✅ |
| [302.AI](https://share.302.ai/SuTG99) | ゲートウェイ | ✅ |
LangBot への貢献に対して、以下の [コード貢献者](https://github.com/langbot-app/LangBot/graphs/contributors) とコミュニティの他のメンバーに感謝します。
[→ すべての統合を表示](https://docs.langbot.app/en/insight/features.html)
---
## なぜ LangBot
| ユースケース | LangBot の活用方法 |
|----------|-------------------|
| **カスタマーサポート** | ナレッジベースを活用して質問に回答するAIエージェントをSlack/Discord/Telegramにデプロイ |
| **社内ツール** | n8n/Difyのワークフローを WeCom/DingTalk に接続し、業務プロセスを自動化 |
| **コミュニティ管理** | AI搭載のコンテンツフィルタリングとインタラクションでQQ/Discordグループをモデレーション |
| **マルチプラットフォーム展開** | 1つのボットで全プラットフォームに対応。単一のダッシュボードから管理 |
---
## ライブデモ
**今すぐ試す:** https://demo.langbot.dev/
- メール: `demo@langbot.app`
- パスワード: `langbot123456`
*注意: 公開デモ環境です。機密情報を入力しないでください。*
---
## コミュニティ
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&label=Discord)](https://discord.gg/wdNEHETs87)
- [Discord コミュニティ](https://discord.gg/wdNEHETs87)
---
## Star 推移
[![Star History Chart](https://api.star-history.com/svg?repos=langbot-app/LangBot&type=Date)](https://star-history.com/#langbot-app/LangBot&Date)
---
## コントリビューター
LangBot をより良くするために貢献してくださったすべての[コントリビューター](https://github.com/langbot-app/LangBot/graphs/contributors)に感謝します:
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />

171
README_KO.md Normal file
View File

@@ -0,0 +1,171 @@
<p align="center">
<a href="https://langbot.app">
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
</a>
<div align="center">
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production&#0045;grade&#0032;IM&#0032;bot&#0032;made&#0032;easy&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<h3>AI 에이전트 IM 봇 구축을 위한 프로덕션 등급 플랫폼.</h3>
<h4>Slack, Discord, Telegram, WeChat 등에 AI 봇을 빠르게 구축, 디버그 및 배포.</h4>
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / 한국어 / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langbot-app/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/langbot-app/LangBot)](https://github.com/langbot-app/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![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://docs.langbot.app/en/insight/features.html">기능</a>
<a href="https://docs.langbot.app/en/insight/guide.html">문서</a>
<a href="https://docs.langbot.app/en/tags/readme.html">API</a>
<a href="https://space.langbot.app">플러그인 마켓</a>
<a href="https://langbot.featurebase.app/roadmap">로드맵</a>
</div>
</p>
---
## LangBot이란?
LangBot은 AI 기반 인스턴트 메시징 봇을 구축하기 위한 **오픈소스 프로덕션 등급 플랫폼**입니다. 대규모 언어 모델(LLM)을 모든 채팅 플랫폼에 연결하여 대화, 작업 실행, 기존 워크플로우와의 통합이 가능한 지능형 에이전트를 만들 수 있습니다.
### 핵심 기능
- **AI 대화 및 에이전트** — 멀티턴 대화, 도구 호출, 멀티모달 지원, 스트리밍 출력. 내장 RAG(지식 베이스)와 [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org) 심층 통합.
- **유니버설 IM 플랫폼 지원** — 단일 코드베이스로 Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK 지원.
- **프로덕션 레디** — 접근 제어, 속도 제한, 민감어 필터링, 종합 모니터링 및 예외 처리. 기업 환경에서 검증됨.
- **플러그인 생태계** — 수백 개의 플러그인, 이벤트 기반 아키텍처, 컴포넌트 확장, [MCP 프로토콜](https://modelcontextprotocol.io/) 지원.
- **웹 관리 패널** — 직관적인 브라우저 인터페이스로 봇을 구성, 관리 및 모니터링. YAML 편집 불필요.
- **멀티 파이프라인 아키텍처** — 다양한 시나리오에 맞는 다양한 봇 구성, 종합 모니터링 및 예외 처리.
[→ 모든 기능 자세히 보기](https://docs.langbot.app/en/insight/features.html)
---
## 빠른 시작
### ☁️ LangBot Cloud (추천)
**[LangBot Cloud](https://space.langbot.app/cloud)** — 배포 없이 바로 사용.
### 원라인 실행
```bash
uvx langbot
```
> [uv](https://docs.astral.sh/uv/getting-started/installation/) 설치 필요. http://localhost:5300 방문 — 완료.
### Docker Compose
```bash
git clone https://github.com/langbot-app/LangBot
cd LangBot/docker
docker compose up -d
```
### 원클릭 클라우드 배포
[![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)
**더 많은 옵션:** [Docker](https://docs.langbot.app/en/deploy/langbot/docker.html) · [수동 배포](https://docs.langbot.app/en/deploy/langbot/manual.html) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
---
## 지원 플랫폼
| 플랫폼 | 상태 | 비고 |
|--------|------|------|
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | ✅ | |
| QQ | ✅ | 개인 및 공식 API |
| WeCom | ✅ | 기업 WeChat, 외부 CS, AI Bot |
| WeChat | ✅ | 개인 및 공식 계정 |
| Lark | ✅ | |
| DingTalk | ✅ | |
| KOOK | ✅ | |
| Satori | ✅ | |
---
## 지원 LLM 및 통합
| 제공자 | 유형 | 상태 |
|--------|------|------|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
| [xAI](https://x.ai/) | LLM | ✅ |
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
| [Ollama](https://ollama.com/) | 로컬 LLM | ✅ |
| [LM Studio](https://lmstudio.ai/) | 로컬 LLM | ✅ |
| [Dify](https://dify.ai) | LLMOps | ✅ |
| [MCP](https://modelcontextprotocol.io/) | 프로토콜 | ✅ |
| [SiliconFlow](https://siliconflow.cn/) | 게이트웨이 | ✅ |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | 게이트웨이 | ✅ |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | 게이트웨이 | ✅ |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | 게이트웨이 | ✅ |
| [GiteeAI](https://ai.gitee.com/) | 게이트웨이 | ✅ |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU 플랫폼 | ✅ |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU 플랫폼 | ✅ |
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU 플랫폼 | ✅ |
| [接口 AI](https://jiekou.ai/) | 게이트웨이 | ✅ |
| [302.AI](https://share.302.ai/SuTG99) | 게이트웨이 | ✅ |
[→ 모든 통합 보기](https://docs.langbot.app/en/insight/features.html)
---
## 왜 LangBot인가?
| 사용 사례 | LangBot 활용 방법 |
|-----------|-------------------|
| **고객 지원** | 지식 베이스를 활용하여 질문에 답변하는 AI 에이전트를 Slack/Discord/Telegram에 배포 |
| **내부 도구** | n8n/Dify 워크플로우를 WeCom/DingTalk에 연결하여 비즈니스 프로세스 자동화 |
| **커뮤니티 관리** | AI 기반 콘텐츠 필터링 및 상호작용으로 QQ/Discord 그룹 관리 |
| **멀티 플랫폼** | 하나의 봇으로 모든 플랫폼 지원. 단일 대시보드에서 관리 |
---
## 라이브 데모
**지금 체험:** https://demo.langbot.dev/
- 이메일: `demo@langbot.app`
- 비밀번호: `langbot123456`
*참고: 공개 데모 환경입니다. 민감한 정보를 입력하지 마세요.*
---
## 커뮤니티
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&label=Discord)](https://discord.gg/wdNEHETs87)
- [Discord 커뮤니티](https://discord.gg/wdNEHETs87)
---
## Star 추이
[![Star History Chart](https://api.star-history.com/svg?repos=langbot-app/LangBot&type=Date)](https://star-history.com/#langbot-app/LangBot&Date)
---
## 기여자
LangBot을 더 나은 프로젝트로 만들어 주신 모든 [기여자](https://github.com/langbot-app/LangBot/graphs/contributors)분들께 감사드립니다:
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a>

171
README_RU.md Normal file
View File

@@ -0,0 +1,171 @@
<p align="center">
<a href="https://langbot.app">
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
</a>
<div align="center">
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production&#0045;grade&#0032;IM&#0032;bot&#0032;made&#0032;easy&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<h3>Платформа производственного уровня для создания агентных IM-ботов.</h3>
<h4>Быстро создавайте, отлаживайте и развертывайте ИИ-ботов в Slack, Discord, Telegram, WeChat и других платформах.</h4>
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / Русский / [Tiếng Việt](README_VI.md)
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langbot-app/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/langbot-app/LangBot)](https://github.com/langbot-app/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![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://docs.langbot.app/en/insight/features.html">Возможности</a>
<a href="https://docs.langbot.app/en/insight/guide.html">Документация</a>
<a href="https://docs.langbot.app/en/tags/readme.html">API</a>
<a href="https://space.langbot.app">Магазин плагинов</a>
<a href="https://langbot.featurebase.app/roadmap">Дорожная карта</a>
</div>
</p>
---
## Что такое LangBot?
LangBot — это **платформа с открытым исходным кодом производственного уровня** для создания ИИ-ботов в мессенджерах. Она связывает большие языковые модели (LLM) с любой чат-платформой, позволяя создавать интеллектуальных агентов, которые могут вести диалоги, выполнять задачи и интегрироваться с вашими существующими рабочими процессами.
### Ключевые возможности
- **ИИ-диалоги и агенты** — Многораундовые диалоги, вызов инструментов, мультимодальная поддержка, потоковый вывод. Встроенная реализация RAG (база знаний) с глубокой интеграцией в [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
- **Универсальная поддержка IM-платформ** — Единая кодовая база для Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
- **Готовность к продакшену** — Контроль доступа, ограничение скорости, фильтрация чувствительных слов, комплексный мониторинг и обработка исключений. Проверено в корпоративной среде.
- **Экосистема плагинов** — Сотни плагинов, событийно-ориентированная архитектура, расширения компонентов и поддержка [протокола MCP](https://modelcontextprotocol.io/).
- **Веб-панель управления** — Настраивайте, управляйте и мониторьте ваших ботов через интуитивный браузерный интерфейс. Ручное редактирование YAML не требуется.
- **Мультиконвейерная архитектура** — Разные боты для разных сценариев с комплексным мониторингом и обработкой исключений.
[→ Подробнее обо всех возможностях](https://docs.langbot.app/en/insight/features.html)
---
## Быстрый старт
### ☁️ LangBot Cloud (Рекомендуется)
**[LangBot Cloud](https://space.langbot.app/cloud)** — Без развёртывания, готово к использованию.
### Запуск одной командой
```bash
uvx langbot
```
> Требуется [uv](https://docs.astral.sh/uv/getting-started/installation/). Откройте http://localhost:5300 — готово.
### Docker Compose
```bash
git clone https://github.com/langbot-app/LangBot
cd LangBot/docker
docker compose up -d
```
### Облачное развертывание одним кликом
[![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)
**Другие варианты:** [Docker](https://docs.langbot.app/en/deploy/langbot/docker.html) · [Ручная установка](https://docs.langbot.app/en/deploy/langbot/manual.html) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
---
## Поддерживаемые платформы
| Платформа | Статус | Примечания |
|-----------|--------|------------|
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | ✅ | |
| QQ | ✅ | Личный и официальный API |
| WeCom | ✅ | Корпоративный WeChat, внешний CS, AI-бот |
| WeChat | ✅ | Личный и официальный аккаунт |
| Lark | ✅ | |
| DingTalk | ✅ | |
| KOOK | ✅ | |
| Satori | ✅ | |
---
## Поддерживаемые LLM и интеграции
| Провайдер | Тип | Статус |
|-----------|-----|--------|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
| [xAI](https://x.ai/) | LLM | ✅ |
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
| [Ollama](https://ollama.com/) | Локальный LLM | ✅ |
| [LM Studio](https://lmstudio.ai/) | Локальный LLM | ✅ |
| [Dify](https://dify.ai) | LLMOps | ✅ |
| [MCP](https://modelcontextprotocol.io/) | Протокол | ✅ |
| [SiliconFlow](https://siliconflow.cn/) | Шлюз | ✅ |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Шлюз | ✅ |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Шлюз | ✅ |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Шлюз | ✅ |
| [GiteeAI](https://ai.gitee.com/) | Шлюз | ✅ |
| [302.AI](https://share.302.ai/SuTG99) | Шлюз | ✅ |
| [接口 AI](https://jiekou.ai/) | Шлюз | ✅ |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Платформа GPU | ✅ |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Платформа GPU | ✅ |
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Платформа GPU | ✅ |
[→ Смотреть все интеграции](https://docs.langbot.app/en/insight/features.html)
---
## Почему LangBot?
| Сценарий использования | Как помогает LangBot |
|------------------------|----------------------|
| **Поддержка клиентов** | Разверните ИИ-агентов в Slack/Discord/Telegram, которые отвечают на вопросы, используя вашу базу знаний |
| **Внутренние инструменты** | Подключите рабочие процессы n8n/Dify к WeCom/DingTalk для автоматизации бизнес-процессов |
| **Управление сообществом** | Модерируйте группы QQ/Discord с помощью ИИ-фильтрации контента и взаимодействия |
| **Мультиплатформенное присутствие** | Один бот — все платформы. Управляйте из единой панели |
---
## Демо
**Попробуйте прямо сейчас:** https://demo.langbot.dev/
- Email: `demo@langbot.app`
- Пароль: `langbot123456`
*Примечание: Публичная демо-среда. Не вводите конфиденциальную информацию.*
---
## Сообщество
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&label=Discord)](https://discord.gg/wdNEHETs87)
- [Сообщество Discord](https://discord.gg/wdNEHETs87)
---
## История Stars
[![Star History Chart](https://api.star-history.com/svg?repos=langbot-app/LangBot&type=Date)](https://star-history.com/#langbot-app/LangBot&Date)
---
## Участники
Спасибо всем [участникам](https://github.com/langbot-app/LangBot/graphs/contributors), которые помогли сделать LangBot лучше:
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a>

View File

@@ -1,43 +1,70 @@
<p align="center">
<a href="https://langbot.app">
<img src="https://docs.langbot.app/social_zh.png" alt="LangBot"/>
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
</a>
<div align="center"><a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<div align="center">
[English](README_EN.md) / [简体中文](README.md) / 繁體中文 / [日本語](README_JP.md) / (PR for your language)
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<h3>生產級 AI 即時通訊機器人開發平台。</h3>
<h4>快速建構、除錯和部署 AI 機器人到微信、QQ、飛書、Slack、Discord、Telegram 等平台。</h4>
[English](README.md) / [简体中文](README_CN.md) / 繁體中文 / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-966235608-blue)](https://qm.qq.com/q/JLi38whHum)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langbot-app/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/langbot-app/LangBot)](https://github.com/langbot-app/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![GitHub stars](https://img.shields.io/github/stars/langbot-app/LangBot?style=social)](https://github.com/langbot-app/LangBot/stargazers)
[![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot)
<a href="https://langbot.app">主頁</a>
<a href="https://docs.langbot.app/zh/insight/guide.html">部署文件</a>
<a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">外掛介紹</a>
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交外掛</a>
<a href="https://langbot.app">官網</a>
<a href="https://docs.langbot.app/zh/insight/features.html">特性</a>
<a href="https://docs.langbot.app/zh/insight/guide.html">文件</a>
<a href="https://docs.langbot.app/zh/tags/readme.html">API</a>
<a href="https://space.langbot.app">外掛市場</a>
<a href="https://langbot.featurebase.app/roadmap">路線圖</a>
</div>
</p>
LangBot 是一個開源的大語言模型原生即時通訊機器人開發平台,旨在提供開箱即用的 IM 機器人開發體驗,具有 Agent、RAG、MCP 等多種 LLM 應用功能,適配全球主流即時通訊平台,並提供豐富的 API 介面,支援自定義開發。
---
## 📦 開始使用
## 什麼是 LangBot
#### 快速部署
LangBot 是一個**開源的生產級平台**,用於建構 AI 驅動的即時通訊機器人。它將大語言模型LLM連接到各種聊天平台幫助你創建能夠對話、執行任務、並整合到現有工作流程中的智能 Agent。
使用 `uvx` 一鍵啟動(需要先安裝 [uv](https://docs.astral.sh/uv/getting-started/installation/)
### 核心能力
- **AI 對話與 Agent** — 多輪對話、工具調用、多模態、流式輸出。自帶 RAG知識庫深度整合 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org) 等 LLMOps 平台。
- **全平台支援** — 一套程式碼,覆蓋 QQ、微信、企業微信、飛書、釘釘、Discord、Telegram、Slack、LINE、KOOK 等平台。
- **生產就緒** — 存取控制、限速、敏感詞過濾、全面監控與異常處理,已被多家企業採用。
- **外掛生態** — 數百個外掛,事件驅動架構,組件擴展,適配 [MCP 協議](https://modelcontextprotocol.io/)。
- **Web 管理面板** — 透過瀏覽器直觀地配置、管理和監控機器人,無需手動編輯設定檔。
- **多流水線架構** — 不同機器人用於不同場景,具備全面的監控和異常處理能力。
[→ 了解更多功能特性](https://docs.langbot.app/zh/insight/features.html)
---
## 快速開始
### ☁️ LangBot Cloud推薦
**[LangBot Cloud](https://space.langbot.app/cloud)** — 免部署,開箱即用。
### 一鍵啟動
```bash
uvx langbot
```
訪問 http://localhost:5300 即可開始使用。
> 需要安裝 [uv](https://docs.astral.sh/uv/getting-started/installation/)。訪問 http://localhost:5300 即可使用。
#### Docker Compose 部署
### Docker Compose
```bash
git clone https://github.com/langbot-app/LangBot
@@ -45,99 +72,63 @@ cd LangBot/docker
docker compose up -d
```
訪問 http://localhost:5300 即可開始使用。
詳細文件[Docker 部署](https://docs.langbot.app/zh/deploy/langbot/docker.html)。
#### 寶塔面板部署
已上架寶塔面板,若您已安裝寶塔面板,可以根據[文件](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html)使用。
#### Zeabur 雲端部署
社群貢獻的 Zeabur 模板。
### 一鍵雲端部署
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH)
#### Railway 雲端部署
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
#### 手動部署
**更多方式:** [Docker](https://docs.langbot.app/zh/deploy/langbot/docker.html) · [手動部署](https://docs.langbot.app/zh/deploy/langbot/manual.html) · [寶塔面板](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
直接使用發行版運行,查看文件[手動部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
---
#### Kubernetes 部署
參考 [Kubernetes 部署](./docker/README_K8S.md) 文件。
## 😎 保持更新
點擊倉庫右上角 Star 和 Watch 按鈕,獲取最新動態。
![star gif](https://docs.langbot.app/star.gif)
## ✨ 特性
- 💬 大模型對話、Agent支援多種大模型適配群聊和私聊具有多輪對話、工具調用、多模態、流式輸出能力自帶 RAG知識庫實現並深度適配 [Dify](https://dify.ai)。
- 🤖 多平台支援:目前支援 QQ、QQ頻道、企業微信、個人微信、飛書、Discord、Telegram 等平台。
- 🛠️ 高穩定性、功能完備:原生支援訪問控制、限速、敏感詞過濾等機制;配置簡單,支援多種部署方式。支援多流水線配置,不同機器人用於不同應用場景。
- 🧩 外掛擴展、活躍社群:支援事件驅動、組件擴展等外掛機制;適配 Anthropic [MCP 協議](https://modelcontextprotocol.io/);目前已有數百個外掛。
- 😻 Web 管理面板:支援通過瀏覽器管理 LangBot 實例,不再需要手動編寫配置文件。
詳細規格特性請訪問[文件](https://docs.langbot.app/zh/insight/features.html)。
或訪問 demo 環境https://demo.langbot.dev/
- 登入資訊:郵箱:`demo@langbot.app` 密碼:`langbot123456`
- 注意:僅展示 WebUI 效果,公開環境,請不要在其中填入您的任何敏感資訊。
### 訊息平台
## 支援的平台
| 平台 | 狀態 | 備註 |
| --- | --- | --- |
|------|------|------|
| QQ | ✅ | 個人號、官方機器人(頻道、私聊、群聊) |
| 微信 | ✅ | 個人微信、微信公眾號 |
| 企業微信 | ✅ | 應用訊息、對外客服、智能機器人 |
| 飛書 | ✅ | |
| 釘釘 | ✅ | |
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | ✅ | |
| QQ 個人號 | ✅ | QQ 個人號私聊、群聊 |
| QQ 官方機器人 | ✅ | QQ 官方機器人,支援頻道、私聊、群聊 |
| 微信 | ✅ | |
| 企微對外客服 | ✅ | |
| 企微智能機器人 | ✅ | |
| 微信公眾號 | ✅ | |
| Lark | ✅ | |
| DingTalk | ✅ | |
| KOOK | ✅ | |
| Satori | ✅ | |
### 大模型能力
---
| 模型 | 狀態 | 備註 |
| --- | --- | --- |
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 介面格式模型 |
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
| [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | |
| [智譜AI](https://open.bigmodel.cn/) | ✅ | |
| [勝算雲](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | 大模型和 GPU 資源平台 |
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 資源平台 |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 資源平台 |
| [接口 AI](https://jiekou.ai/) | ✅ | 大模型聚合平台,專注全球大模型接入 |
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型運行平台 |
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型運行平台 |
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型介面聚合平台 |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
| [阿里雲百煉](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支援通過 MCP 協議獲取工具 |
## 支援的大模型與整合
### TTS
| 提供商 | 類型 | 狀態 |
|--------|------|------|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
| [xAI](https://x.ai/) | LLM | ✅ |
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
| [智譜AI](https://open.bigmodel.cn/) | LLM | ✅ |
| [Ollama](https://ollama.com/) | 本地 LLM | ✅ |
| [LM Studio](https://lmstudio.ai/) | 本地 LLM | ✅ |
| [Dify](https://dify.ai) | LLMOps | ✅ |
| [MCP](https://modelcontextprotocol.io/) | 協議 | ✅ |
| [SiliconFlow](https://siliconflow.cn/) | 聚合平台 | ✅ |
| [阿里雲百煉](https://bailian.console.aliyun.com/) | 聚合平台 | ✅ |
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | 聚合平台 | ✅ |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | 聚合平台 | ✅ |
| [GiteeAI](https://ai.gitee.com/) | 聚合平台 | ✅ |
| [勝算雲](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU 平台 | ✅ |
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU 平台 | ✅ |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU 平台 | ✅ |
| [接口 AI](https://jiekou.ai/) | 聚合平台 | ✅ |
| [302.AI](https://share.302.ai/SuTG99) | 聚合平台 | ✅ |
### TTS語音合成
| 平台/模型 | 備註 |
| --- | --- |
|-----------|------|
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
| [AzureTTS](https://portal.azure.com/) | [外掛](https://github.com/Ingnaryk/LangBot_AzureTTS) |
@@ -145,13 +136,54 @@ docker compose up -d
### 文生圖
| 平台/模型 | 備註 |
| --- | --- |
| 阿里雲百煉 | [外掛](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
|-----------|------|
| 阿里雲百煉 | [外掛](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin) |
## 😘 社群貢獻
[→ 查看完整整合列表](https://docs.langbot.app/zh/insight/features.html)
感謝以下[程式碼貢獻者](https://github.com/langbot-app/LangBot/graphs/contributors)和社群裡其他成員對 LangBot 的貢獻:
---
## 為什麼選擇 LangBot
| 使用場景 | LangBot 如何幫助 |
|----------|------------------|
| **客戶服務** | 將 AI Agent 部署到微信/企微/釘釘/飛書,基於知識庫自動回答使用者問題 |
| **內部工具** | 將 n8n/Dify 工作流接入企微/釘釘,實現業務流程自動化 |
| **社群運營** | 在 QQ/Discord 群中使用 AI 驅動的內容審核與智能互動 |
| **多平台觸達** | 一個機器人,覆蓋所有平台。透過統一面板集中管理 |
---
## 線上演示
**立即體驗:** https://demo.langbot.dev/
- 信箱:`demo@langbot.app`
- 密碼:`langbot123456`
*注意:公開演示環境,請不要在其中填入任何敏感資訊。*
---
## 社群
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&label=Discord)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-966235608-blue)](https://qm.qq.com/q/JLi38whHum)
- [Discord 社群](https://discord.gg/wdNEHETs87)
- [QQ 社群群](https://qm.qq.com/q/JLi38whHum)
---
## Star 趨勢
[![Star History Chart](https://api.star-history.com/svg?repos=langbot-app/LangBot&type=Date)](https://star-history.com/#langbot-app/LangBot&Date)
---
## 貢獻者
感謝所有[貢獻者](https://github.com/langbot-app/LangBot/graphs/contributors)對 LangBot 的幫助:
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a>
</a>

171
README_VI.md Normal file
View File

@@ -0,0 +1,171 @@
<p align="center">
<a href="https://langbot.app">
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
</a>
<div align="center">
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production&#0045;grade&#0032;IM&#0032;bot&#0032;made&#0032;easy&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<h3>Nền tảng cấp sản xuất để xây dựng bot IM với AI agent.</h3>
<h4>Xây dựng, gỡ lỗi và triển khai bot AI nhanh chóng trên Slack, Discord, Telegram, WeChat và nhiều nền tảng khác.</h4>
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / Tiếng Việt
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langbot-app/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/langbot-app/LangBot)](https://github.com/langbot-app/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![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://docs.langbot.app/en/insight/features.html">Tính năng</a>
<a href="https://docs.langbot.app/en/insight/guide.html">Tài liệu</a>
<a href="https://docs.langbot.app/en/tags/readme.html">API</a>
<a href="https://space.langbot.app">Chợ Plugin</a>
<a href="https://langbot.featurebase.app/roadmap">Lộ trình</a>
</div>
</p>
---
## LangBot là gì?
LangBot là một **nền tảng mã nguồn mở, cấp sản xuất** để xây dựng bot nhắn tin tức thời được hỗ trợ bởi AI. Nó kết nối các Mô hình Ngôn ngữ Lớn (LLM) với bất kỳ nền tảng chat nào, cho phép bạn tạo các agent thông minh có thể trò chuyện, thực hiện tác vụ và tích hợp với quy trình làm việc hiện có của bạn.
### Khả năng chính
- **Hội thoại AI & Agent** — Đối thoại nhiều lượt, gọi công cụ, hỗ trợ đa phương thức, đầu ra streaming. RAG (cơ sở kiến thức) tích hợp sẵn với tích hợp sâu vào [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
- **Hỗ trợ đa nền tảng IM** — Một mã nguồn cho Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
- **Sẵn sàng cho sản xuất** — Kiểm soát truy cập, giới hạn tốc độ, lọc từ nhạy cảm, giám sát toàn diện và xử lý ngoại lệ. Được doanh nghiệp tin dùng.
- **Hệ sinh thái Plugin** — Hàng trăm plugin, kiến trúc hướng sự kiện, mở rộng thành phần, và hỗ trợ [giao thức MCP](https://modelcontextprotocol.io/).
- **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ệ.
[→ Tìm hiểu thêm về tất cả tính năng](https://docs.langbot.app/en/insight/features.html)
---
## Bắt đầu nhanh
### ☁️ LangBot Cloud (Khuyên dùng)
**[LangBot Cloud](https://space.langbot.app/cloud)** — Không cần triển khai, sẵn sàng sử dụng.
### Khởi chạy một dòng
```bash
uvx langbot
```
> Yêu cầu [uv](https://docs.astral.sh/uv/getting-started/installation/). Truy cập http://localhost:5300 — xong.
### Docker Compose
```bash
git clone https://github.com/langbot-app/LangBot
cd LangBot/docker
docker compose up -d
```
### Triển khai đám mây một cú nhấp
[![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)
**Thêm tùy chọn:** [Docker](https://docs.langbot.app/en/deploy/langbot/docker.html) · [Thủ công](https://docs.langbot.app/en/deploy/langbot/manual.html) · [BTPanel](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) · [Kubernetes](./docker/README_K8S.md)
---
## Nền tảng được hỗ trợ
| Nền tảng | Trạng thái | Ghi chú |
|----------|--------|-------|
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | ✅ | |
| QQ | ✅ | Cá nhân & API chính thức |
| WeCom | ✅ | WeChat doanh nghiệp, CS bên ngoài, AI Bot |
| WeChat | ✅ | Cá nhân & Tài khoản công khai |
| Lark | ✅ | |
| DingTalk | ✅ | |
| KOOK | ✅ | |
| Satori | ✅ | |
---
## LLM và tích hợp được hỗ trợ
| Nhà cung cấp | Loại | Trạng thái |
|----------|------|--------|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
| [xAI](https://x.ai/) | LLM | ✅ |
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
| [Ollama](https://ollama.com/) | LLM cục bộ | ✅ |
| [LM Studio](https://lmstudio.ai/) | LLM cục bộ | ✅ |
| [Dify](https://dify.ai) | LLMOps | ✅ |
| [MCP](https://modelcontextprotocol.io/) | Giao thức | ✅ |
| [SiliconFlow](https://siliconflow.cn/) | Cổng | ✅ |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Cổng | ✅ |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Cổng | ✅ |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Cổng | ✅ |
| [GiteeAI](https://ai.gitee.com/) | Cổng | ✅ |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Nền tảng GPU | ✅ |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Nền tảng GPU | ✅ |
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Nền tảng GPU | ✅ |
| [接口 AI](https://jiekou.ai/) | Cổng | ✅ |
| [302.AI](https://share.302.ai/SuTG99) | Cổng | ✅ |
[→ Xem tất cả tích hợp](https://docs.langbot.app/en/insight/features.html)
---
## Tại sao chọn LangBot?
| Trường hợp sử dụng | LangBot giúp như thế nào |
|----------|-------------------|
| **Hỗ trợ khách hàng** | Triển khai agent AI trên Slack/Discord/Telegram để trả lời câu hỏi bằng cơ sở kiến thức của bạn |
| **Công cụ nội bộ** | Kết nối quy trình n8n/Dify với WeCom/DingTalk để tự động hóa quy trình kinh doanh |
| **Quản lý cộng đồng** | Quản lý nhóm QQ/Discord với tính năng lọc nội dung và tương tác được hỗ trợ bởi AI |
| **Đa nền tảng** | Một bot, tất cả nền tảng. Quản lý từ một bảng điều khiển duy nhất |
---
## Demo trực tuyến
**Thử ngay:** https://demo.langbot.dev/
- Email: `demo@langbot.app`
- Mật khẩu: `langbot123456`
*Lưu ý: Môi trường demo công khai. Không nhập thông tin nhạy cảm.*
---
## Cộng đồng
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&label=Discord)](https://discord.gg/wdNEHETs87)
- [Cộng đồng Discord](https://discord.gg/wdNEHETs87)
---
## Lịch sử Star
[![Star History Chart](https://api.star-history.com/svg?repos=langbot-app/LangBot&type=Date)](https://star-history.com/#langbot-app/LangBot&Date)
---
## Người đóng góp
Cảm ơn tất cả [người đóng góp](https://github.com/langbot-app/LangBot/graphs/contributors) đã giúp LangBot trở nên tốt hơn:
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a>

View File

@@ -7,7 +7,6 @@ services:
langbot_plugin_runtime:
image: rockchin/langbot:latest
container_name: langbot_plugin_runtime
platform: linux/amd64 # For Apple Silicon compatibility
volumes:
- ./data/plugins:/app/data/plugins
ports:
@@ -15,23 +14,21 @@ services:
restart: on-failure
environment:
- TZ=Asia/Shanghai
command: ["uv", "run", "-m", "langbot_plugin.cli.__init__", "rt"]
command: ["uv", "run", "--no-sync", "-m", "langbot_plugin.cli.__init__", "rt"]
networks:
- langbot_network
langbot:
image: rockchin/langbot:latest
container_name: langbot
platform: linux/amd64 # For Apple Silicon compatibility
volumes:
- ./data:/app/data
- ./plugins:/app/plugins
restart: on-failure
environment:
- TZ=Asia/Shanghai
ports:
- 5300:5300 # For web ui
- 2280-2290:2280-2290 # For platform webhook
- 5300:5300 # For web ui and webhook callback
- 2280-2285:2280-2285 # For platform reverse connection
networks:
- langbot_network

412
docs/MIGRATION_SUMMARY.md Normal file
View File

@@ -0,0 +1,412 @@
# WebChat 到 WebSocket 迁移总结
## 概述
已完全移除旧的基于SSE的WebChat系统并替换为基于WebSocket的双向实时通信系统。这是一个内置在LangBot中的完整IM系统支持流式输出。
## 已删除的文件
### 后端
-`src/langbot/pkg/api/http/controller/groups/pipelines/webchat.py` - 旧的SSE路由
-`src/langbot/pkg/platform/sources/webchat.py` - 旧的WebChat适配器
-`src/langbot/pkg/platform/sources/webchat.yaml` - 旧的配置文件
### 前端
- ❌ BackendClient中所有SSE相关代码已完全移除
- ❌ DebugDialog中所有SSE相关逻辑已完全替换
## 新增的文件
### 后端核心文件
**1. WebSocket连接管理器**
```
src/langbot/pkg/platform/sources/websocket_manager.py
```
- 管理所有并发WebSocket连接
- 线程安全的连接池
- 按流水线、会话类型分组
- 广播和单播消息功能
- 连接统计和监控
**2. WebSocket适配器**
```
src/langbot/pkg/platform/sources/websocket_adapter.py
```
- 实现平台适配器接口
- **完整流式支持** (`reply_message_chunk` 方法)
- 双向消息流处理
- 消息历史管理
- 会话管理
**3. WebSocket路由控制器**
```
src/langbot/pkg/api/http/controller/groups/pipelines/websocket_chat.py
```
- WebSocket端点处理
- REST API接口
- 心跳机制
- 连接生命周期管理
**4. 配置文件**
```
src/langbot/pkg/platform/sources/websocket.yaml
```
- WebSocket适配器元数据
### 前端核心文件
**1. WebSocket客户端**
```
web/src/app/infra/websocket/WebSocketClient.ts
```
- WebSocket连接管理
- 自动重连最多5次
- 心跳机制30秒
- 事件回调系统
**2. 更新的组件**
```
web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx
```
- 完全重写使用WebSocket
- 实时连接状态显示
- 流式消息支持
- 自动重连
**3. HTTP客户端更新**
```
web/src/app/infra/http/BackendClient.ts
```
- 移除所有旧的WebChat API
- 仅保留WebSocket API
### 测试工具
**Python测试客户端**
```
test_websocket_client.py
```
- 单连接交互测试
- 多连接并发测试
- 命令行工具
### 文档
**使用文档**
```
WEBSOCKET_README.md
```
- 完整的API文档
- 架构说明
- 使用示例
- 故障排查
## 核心变更
### 后端变更
**1. botmgr.py**
- ❌ 移除 `webchat_proxy_bot`
- ✅ 仅保留 `websocket_proxy_bot`
- ✅ 更新适配器过滤逻辑(排除`websocket`而非`webchat`
**2. 适配器注册**
```python
# 旧代码(已删除)
webchat_adapter_class = self.adapter_dict['webchat']
self.webchat_proxy_bot = RuntimeBot(...)
# 新代码
websocket_adapter_class = self.adapter_dict['websocket']
self.websocket_proxy_bot = RuntimeBot(
uuid='websocket-proxy-bot',
name='WebSocket',
adapter='websocket',
...
)
```
### 前端变更
**1. API调用完全更换**
旧代码(已删除):
```typescript
// SSE流式请求
await fetch(url, {
method: 'POST',
body: JSON.stringify({ is_stream: true })
})
// 手动解析 text/event-stream
```
新代码:
```typescript
// WebSocket实时通信
const wsClient = new WebSocketClient(pipelineId, sessionType);
await wsClient.connect();
wsClient.onMessage((message) => {
// 流式消息自动处理
setMessages(prev => [...prev, message]);
});
wsClient.sendMessage(messageChain);
```
**2. 连接状态管理**
新增功能:
- ✅ 实时连接状态指示器(绿色/红色圆点)
- ✅ 连接/断开toast提示
- ✅ 自动重连逻辑
- ✅ 心跳保活
**3. 流式支持**
完整的流式消息处理:
```typescript
wsClient.onMessage((message) => {
if (message.is_final) {
// 最终消息
finalizeBotMessage(message);
} else {
// 中间消息块实时更新UI
updateBotMessage(message);
}
});
```
## API对比
### WebSocket端点
**连接**
```
ws://localhost:8000/api/v1/pipelines/<pipeline_uuid>/ws/connect?session_type=<person|group>
```
**消息格式**
客户端发送:
```json
{
"type": "message",
"message": [
{"type": "Plain", "text": "你好"}
]
}
```
服务器响应(流式):
```json
{
"type": "response",
"data": {
"id": 1,
"role": "assistant",
"content": "你好,我是...",
"is_final": false,
"timestamp": "2025-01-28T..."
}
}
```
### REST API
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/v1/pipelines/<uuid>/ws/messages/<type>` | GET | 获取消息历史 |
| `/api/v1/pipelines/<uuid>/ws/reset/<type>` | POST | 重置会话 |
| `/api/v1/pipelines/<uuid>/ws/connections` | GET | 获取连接统计 |
| `/api/v1/pipelines/<uuid>/ws/broadcast` | POST | 广播消息 |
## 流式支持详解
### 后端流式实现
**WebSocket Adapter**
```python
async def reply_message_chunk(
self,
message_source: platform_events.MessageEvent,
bot_message,
message: platform_message.MessageChain,
quote_origin: bool = False,
is_final: bool = False,
) -> dict:
"""回复消息块 - 流式"""
message_data = WebSocketMessage(
id=-1,
role='assistant',
content=str(message),
message_chain=[component.__dict__ for component in message],
timestamp=datetime.now().isoformat(),
is_final=is_final and bot_message.tool_calls is None,
)
# 发送到队列由WebSocket连接处理发送
await session.resp_queues[message_id].put(message_data)
return message_data.model_dump()
async def is_stream_output_supported(self) -> bool:
"""WebSocket始终支持流式输出"""
return True
```
### 前端流式处理
**DebugDialog组件**
```typescript
wsClient.onMessage((message) => {
setMessages((prevMessages) => {
const existingIndex = prevMessages.findIndex(
(msg) => msg.role === 'assistant' && msg.content === 'Generating...'
);
if (existingIndex !== -1) {
// 更新正在生成的消息
const updatedMessages = [...prevMessages];
updatedMessages[existingIndex] = message;
return updatedMessages;
} else {
// 添加新消息
return [...prevMessages, message];
}
});
});
```
## 兼容性说明
### ⚠️ 不兼容旧版本
此次迁移**完全不兼容**旧的WebChat系统
1. **API端点变更**
- 旧: `/api/v1/pipelines/<uuid>/chat/send`
- 新: `ws://.../<uuid>/ws/connect`
2. **通信协议变更**
- 旧: HTTP + SSE (Server-Sent Events)
- 新: WebSocket (双向)
3. **流式实现变更**
- 旧: `text/event-stream` 格式
- 新: WebSocket JSON消息
### 迁移要求
使用新系统需要:
1. ✅ 前端必须支持WebSocket
2. ✅ 后端必须运行新的WebSocket适配器
3. ✅ 清除旧的WebChat相关配置
## 优势对比
| 特性 | 旧WebChat (SSE) | 新WebSocket |
|------|----------------|-------------|
| 双向通信 | ❌ 单向(服务器→客户端) | ✅ 双向 |
| 主动推送 | ❌ 不支持 | ✅ 支持 |
| 连接管理 | ❌ 无状态 | ✅ 有状态,完整生命周期 |
| 流式输出 | ✅ 支持 | ✅ 支持(更优) |
| 心跳机制 | ❌ 无 | ✅ 30秒心跳 |
| 自动重连 | ❌ 无 | ✅ 最多5次 |
| 多连接 | ⚠️ 难以管理 | ✅ 完整支持 |
| 连接状态 | ❌ 不可见 | ✅ 实时显示 |
| 广播功能 | ❌ 不支持 | ✅ 支持 |
## 测试方式
### 1. Python测试客户端
```bash
# 单连接测试
python test_websocket_client.py <pipeline_uuid>
# 指定会话类型
python test_websocket_client.py <pipeline_uuid> --session-type group
# 多连接并发测试5个连接
python test_websocket_client.py <pipeline_uuid> --multi 5
```
### 2. 前端测试
1. 启动LangBot服务器
2. 访问前端界面
3. 打开流水线调试对话框
4. 观察连接状态指示器(左下角圆点)
5. 发送消息测试流式响应
### 3. 浏览器控制台测试
```javascript
const ws = new WebSocket('ws://localhost:8000/api/v1/pipelines/<uuid>/ws/connect?session_type=person');
ws.onopen = () => {
console.log('已连接');
ws.send(JSON.stringify({
type: 'message',
message: [{type: 'Plain', text: '你好'}]
}));
};
ws.onmessage = (event) => {
console.log('收到:', JSON.parse(event.data));
};
```
## 常见问题
### Q: 为什么完全删除旧代码而不保留兼容性?
A: 根据需求,不需要考虑任何对老版本的兼容性,彻底迁移可以避免代码冗余和维护负担。
### Q: 流式输出如何工作?
A:
1. 后端通过`reply_message_chunk`发送消息块
2. 消息块放入队列
3. WebSocket连接从队列取出并发送
4. 前端实时更新UI
5. `is_final=true`表示最后一块
### Q: 如何确保连接不断开?
A:
1. 客户端每30秒发送心跳ping
2. 服务器响应pong
3. 连接断开时自动重连最多5次
### Q: 如何实现后端主动推送?
A:
1. 调用 `/api/v1/pipelines/<uuid>/ws/broadcast` API
2. 消息会被推送到该流水线的所有连接
3. 前端通过`onBroadcast`回调接收
## 总结
**完成的工作**
- 完全移除旧的WebChat/SSE系统
- 实现完整的WebSocket双向通信系统
- 支持流式输出
- 支持多连接并发
- 实现自动重连和心跳机制
- 提供完整的测试工具和文档
**核心特性**
- 双向实时通信
- 流式消息支持
- 多连接管理
- 自动重连
- 心跳保活
- 连接状态可视化
- 广播消息
**技术亮点**
- 异步架构asyncio
- 线程安全的连接管理
- 独立的消息队列
- 完整的错误处理
- 模块化设计
🎉 系统已完全迁移到WebSocket无任何旧代码遗留

259
docs/SEEKDB_INTEGRATION.md Normal file
View File

@@ -0,0 +1,259 @@
# SeekDB Vector Database Integration
This document describes how to use OceanBase SeekDB as the vector database backend for LangBot's knowledge base feature.
## What is SeekDB?
**OceanBase SeekDB** is an AI-native search database that unifies relational, vector, text, JSON and GIS in a single engine, enabling hybrid search and in-database AI workflows. It's developed by OceanBase and released under Apache 2.0 license.
### Key Features
- **Hybrid Search**: Combine vector search, full-text search and relational query in a single statement
- **Multi-Model Support**: Support relational, vector, text, JSON and GIS in a single engine
- **Lightweight**: Requires as little as 1 CPU core and 2 GB of memory
- **Multiple Deployment Modes**: Supports both embedded mode and client/server mode
- **MySQL Compatible**: Powered by OceanBase engine with full ACID compliance and MySQL compatibility
## Installation
SeekDB support is automatically included when you install LangBot. The required dependency `pyseekdb` is listed in `pyproject.toml`.
If you need to install it manually:
```bash
pip install pyseekdb
```
## ⚠️ Platform Compatibility
### Embedded Mode
| Platform | Status | Notes |
|----------|--------|-------|
| Linux | ✅ Supported | Full embedded mode support via `pylibseekdb` |
| macOS | ❌ Not Supported | `pylibseekdb` is Linux-only; use server mode instead |
| Windows | ❌ Not Supported | `pylibseekdb` is Linux-only; use server mode instead |
**Important**: Embedded mode requires the `pylibseekdb` library, which is only available on Linux. If you're on macOS or Windows, you must use server mode.
### Server Mode (Docker)
| Platform | Status | Notes |
|----------|--------|-------|
| Linux | ✅ Supported | Full Docker support |
| macOS | ⚠️ Known Issue | Docker container initialization failure - [See Issue #36](https://github.com/oceanbase/seekdb/issues/36) |
| Windows | ⚠️ Untested | Should work but not yet tested |
**macOS Users**: Currently, SeekDB Docker containers have an initialization issue on macOS ([oceanbase/seekdb#36](https://github.com/oceanbase/seekdb/issues/36)). Until this is resolved, we recommend:
- Using ChromaDB or Qdrant as alternatives
- Connecting to a remote SeekDB server on Linux if available
### Server Mode (Remote Connection)
| Platform | Status | Notes |
|----------|--------|-------|
| All Platforms | ✅ Supported | Connect to SeekDB running on a remote Linux server |
**Recommendation for macOS/Windows users**: Deploy SeekDB on a Linux server and connect via server mode configuration.
## Configuration
### Embedded Mode (Recommended for Development)
Embedded mode runs SeekDB directly within the LangBot process, storing data locally. This is the simplest setup and requires no external services.
Edit your `config.yaml`:
```yaml
vdb:
use: seekdb
seekdb:
mode: embedded
path: './data/seekdb' # Path to store SeekDB data
database: 'langbot' # Database name
```
### Server Mode (For Production)
Server mode connects to a remote SeekDB server or OceanBase server. This is recommended for production deployments.
#### SeekDB Server
```yaml
vdb:
use: seekdb
seekdb:
mode: server
host: 'localhost'
port: 2881
database: 'langbot'
user: 'root'
password: '' # Can also use SEEKDB_PASSWORD env var
```
#### OceanBase Server
If you're using OceanBase with seekdb capabilities:
```yaml
vdb:
use: seekdb
seekdb:
mode: server
host: 'localhost'
port: 2881
tenant: 'sys' # OceanBase tenant name
database: 'langbot'
user: 'root'
password: ''
```
## Configuration Parameters
| Parameter | Required | Default | Description |
|-----------|----------|--------------|-------------|
| `mode` | No | `embedded` | Deployment mode: `embedded` or `server` |
| `path` | No | `./data/seekdb` | Data directory for embedded mode |
| `database` | No | `langbot` | Database name |
| `host` | No | `localhost` | Server host (server mode only) |
| `port` | No | `2881` | Server port (server mode only) |
| `user` | No | `root` | Username (server mode only) |
| `password` | No | `''` | Password (server mode only) |
| `tenant` | No | None | OceanBase tenant (optional, server mode only) |
## Usage
Once configured, SeekDB will be used automatically for all knowledge base operations in LangBot:
1. **Creating Knowledge Bases**: Vectors will be stored in SeekDB collections
2. **Adding Documents**: Document embeddings will be indexed in SeekDB
3. **Searching**: Vector similarity search will use SeekDB's efficient indexing
4. **Deleting**: Document removal will delete vectors from SeekDB
No code changes are required - just update your configuration!
## Architecture Details
### Implementation
The SeekDB adapter is implemented in `src/langbot/pkg/vector/vdbs/seekdb.py` and follows the same `VectorDatabase` interface as Chroma and Qdrant adapters.
Key methods:
- `add_embeddings()`: Add vectors with metadata to a collection
- `search()`: Perform vector similarity search
- `delete_by_file_id()`: Delete vectors by file ID metadata
- `get_or_create_collection()`: Manage collections
- `delete_collection()`: Remove entire collections
### Vector Storage
- Collections are created with HNSW (Hierarchical Navigable Small World) index
- Default distance metric: Cosine similarity
- Default vector dimension: 384 (adjusts automatically based on embeddings)
- Metadata is stored alongside vectors for filtering
## Advantages Over Other Vector Databases
### vs. ChromaDB
- ✅ Better MySQL compatibility
- ✅ Hybrid search capabilities (vector + full-text + SQL)
- ✅ Production-grade distributed mode support
- ✅ Lightweight embedded mode
### vs. Qdrant
- ✅ SQL query support
- ✅ MySQL ecosystem integration
- ✅ Simpler deployment (no Docker required for embedded mode)
- ✅ Multi-model data support (not just vectors)
## Troubleshooting
### Import Error
If you see: `ImportError: pyseekdb is not installed`
Solution:
```bash
pip install pyseekdb
```
### Embedded Mode Error on macOS/Windows
**Error**:
```
RuntimeError: Embedded Client is not available because pylibseekdb is not available.
Please install pylibseekdb (Linux only) or use RemoteServerClient (host/port) instead.
```
**Cause**: `pylibseekdb` is only available on Linux platforms.
**Solution**: Use server mode instead:
1. Deploy SeekDB on a Linux server or VM
2. Configure LangBot to use server mode:
```yaml
vdb:
use: seekdb
seekdb:
mode: server
host: 'your-seekdb-server-ip'
port: 2881
database: 'langbot'
user: 'root'
password: ''
```
**Alternative**: Use ChromaDB or Qdrant, which work on all platforms:
```yaml
vdb:
use: chroma # or qdrant
```
### Docker Container Fails on macOS
**Symptoms**:
```bash
docker run -d -p 2881:2881 oceanbase/seekdb:latest
# Container exits immediately with code 30
```
**Error in logs**:
```
[ERROR] Code: Agent.SeekDB.Not.Exists
Message: initialize failed: init agent failed: SeekDB not exists in current directory.
```
**Cause**: This is a known issue with SeekDB Docker containers on macOS. See [oceanbase/seekdb#36](https://github.com/oceanbase/seekdb/issues/36).
**Status**: Under investigation by OceanBase team.
**Workaround Options**:
1. **Use alternatives**: ChromaDB or Qdrant work perfectly on macOS
2. **Remote server**: Deploy SeekDB on a Linux server and connect remotely
3. **Wait for fix**: Monitor the GitHub issue for updates
### Connection Error (Server Mode)
If SeekDB server is not reachable, check:
1. Server is running: `ps aux | grep observer`
2. Port is accessible: `nc -zv localhost 2881`
3. Credentials are correct in config
4. Firewall allows connections on port 2881
### Performance Issues
For large datasets:
- Use server mode instead of embedded mode
- Ensure adequate memory allocation
- Consider using OceanBase distributed mode for very large scale
- Adjust HNSW index parameters if needed
## Resources
- SeekDB GitHub: https://github.com/oceanbase/seekdb
- pyseekdb SDK: https://github.com/oceanbase/pyseekdb
- OceanBase Documentation: https://oceanbase.ai
- LangBot Documentation: https://docs.langbot.app
## License
SeekDB is licensed under Apache License 2.0.

394
docs/WEBSOCKET_README.md Normal file
View File

@@ -0,0 +1,394 @@
# LangBot WebSocket 双向通信系统
## 概述
这是一个内置在 LangBot 中的完整 IM (即时通讯) 系统,支持:
- ✅ WebSocket 双向实时通信
- ✅ 多个客户端并发连接
- ✅ 前端到后端的消息发送
- ✅ 后端到前端的主动推送
- ✅ 流式响应支持
- ✅ 连接管理和会话隔离
- ✅ 心跳机制
- ✅ 广播消息功能
## 架构设计
### 核心组件
1. **WebSocketConnectionManager** (`websocket_manager.py`)
- 管理所有活跃的 WebSocket 连接
- 支持按流水线、会话类型查询连接
- 提供广播和单播功能
- 线程安全的并发访问控制
2. **WebSocketAdapter** (`websocket_adapter.py`)
- 实现平台适配器接口
- 处理消息的接收和发送
- 支持流式输出
- 管理消息历史
3. **WebSocketChatRouterGroup** (`websocket_chat.py`)
- WebSocket 路由控制器
- 处理连接建立、消息收发
- 实现心跳机制
- 提供 REST API 接口
## API 接口
### WebSocket 连接
#### 建立连接
```
ws://localhost:8000/api/v1/pipelines/<pipeline_uuid>/ws/connect?session_type=<person|group>
```
**参数:**
- `pipeline_uuid`: 流水线 UUID (必需)
- `session_type`: 会话类型,可选 `person``group` (默认: `person`)
**连接成功响应:**
```json
{
"type": "connected",
"connection_id": "550e8400-e29b-41d4-a716-446655440000",
"pipeline_uuid": "your-pipeline-uuid",
"session_type": "person",
"timestamp": "2025-01-28T12:00:00"
}
```
### 消息格式
#### 客户端发送消息
**发送聊天消息:**
```json
{
"type": "message",
"message": [
{
"type": "Plain",
"text": "你好,这是一条测试消息"
}
]
}
```
**发送心跳:**
```json
{
"type": "ping"
}
```
**主动断开连接:**
```json
{
"type": "disconnect"
}
```
#### 服务器响应消息
**聊天响应 (流式):**
```json
{
"type": "response",
"data": {
"id": 1,
"role": "assistant",
"content": "这是机器人的回复",
"message_chain": [...],
"timestamp": "2025-01-28T12:00:00",
"is_final": false,
"connection_id": "..."
}
}
```
**心跳响应:**
```json
{
"type": "pong",
"timestamp": "2025-01-28T12:00:00"
}
```
**广播消息:**
```json
{
"type": "broadcast",
"message": "这是一条广播消息",
"timestamp": "2025-01-28T12:00:00"
}
```
**错误消息:**
```json
{
"type": "error",
"message": "错误描述"
}
```
### REST API 接口
#### 1. 获取消息历史
```http
GET /api/v1/pipelines/<pipeline_uuid>/ws/messages/<session_type>
```
**响应:**
```json
{
"code": 0,
"msg": "ok",
"data": {
"messages": [...]
}
}
```
#### 2. 重置会话
```http
POST /api/v1/pipelines/<pipeline_uuid>/ws/reset/<session_type>
```
**响应:**
```json
{
"code": 0,
"msg": "ok",
"data": {
"message": "Session reset successfully"
}
}
```
#### 3. 获取连接统计
```http
GET /api/v1/pipelines/<pipeline_uuid>/ws/connections
```
**响应:**
```json
{
"code": 0,
"msg": "ok",
"data": {
"stats": {
"total_connections": 5,
"pipelines": 2,
"connections_by_pipeline": {
"pipeline-1": 3,
"pipeline-2": 2
},
"connections_by_session_type": {
"person": 4,
"group": 1
}
},
"connections": [
{
"connection_id": "...",
"session_type": "person",
"created_at": "2025-01-28T12:00:00",
"last_active": "2025-01-28T12:05:00",
"is_active": true
}
]
}
}
```
#### 4. 广播消息 (后端主动推送)
```http
POST /api/v1/pipelines/<pipeline_uuid>/ws/broadcast
Content-Type: application/json
{
"message": "广"
}
```
**响应:**
```json
{
"code": 0,
"msg": "ok",
"data": {
"message": "Broadcast sent successfully"
}
}
```
## 使用示例
### Python 客户端示例
使用提供的测试客户端:
```bash
# 安装依赖
pip install websockets
# 单个连接测试
python test_websocket_client.py <pipeline_uuid>
# 指定会话类型
python test_websocket_client.py <pipeline_uuid> --session-type group
# 多连接并发测试
python test_websocket_client.py <pipeline_uuid> --multi 5
```
### JavaScript 客户端示例
```javascript
// 建立 WebSocket 连接
const ws = new WebSocket('ws://localhost:8000/api/v1/pipelines/your-pipeline-uuid/ws/connect?session_type=person');
// 连接建立
ws.onopen = () => {
console.log('WebSocket 连接已建立');
// 发送消息
ws.send(JSON.stringify({
type: 'message',
message: [
{
type: 'Plain',
text: '你好'
}
]
}));
};
// 接收消息
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'connected') {
console.log('连接成功:', data.connection_id);
} else if (data.type === 'response') {
console.log('机器人回复:', data.data.content);
if (data.data.is_final) {
console.log('响应完成');
}
} else if (data.type === 'broadcast') {
console.log('收到广播:', data.message);
}
};
// 连接关闭
ws.onclose = () => {
console.log('WebSocket 连接已关闭');
};
// 错误处理
ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
// 发送心跳
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000); // 每 30 秒发送一次心跳
```
## 特性说明
### 1. 多连接支持
系统支持同时建立多个 WebSocket 连接,每个连接都有唯一的 `connection_id`。连接按照流水线和会话类型进行分组管理。
### 2. 双向通信
- **前端 → 后端**: 客户端可以主动发送消息给服务器
- **后端 → 前端**: 服务器可以通过广播 API 主动推送消息给客户端
### 3. 流式响应
支持流式输出,机器人的响应会分块发送,客户端可以实时显示部分响应内容。
### 4. 会话隔离
支持 `person``group` 两种会话类型,不同类型的会话消息历史互不影响。
### 5. 连接管理
- 自动追踪连接状态
- 记录最后活跃时间
- 支持连接统计查询
- 连接断开时自动清理资源
### 6. 心跳机制
客户端可以定期发送 `ping` 消息,服务器会响应 `pong`,用于保持连接活跃和检测连接状态。
## 架构优势
1. **高并发**: 使用 asyncio 异步架构,支持大量并发连接
2. **可扩展**: 模块化设计,易于扩展新功能
3. **线程安全**: 连接管理器使用锁机制保证并发安全
4. **消息队列**: 每个连接独立的发送队列,避免消息混乱
5. **灵活路由**: 支持按流水线、会话类型灵活路由消息
## 注意事项
1. **认证**: 当前 WebSocket 连接不需要认证,生产环境建议添加认证机制
2. **心跳**: 建议客户端实现心跳机制,避免连接超时
3. **重连**: 客户端应实现断线重连逻辑
4. **消息大小**: 注意控制单条消息大小,避免内存溢出
5. **连接数限制**: 生产环境建议设置最大连接数限制
## 故障排查
### 连接失败
1. 检查流水线 UUID 是否正确
2. 检查服务器是否正常运行
3. 检查防火墙设置
### 消息发送失败
1. 检查消息格式是否正确
2. 检查连接是否仍然活跃
3. 查看服务器日志获取详细错误信息
### 性能问题
1. 检查并发连接数是否过多
2. 检查消息处理速度
3. 考虑使用连接池或负载均衡
## 开发调试
启用详细日志:
```python
import logging
logging.getLogger('langbot.pkg.platform.sources.websocket_adapter').setLevel(logging.DEBUG)
logging.getLogger('langbot.pkg.platform.sources.websocket_manager').setLevel(logging.DEBUG)
logging.getLogger('langbot.pkg.api.http.controller.groups.pipelines.websocket_chat').setLevel(logging.DEBUG)
```
## 后续改进建议
1. 添加用户认证和授权机制
2. 实现消息持久化
3. 添加消息加密
4. 实现更丰富的消息类型 (图片、文件等)
5. 添加消息已读/未读状态
6. 实现群组聊天功能
7. 添加在线状态显示
8. 实现消息撤回功能

View File

@@ -9,7 +9,7 @@
"url": "https://langbot.app"
},
"license": {
"name": "AGPL-3.0",
"name": "Apache-2.0",
"url": "https://github.com/langbot-app/LangBot/blob/master/LICENSE"
}
},

View File

@@ -1,10 +1,10 @@
[project]
name = "langbot"
version = "4.5.3"
description = "Easy-to-use global IM bot platform designed for LLM era"
version = "4.8.7"
description = "Production-grade platform for building agentic IM bots"
readme = "README.md"
license-files = ["LICENSE"]
requires-python = ">=3.10.1,<4.0"
requires-python = ">=3.11,<4.0"
dependencies = [
"aiocqhttp>=1.4.4",
"aiofiles>=24.1.0",
@@ -17,13 +17,13 @@ dependencies = [
"certifi>=2025.4.26",
"colorlog~=6.6.0",
"cryptography>=44.0.3",
"dashscope>=1.23.2",
"dashscope>=1.25.10",
"dingtalk-stream>=0.24.0",
"discord-py>=2.5.2",
"pynacl>=1.5.0", # Required for Discord voice support
"gewechat-client>=0.1.5",
"lark-oapi>=1.4.15",
"mcp>=1.8.1",
"mcp>=1.25.0",
"nakuru-project-idk>=0.0.2.1",
"ollama>=0.4.8",
"openai>1.0.0",
@@ -63,11 +63,15 @@ dependencies = [
"langchain-text-splitters>=0.0.1",
"chromadb>=0.4.24",
"qdrant-client (>=1.15.1,<2.0.0)",
"langbot-plugin==0.1.11",
"pyseekdb==1.0.0b7",
"langbot-plugin==0.3.0rc1",
"asyncpg>=0.30.0",
"line-bot-sdk>=3.19.0",
"tboxsdk>=0.0.10",
"boto3>=1.35.0",
"pymilvus>=2.6.4",
"pgvector>=0.4.1",
"botocore>=1.42.39",
]
keywords = [
"bot",

View File

@@ -26,7 +26,7 @@ markers =
# Coverage options (when using pytest-cov)
[coverage:run]
source = langbot.pkg
source = langbot
omit =
*/tests/*
*/test_*.py

BIN
res/logo-blue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -22,7 +22,7 @@ echo "Running all unit tests..."
# Run tests with coverage
pytest tests/unit_tests/ -v --tb=short \
--cov=pkg \
--cov=langbot \
--cov-report=xml \
"$@"

View File

@@ -1,3 +1,3 @@
"""LangBot - Easy-to-use global IM bot platform designed for LLM era"""
"""LangBot - Production-grade platform for building agentic IM bots"""
__version__ = '4.5.3'
__version__ = '4.8.7'

View File

@@ -32,6 +32,7 @@ class AsyncDifyServiceClient:
conversation_id: str = '',
files: list[dict[str, typing.Any]] = [],
timeout: float = 30.0,
model_config: dict[str, typing.Any] | None = None,
) -> typing.AsyncGenerator[dict[str, typing.Any], None]:
"""发送消息"""
if response_mode != 'streaming':
@@ -42,6 +43,16 @@ class AsyncDifyServiceClient:
trust_env=True,
timeout=timeout,
) as client:
payload = {
'inputs': inputs,
'query': query,
'user': user,
'response_mode': response_mode,
'conversation_id': conversation_id,
'files': files,
'model_config': model_config or {},
}
async with client.stream(
'POST',
'/chat-messages',
@@ -49,14 +60,7 @@ class AsyncDifyServiceClient:
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json',
},
json={
'inputs': inputs,
'query': query,
'user': user,
'response_mode': response_mode,
'conversation_id': conversation_id,
'files': files,
},
json=payload,
) as r:
async for chunk in r.aiter_lines():
if r.status_code != 200:

View File

@@ -1,8 +1,11 @@
import asyncio
import base64
import json
import time
import urllib.parse
from typing import Callable
import dingtalk_stream # type: ignore
import websockets
from .EchoHandler import EchoTextHandler
from .dingtalkevent import DingTalkEvent
import httpx
@@ -36,6 +39,7 @@ class DingTalkClient:
self.access_token_expiry_time = ''
self.markdown_card = markdown_card
self.logger = logger
self._stopped = False # Flag to control the event loop
async def get_access_token(self):
url = 'https://api.dingtalk.com/v1.0/oauth2/accessToken'
@@ -170,6 +174,9 @@ class DingTalkClient:
"""
处理消息事件。
"""
# Skip message handling if stopped
if self._stopped:
return
msg_type = event.conversation
if msg_type in self._message_handlers:
for handler in self._message_handlers[msg_type]:
@@ -340,10 +347,15 @@ class DingTalkClient:
raise Exception(f'failed to send proactive massage to group: {traceback.format_exc()}')
async def create_and_card(
self, temp_card_id: str, incoming_message: dingtalk_stream.ChatbotMessage, quote_origin: bool = False
self,
temp_card_id: str,
incoming_message: dingtalk_stream.ChatbotMessage,
quote_origin: bool = False,
card_auto_layout: bool = False,
):
content_key = 'content'
card_data = {content_key: ''}
card_data = {}
card_data['config'] = json.dumps({'autoLayout': card_auto_layout})
card_data['content'] = ''
card_instance = dingtalk_stream.AICardReplier(self.client, incoming_message)
# print(card_instance)
@@ -378,4 +390,70 @@ class DingTalkClient:
async def start(self):
"""启动 WebSocket 连接,监听消息"""
await self.client.start()
self._stopped = False
self.client.pre_start()
while not self._stopped:
try:
connection = self.client.open_connection()
if not connection:
if self.logger:
await self.logger.error('DingTalk: open connection failed')
await asyncio.sleep(10)
continue
uri = '%s?ticket=%s' % (connection['endpoint'], urllib.parse.quote_plus(connection['ticket']))
async with websockets.connect(uri) as websocket:
self.client.websocket = websocket
keepalive_task = asyncio.create_task(self._keepalive(websocket))
try:
async for raw_message in websocket:
if self._stopped:
break
json_message = json.loads(raw_message)
asyncio.create_task(self.client.background_task(json_message))
finally:
keepalive_task.cancel()
try:
await keepalive_task
except asyncio.CancelledError:
pass
except asyncio.CancelledError:
# Properly exit when task is cancelled
break
except websockets.exceptions.ConnectionClosedError as e:
if self._stopped:
break
if self.logger:
await self.logger.error(f'DingTalk: connection closed, reconnecting... error={e}')
await asyncio.sleep(5)
continue
except Exception as e:
if self._stopped:
break
if self.logger:
await self.logger.error(f'DingTalk: unknown exception, reconnecting... error={e}')
await asyncio.sleep(3)
continue
async def _keepalive(self, ws, ping_interval=60):
"""Keep WebSocket connection alive"""
while not self._stopped:
await asyncio.sleep(ping_interval)
try:
await ws.ping()
except websockets.exceptions.ConnectionClosed:
break
async def stop(self):
"""停止 WebSocket 连接"""
self._stopped = True
# Close WebSocket connection if exists
if self.client.websocket:
try:
await self.client.websocket.close()
except Exception:
pass
# Clear message handlers to prevent stale callbacks
self._message_handlers = {'example': []}

View File

@@ -23,20 +23,34 @@ xml_template = """
class OAClient:
def __init__(self, token: str, EncodingAESKey: str, AppID: str, Appsecret: str, logger: None):
def __init__(
self,
token: str,
EncodingAESKey: str,
AppID: str,
Appsecret: str,
logger: None,
unified_mode: bool = False,
api_base_url: str = 'https://api.weixin.qq.com',
):
self.token = token
self.aes = EncodingAESKey
self.appid = AppID
self.appsecret = Appsecret
self.base_url = 'https://api.weixin.qq.com'
self.base_url = api_base_url
self.access_token = ''
self.unified_mode = unified_mode
self.app = Quart(__name__)
self.app.add_url_rule(
'/callback/command',
'handle_callback',
self.handle_callback_request,
methods=['GET', 'POST'],
)
# 只有在非统一模式下才注册独立路由
if not self.unified_mode:
self.app.add_url_rule(
'/callback/command',
'handle_callback',
self.handle_callback_request,
methods=['GET', 'POST'],
)
self._message_handlers = {
'example': [],
}
@@ -46,19 +60,39 @@ class OAClient:
self.logger = logger
async def handle_callback_request(self):
"""处理回调请求(独立端口模式,使用全局 request"""
return await self._handle_callback_internal(request)
async def handle_unified_webhook(self, req):
"""处理回调请求(统一 webhook 模式,显式传递 request
Args:
req: Quart Request 对象
Returns:
响应数据
"""
return await self._handle_callback_internal(req)
async def _handle_callback_internal(self, req):
"""处理回调请求的内部实现,包括 GET 验证和 POST 消息接收。
Args:
req: Quart Request 对象
"""
try:
# 每隔100毫秒查询是否生成ai回答
start_time = time.time()
signature = request.args.get('signature', '')
timestamp = request.args.get('timestamp', '')
nonce = request.args.get('nonce', '')
echostr = request.args.get('echostr', '')
msg_signature = request.args.get('msg_signature', '')
signature = req.args.get('signature', '')
timestamp = req.args.get('timestamp', '')
nonce = req.args.get('nonce', '')
echostr = req.args.get('echostr', '')
msg_signature = req.args.get('msg_signature', '')
if msg_signature is None:
await self.logger.error('msg_signature不在请求体中')
raise Exception('msg_signature不在请求体中')
if request.method == 'GET':
if req.method == 'GET':
# 校验签名
check_str = ''.join(sorted([self.token, timestamp, nonce]))
check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest()
@@ -68,8 +102,8 @@ class OAClient:
else:
await self.logger.error('拒绝请求')
raise Exception('拒绝请求')
elif request.method == 'POST':
encryt_msg = await request.data
elif req.method == 'POST':
encryt_msg = await req.data
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid)
ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce)
xml_msg = xml_msg.decode('utf-8')
@@ -182,20 +216,27 @@ class OAClientForLongerResponse:
Appsecret: str,
LoadingMessage: str,
logger: None,
unified_mode: bool = False,
api_base_url: str = 'https://api.weixin.qq.com',
):
self.token = token
self.aes = EncodingAESKey
self.appid = AppID
self.appsecret = Appsecret
self.base_url = 'https://api.weixin.qq.com'
self.base_url = api_base_url
self.access_token = ''
self.unified_mode = unified_mode
self.app = Quart(__name__)
self.app.add_url_rule(
'/callback/command',
'handle_callback',
self.handle_callback_request,
methods=['GET', 'POST'],
)
# 只有在非统一模式下才注册独立路由
if not self.unified_mode:
self.app.add_url_rule(
'/callback/command',
'handle_callback',
self.handle_callback_request,
methods=['GET', 'POST'],
)
self._message_handlers = {
'example': [],
}
@@ -206,24 +247,44 @@ class OAClientForLongerResponse:
self.logger = logger
async def handle_callback_request(self):
"""处理回调请求(独立端口模式,使用全局 request"""
return await self._handle_callback_internal(request)
async def handle_unified_webhook(self, req):
"""处理回调请求(统一 webhook 模式,显式传递 request
Args:
req: Quart Request 对象
Returns:
响应数据
"""
return await self._handle_callback_internal(req)
async def _handle_callback_internal(self, req):
"""处理回调请求的内部实现,包括 GET 验证和 POST 消息接收。
Args:
req: Quart Request 对象
"""
try:
signature = request.args.get('signature', '')
timestamp = request.args.get('timestamp', '')
nonce = request.args.get('nonce', '')
echostr = request.args.get('echostr', '')
msg_signature = request.args.get('msg_signature', '')
signature = req.args.get('signature', '')
timestamp = req.args.get('timestamp', '')
nonce = req.args.get('nonce', '')
echostr = req.args.get('echostr', '')
msg_signature = req.args.get('msg_signature', '')
if msg_signature is None:
await self.logger.error('msg_signature不在请求体中')
raise Exception('msg_signature不在请求体中')
if request.method == 'GET':
if req.method == 'GET':
check_str = ''.join(sorted([self.token, timestamp, nonce]))
check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest()
return echostr if check_signature == signature else '拒绝请求'
elif request.method == 'POST':
encryt_msg = await request.data
elif req.method == 'POST':
encryt_msg = await req.data
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid)
ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce)
xml_msg = xml_msg.decode('utf-8')

View File

@@ -10,38 +10,20 @@ import traceback
from cryptography.hazmat.primitives.asymmetric import ed25519
def handle_validation(body: dict, bot_secret: str):
# bot正确的secert是32位的此处仅为了适配演示demo
while len(bot_secret) < 32:
bot_secret = bot_secret * 2
bot_secret = bot_secret[:32]
# 实际使用场景中以上三行内容可清除
seed_bytes = bot_secret.encode()
signing_key = ed25519.Ed25519PrivateKey.from_private_bytes(seed_bytes)
msg = body['d']['event_ts'] + body['d']['plain_token']
msg_bytes = msg.encode()
signature = signing_key.sign(msg_bytes)
signature_hex = signature.hex()
response = {'plain_token': body['d']['plain_token'], 'signature': signature_hex}
return response
class QQOfficialClient:
def __init__(self, secret: str, token: str, app_id: str, logger: None):
def __init__(self, secret: str, token: str, app_id: str, logger: None, unified_mode: bool = False):
self.unified_mode = unified_mode
self.app = Quart(__name__)
self.app.add_url_rule(
'/callback/command',
'handle_callback',
self.handle_callback_request,
methods=['GET', 'POST'],
)
# 只有在非统一模式下才注册独立路由
if not self.unified_mode:
self.app.add_url_rule(
'/callback/command',
'handle_callback',
self.handle_callback_request,
methods=['GET', 'POST'],
)
self.secret = secret
self.token = token
self.app_id = app_id
@@ -82,18 +64,43 @@ class QQOfficialClient:
raise Exception(f'获取access_token失败: {e}')
async def handle_callback_request(self):
"""处理回调请求"""
"""处理回调请求(独立端口模式,使用全局 request"""
return await self._handle_callback_internal(request)
async def handle_unified_webhook(self, req):
"""处理回调请求(统一 webhook 模式,显式传递 request
Args:
req: Quart Request 对象
Returns:
响应数据
"""
return await self._handle_callback_internal(req)
async def _handle_callback_internal(self, req):
"""处理回调请求的内部实现。
Args:
req: Quart Request 对象
"""
try:
# 读取请求数据
body = await request.get_data()
body = await req.get_data()
print(f'[QQ Official] Received request, body length: {len(body)}')
if not body or len(body) == 0:
print('[QQ Official] Received empty body, might be health check or GET request')
return {'code': 0, 'message': 'ok'}, 200
payload = json.loads(body)
# 验证是否为回调验证请求
if payload.get('op') == 13:
# 生成签名
response = handle_validation(payload, self.secret)
return response
validation_data = payload.get('d')
if not validation_data:
return {'error': "missing 'd' field"}, 400
response = await self.verify(validation_data)
return response, 200
if payload.get('op') == 0:
message_data = await self.get_message(payload)
@@ -104,6 +111,7 @@ class QQOfficialClient:
return {'code': 0, 'message': 'success'}
except Exception as e:
print(f'[QQ Official] ERROR: {traceback.format_exc()}')
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
return {'error': str(e)}, 400
@@ -261,3 +269,26 @@ class QQOfficialClient:
if self.access_token_expiry_time is None:
return True
return time.time() > self.access_token_expiry_time
async def repeat_seed(self, bot_secret: str, target_size: int = 32) -> bytes:
seed = bot_secret
while len(seed) < target_size:
seed *= 2
return seed[:target_size].encode('utf-8')
async def verify(self, validation_payload: dict):
seed = await self.repeat_seed(self.secret)
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(seed)
event_ts = validation_payload.get('event_ts', '')
plain_token = validation_payload.get('plain_token', '')
msg = event_ts + plain_token
# sign
signature = private_key.sign(msg.encode()).hex()
response = {
'plain_token': plain_token,
'signature': signature,
}
return response

View File

@@ -8,14 +8,19 @@ import langbot_plugin.api.entities.builtin.platform.events as platform_events
class SlackClient:
def __init__(self, bot_token: str, signing_secret: str, logger: None):
def __init__(self, bot_token: str, signing_secret: str, logger: None, unified_mode: bool = False):
self.bot_token = bot_token
self.signing_secret = signing_secret
self.unified_mode = unified_mode
self.app = Quart(__name__)
self.client = AsyncWebClient(self.bot_token)
self.app.add_url_rule(
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
)
# 只有在非统一模式下才注册独立路由
if not self.unified_mode:
self.app.add_url_rule(
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
)
self._message_handlers = {
'example': [],
}
@@ -23,8 +28,28 @@ class SlackClient:
self.logger = logger
async def handle_callback_request(self):
"""处理回调请求(独立端口模式,使用全局 request"""
return await self._handle_callback_internal(request)
async def handle_unified_webhook(self, req):
"""处理回调请求(统一 webhook 模式,显式传递 request
Args:
req: Quart Request 对象
Returns:
响应数据
"""
return await self._handle_callback_internal(req)
async def _handle_callback_internal(self, req):
"""处理回调请求的内部实现。
Args:
req: Quart Request 对象
"""
try:
body = await request.get_data()
body = await req.get_data()
data = json.loads(body)
if 'type' in data:
if data['type'] == 'url_verification':

View File

@@ -1,5 +1,5 @@
import requests
import aiohttp
from langbot.pkg.utils import httpclient
def post_json(base_url, token, data=None):
@@ -63,16 +63,16 @@ async def async_request(
"""
headers = {'Content-Type': 'application/json'}
url = f'{base_url}?key={token_key}'
async with aiohttp.ClientSession() as session:
async with session.request(
method=method, url=url, params=params, headers=headers, data=data, json=json
) as response:
response.raise_for_status() # 如果状态码不是200抛出异常
result = await response.json()
# print(result)
return result
# if result.get('Code') == 200:
#
# return await result
# else:
# raise RuntimeError("请求失败",response.text)
session = httpclient.get_session()
async with session.request(
method=method, url=url, params=params, headers=headers, data=data, json=json
) as response:
response.raise_for_status() # 如果状态码不是200抛出异常
result = await response.json()
# print(result)
return result
# if result.get('Code') == 200:
#
# return await result
# else:
# raise RuntimeError("请求失败",response.text)

View File

@@ -200,7 +200,7 @@ class StreamSessionManager:
class WecomBotClient:
def __init__(self, Token: str, EnCodingAESKey: str, Corpid: str, logger: EventLogger):
def __init__(self, Token: str, EnCodingAESKey: str, Corpid: str, logger: EventLogger, unified_mode: bool = False):
"""企业微信智能机器人客户端。
Args:
@@ -208,6 +208,7 @@ class WecomBotClient:
EnCodingAESKey: 企业微信消息加解密密钥。
Corpid: 企业 ID。
logger: 日志记录器。
unified_mode: 是否使用统一 webhook 模式(默认 False
Example:
>>> client = WecomBotClient(Token='token', EnCodingAESKey='aeskey', Corpid='corp', logger=logger)
@@ -217,10 +218,15 @@ class WecomBotClient:
self.EnCodingAESKey = EnCodingAESKey
self.Corpid = Corpid
self.ReceiveId = ''
self.unified_mode = unified_mode
self.app = Quart(__name__)
self.app.add_url_rule(
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['POST', 'GET']
)
# 只有在非统一模式下才注册独立路由
if not self.unified_mode:
self.app.add_url_rule(
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['POST', 'GET']
)
self._message_handlers = {
'example': [],
}
@@ -359,7 +365,7 @@ class WecomBotClient:
return await self._encrypt_and_reply(payload, nonce)
async def handle_callback_request(self):
"""企业微信回调入口。
"""企业微信回调入口(独立端口模式,使用全局 request
Returns:
Quart Response: 根据请求类型返回验证、首包或刷新结果。
@@ -367,15 +373,33 @@ class WecomBotClient:
Example:
作为 Quart 路由处理函数直接注册并使用。
"""
return await self._handle_callback_internal(request)
async def handle_unified_webhook(self, req):
"""处理回调请求(统一 webhook 模式,显式传递 request
Args:
req: Quart Request 对象
Returns:
响应数据
"""
return await self._handle_callback_internal(req)
async def _handle_callback_internal(self, req):
"""处理回调请求的内部实现,包括 GET 验证和 POST 消息接收。
Args:
req: Quart Request 对象
"""
try:
self.wxcpt = WXBizMsgCrypt(self.Token, self.EnCodingAESKey, '')
await self.logger.info(f'{request.method} {request.url} {str(request.args)}')
if request.method == 'GET':
return await self._handle_get_callback()
if req.method == 'GET':
return await self._handle_get_callback(req)
if request.method == 'POST':
return await self._handle_post_callback()
if req.method == 'POST':
return await self._handle_post_callback(req)
return Response('', status=405)
@@ -383,13 +407,13 @@ class WecomBotClient:
await self.logger.error(traceback.format_exc())
return Response('Internal Server Error', status=500)
async def _handle_get_callback(self) -> tuple[Response, int] | Response:
async def _handle_get_callback(self, req) -> tuple[Response, int] | Response:
"""处理企业微信的 GET 验证请求。"""
msg_signature = unquote(request.args.get('msg_signature', ''))
timestamp = unquote(request.args.get('timestamp', ''))
nonce = unquote(request.args.get('nonce', ''))
echostr = unquote(request.args.get('echostr', ''))
msg_signature = unquote(req.args.get('msg_signature', ''))
timestamp = unquote(req.args.get('timestamp', ''))
nonce = unquote(req.args.get('nonce', ''))
echostr = unquote(req.args.get('echostr', ''))
if not all([msg_signature, timestamp, nonce, echostr]):
await self.logger.error('请求参数缺失')
@@ -402,16 +426,16 @@ class WecomBotClient:
return Response(decrypted_str, mimetype='text/plain')
async def _handle_post_callback(self) -> tuple[Response, int] | Response:
async def _handle_post_callback(self, req) -> tuple[Response, int] | Response:
"""处理企业微信的 POST 回调请求。"""
self.stream_sessions.cleanup()
msg_signature = unquote(request.args.get('msg_signature', ''))
timestamp = unquote(request.args.get('timestamp', ''))
nonce = unquote(request.args.get('nonce', ''))
msg_signature = unquote(req.args.get('msg_signature', ''))
timestamp = unquote(req.args.get('timestamp', ''))
nonce = unquote(req.args.get('nonce', ''))
encrypted_json = await request.get_json()
encrypted_json = await req.get_json()
encrypted_msg = (encrypted_json or {}).get('encrypt', '')
if not encrypted_msg:
await self.logger.error("请求体中缺少 'encrypt' 字段")
@@ -433,32 +457,174 @@ class WecomBotClient:
async def get_message(self, msg_json):
message_data = {}
msg_type = msg_json.get('msgtype', '')
if msg_type:
message_data['msgtype'] = msg_type
if msg_json.get('chattype', '') == 'single':
message_data['type'] = 'single'
elif msg_json.get('chattype', '') == 'group':
message_data['type'] = 'group'
if msg_json.get('msgtype') == 'text':
max_inline_file_size = 5 * 1024 * 1024 # avoid decoding very large payloads by default
async def _safe_download(url: str):
if not url:
return None
return await self.download_url_to_base64(url, self.EnCodingAESKey)
if msg_type == 'text':
message_data['content'] = msg_json.get('text', {}).get('content')
elif msg_json.get('msgtype') == 'image':
elif msg_type == 'markdown':
message_data['content'] = msg_json.get('markdown', {}).get('content') or msg_json.get('text', {}).get(
'content', ''
)
elif msg_type == 'image':
picurl = msg_json.get('image', {}).get('url', '')
base64 = await self.download_url_to_base64(picurl, self.EnCodingAESKey)
message_data['picurl'] = base64
elif msg_json.get('msgtype') == 'mixed':
base64_data = await _safe_download(picurl)
if base64_data:
message_data['picurl'] = base64_data
message_data['images'] = [base64_data]
elif msg_type == 'voice':
voice_info = msg_json.get('voice', {}) or {}
download_url = voice_info.get('url')
message_data['voice'] = {
'url': download_url,
'md5sum': voice_info.get('md5sum') or voice_info.get('md5'),
'filesize': voice_info.get('filesize') or voice_info.get('size'),
'sdkfileid': voice_info.get('sdkfileid') or voice_info.get('fileid'),
}
# 企业微信智能转写文本(如果已有)直接复用,避免重复转写
if voice_info.get('content'):
message_data['content'] = voice_info.get('content')
if (message_data['voice'].get('filesize') or 0) <= max_inline_file_size:
voice_base64 = await _safe_download(download_url)
if voice_base64:
message_data['voice']['base64'] = voice_base64
elif msg_type == 'video':
video_info = msg_json.get('video', {}) or {}
download_url = video_info.get('url')
video_data = {
'url': download_url,
'filesize': video_info.get('filesize') or video_info.get('size'),
'sdkfileid': video_info.get('sdkfileid') or video_info.get('fileid'),
'md5sum': video_info.get('md5sum') or video_info.get('md5'),
'filename': video_info.get('filename') or video_info.get('name'),
}
if (video_data.get('filesize') or 0) <= max_inline_file_size:
video_base64 = await _safe_download(download_url)
if video_base64:
video_data['base64'] = video_base64
message_data['video'] = video_data
elif msg_type == 'file':
file_info = msg_json.get('file', {}) or {}
download_url = file_info.get('url') or file_info.get('fileurl')
file_data = {
'filename': file_info.get('filename') or file_info.get('name'),
'filesize': file_info.get('filesize') or file_info.get('size'),
'md5sum': file_info.get('md5sum') or file_info.get('md5'),
'sdkfileid': file_info.get('sdkfileid') or file_info.get('fileid'),
'download_url': download_url,
'extra': file_info,
}
if (file_data.get('filesize') or 0) <= max_inline_file_size:
file_base64 = await _safe_download(download_url)
if file_base64:
file_data['base64'] = file_base64
message_data['file'] = file_data
elif msg_type == 'link':
message_data['link'] = msg_json.get('link', {})
if not message_data.get('content'):
title = message_data['link'].get('title', '')
desc = message_data['link'].get('description') or message_data['link'].get('digest', '')
message_data['content'] = '\n'.join(filter(None, [title, desc]))
elif msg_type == 'mixed':
items = msg_json.get('mixed', {}).get('msg_item', [])
texts = []
picurl = None
images = []
files = []
voices = []
videos = []
links = []
for item in items:
if item.get('msgtype') == 'text':
item_type = item.get('msgtype')
if item_type == 'text':
texts.append(item.get('text', {}).get('content', ''))
elif item.get('msgtype') == 'image' and picurl is None:
picurl = item.get('image', {}).get('url')
elif item_type == 'image':
img_url = item.get('image', {}).get('url')
base64_data = await _safe_download(img_url)
if base64_data:
images.append(base64_data)
elif item_type == 'file':
file_info = item.get('file', {}) or {}
download_url = file_info.get('url') or file_info.get('fileurl')
file_data = {
'filename': file_info.get('filename') or file_info.get('name'),
'filesize': file_info.get('filesize') or file_info.get('size'),
'md5sum': file_info.get('md5sum') or file_info.get('md5'),
'sdkfileid': file_info.get('sdkfileid') or file_info.get('fileid'),
'download_url': download_url,
'extra': file_info,
}
if (file_data.get('filesize') or 0) <= max_inline_file_size:
file_base64 = await _safe_download(download_url)
if file_base64:
file_data['base64'] = file_base64
files.append(file_data)
elif item_type == 'voice':
voice_info = item.get('voice', {}) or {}
download_url = voice_info.get('url')
voice_data = {
'url': download_url,
'md5sum': voice_info.get('md5sum') or voice_info.get('md5'),
'filesize': voice_info.get('filesize') or voice_info.get('size'),
'sdkfileid': voice_info.get('sdkfileid') or voice_info.get('fileid'),
}
if voice_info.get('content'):
texts.append(voice_info.get('content'))
if (voice_data.get('filesize') or 0) <= max_inline_file_size:
voice_base64 = await _safe_download(download_url)
if voice_base64:
voice_data['base64'] = voice_base64
voices.append(voice_data)
elif item_type == 'video':
video_info = item.get('video', {}) or {}
download_url = video_info.get('url')
video_data = {
'url': download_url,
'filesize': video_info.get('filesize') or video_info.get('size'),
'sdkfileid': video_info.get('sdkfileid') or video_info.get('fileid'),
'md5sum': video_info.get('md5sum') or video_info.get('md5'),
'filename': video_info.get('filename') or video_info.get('name'),
}
if (video_data.get('filesize') or 0) <= max_inline_file_size:
video_base64 = await _safe_download(download_url)
if video_base64:
video_data['base64'] = video_base64
videos.append(video_data)
elif item_type == 'link':
links.append(item.get('link', {}))
if texts:
message_data['content'] = ''.join(texts) # 拼接所有 text
if picurl:
base64 = await self.download_url_to_base64(picurl, self.EnCodingAESKey)
message_data['picurl'] = base64 # 只保留第一个 image
message_data['content'] = ' '.join(texts) # 拼接所有 text
if images:
message_data['images'] = images
message_data['picurl'] = images[0] # 只保留第一个 image
if files:
message_data['files'] = files
message_data['file'] = files[0]
if voices:
message_data['voices'] = voices
message_data['voice'] = voices[0]
if videos:
message_data['videos'] = videos
message_data['video'] = videos[0]
if links:
message_data['link'] = links[0]
if items:
message_data['attachments'] = items
else:
message_data['raw_msg'] = msg_json
# Extract user information
from_info = msg_json.get('from', {})

View File

@@ -17,6 +17,13 @@ class WecomBotEvent(dict):
"""
return self.get('type', '')
@property
def msgtype(self) -> str:
"""
消息 msgtype
"""
return self.get('msgtype', '')
@property
def userid(self) -> str:
"""
@@ -57,6 +64,55 @@ class WecomBotEvent(dict):
"""
return self.get('picurl', '')
@property
def images(self):
"""
图片列表(兼容 mixed
"""
return self.get('images', [])
@property
def file(self):
"""
文件信息
"""
return self.get('file', {})
@property
def voice(self):
"""
语音信息
"""
return self.get('voice', {})
@property
def video(self):
"""
视频信息
"""
return self.get('video', {})
@property
def link(self):
"""
链接消息信息
"""
return self.get('link', {})
@property
def location(self):
"""
位置信息
"""
return self.get('location', {})
@property
def attachments(self):
"""
原始 mixed 中的附件项
"""
return self.get('attachments', [])
@property
def chatid(self) -> str:
"""

View File

@@ -21,23 +21,30 @@ class WecomClient:
EncodingAESKey: str,
contacts_secret: str,
logger: None,
unified_mode: bool = False,
api_base_url: str = 'https://qyapi.weixin.qq.com/cgi-bin',
):
self.corpid = corpid
self.secret = secret
self.access_token_for_contacts = ''
self.token = token
self.aes = EncodingAESKey
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
self.base_url = api_base_url
self.access_token = ''
self.secret_for_contacts = contacts_secret
self.logger = logger
self.unified_mode = unified_mode
self.app = Quart(__name__)
self.app.add_url_rule(
'/callback/command',
'handle_callback',
self.handle_callback_request,
methods=['GET', 'POST'],
)
# 只有在非统一模式下才注册独立路由
if not self.unified_mode:
self.app.add_url_rule(
'/callback/command',
'handle_callback',
self.handle_callback_request,
methods=['GET', 'POST'],
)
self._message_handlers = {
'example': [],
}
@@ -50,7 +57,7 @@ class WecomClient:
return bool(self.access_token_for_contacts and self.access_token_for_contacts.strip())
async def get_access_token(self, secret):
url = f'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={self.corpid}&corpsecret={secret}'
url = f'{self.base_url}/gettoken?corpid={self.corpid}&corpsecret={secret}'
async with httpx.AsyncClient() as client:
response = await client.get(url)
data = response.json()
@@ -109,14 +116,13 @@ class WecomClient:
async def send_image(self, user_id: str, agent_id: int, media_id: str):
if not await self.check_access_token():
self.access_token = await self.get_access_token(self.secret)
url = self.base_url + '/media/upload?access_token=' + self.access_token
url = self.base_url + '/message/send?access_token=' + self.access_token
async with httpx.AsyncClient() as client:
params = {
'touser': user_id,
'toparty': '',
'totag': '',
'agentid': agent_id,
'msgtype': 'image',
'agentid': agent_id,
'image': {
'media_id': media_id,
},
@@ -125,27 +131,73 @@ class WecomClient:
'enable_duplicate_check': 0,
'duplicate_check_interval': 1800,
}
try:
response = await client.post(url, json=params)
data = response.json()
except Exception as e:
await self.logger.error(f'发送图片失败:{data}')
raise Exception('Failed to send image: ' + str(e))
# 企业微信错误码40014和42001代表accesstoken问题
response = await client.post(url, json=params)
data = response.json()
if data['errcode'] == 40014 or data['errcode'] == 42001:
self.access_token = await self.get_access_token(self.secret)
return await self.send_image(user_id, agent_id, media_id)
if data['errcode'] != 0:
await self.logger.error(f'发送图片失败:{data}')
raise Exception('Failed to send image: ' + str(data))
async def send_voice(self, user_id: str, agent_id: int, media_id: str):
if not await self.check_access_token():
self.access_token = await self.get_access_token(self.secret)
url = self.base_url + '/message/send?access_token=' + self.access_token
async with httpx.AsyncClient() as client:
params = {
'touser': user_id,
'msgtype': 'voice',
'agentid': agent_id,
'voice': {
'media_id': media_id,
},
'safe': 0,
'enable_id_trans': 0,
'enable_duplicate_check': 0,
'duplicate_check_interval': 1800,
}
response = await client.post(url, json=params)
data = response.json()
if data['errcode'] == 40014 or data['errcode'] == 42001:
self.access_token = await self.get_access_token(self.secret)
return await self.send_voice(user_id, agent_id, media_id)
if data['errcode'] != 0:
await self.logger.error(f'发送语音失败:{data}')
raise Exception('Failed to send voice: ' + str(data))
async def send_file(self, user_id: str, agent_id: int, media_id: str):
if not await self.check_access_token():
self.access_token = await self.get_access_token(self.secret)
url = self.base_url + '/message/send?access_token=' + self.access_token
async with httpx.AsyncClient() as client:
params = {
'touser': user_id,
'msgtype': 'file',
'agentid': agent_id,
'file': {
'media_id': media_id,
},
'safe': 0,
'enable_id_trans': 0,
'enable_duplicate_check': 0,
'duplicate_check_interval': 1800,
}
response = await client.post(url, json=params)
data = response.json()
if data['errcode'] == 40014 or data['errcode'] == 42001:
self.access_token = await self.get_access_token(self.secret)
return await self.send_file(user_id, agent_id, media_id)
if data['errcode'] != 0:
await self.logger.error(f'发送文件失败:{data}')
raise Exception('Failed to send file: ' + str(data))
async def send_private_msg(self, user_id: str, agent_id: int, content: str):
if not await self.check_access_token():
self.access_token = await self.get_access_token(self.secret)
url = self.base_url + '/message/send?access_token=' + self.access_token
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient(timeout=None) as client:
params = {
'touser': user_id,
'msgtype': 'text',
@@ -168,25 +220,43 @@ class WecomClient:
raise Exception('Failed to send message: ' + str(data))
async def handle_callback_request(self):
"""处理回调请求(独立端口模式,使用全局 request"""
return await self._handle_callback_internal(request)
async def handle_unified_webhook(self, req):
"""处理回调请求(统一 webhook 模式,显式传递 request
Args:
req: Quart Request 对象
Returns:
响应数据
"""
处理回调请求,包括 GET 验证和 POST 消息接收。
return await self._handle_callback_internal(req)
async def _handle_callback_internal(self, req):
"""
处理回调请求的内部实现,包括 GET 验证和 POST 消息接收。
Args:
req: Quart Request 对象
"""
try:
msg_signature = request.args.get('msg_signature')
timestamp = request.args.get('timestamp')
nonce = request.args.get('nonce')
msg_signature = req.args.get('msg_signature')
timestamp = req.args.get('timestamp')
nonce = req.args.get('nonce')
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
if request.method == 'GET':
echostr = request.args.get('echostr')
if req.method == 'GET':
echostr = req.args.get('echostr')
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
if ret != 0:
await self.logger.error('验证失败')
raise Exception(f'验证失败,错误码: {ret}')
return reply_echo_str
elif request.method == 'POST':
encrypt_msg = await request.data
elif req.method == 'POST':
encrypt_msg = await req.data
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
if ret != 0:
await self.logger.error('消息解密失败')
@@ -270,7 +340,7 @@ class WecomClient:
return ext
return 'jpg' # 默认返回jpg
async def upload_to_work(self, image: platform_message.Image):
async def upload_image_to_work(self, image: platform_message.Image):
"""
获取 media_id
"""
@@ -287,7 +357,7 @@ class WecomClient:
file_bytes = await f.read()
file_name = image.path.split('/')[-1]
elif image.url:
file_bytes = await self.download_image_to_bytes(image.url)
file_bytes = await self.download_media_to_bytes(image.url)
file_name = image.url.split('/')[-1]
elif image.base64:
try:
@@ -322,7 +392,7 @@ class WecomClient:
data = response.json()
if data['errcode'] == 40014 or data['errcode'] == 42001:
self.access_token = await self.get_access_token(self.secret)
media_id = await self.upload_to_work(image)
media_id = await self.upload_image_to_work(image)
if data.get('errcode', 0) != 0:
await self.logger.error(f'上传图片失败:{data}')
raise Exception('failed to upload file')
@@ -330,13 +400,128 @@ class WecomClient:
media_id = data.get('media_id')
return media_id
async def download_image_to_bytes(self, url: str) -> bytes:
async def upload_voice_to_work(self, voice: platform_message.Voice):
"""
上传语音文件到企业微信
"""
if not await self.check_access_token():
self.access_token = await self.get_access_token(self.secret)
url = self.base_url + '/media/upload?access_token=' + self.access_token + '&type=file'
file_bytes = None
file_name = 'voice.mp3'
if voice.path:
async with aiofiles.open(voice.path, 'rb') as f:
file_bytes = await f.read()
file_name = voice.path.split('/')[-1]
elif voice.url:
file_bytes = await self.download_media_to_bytes(voice.url)
file_name = voice.url.split('/')[-1]
elif voice.base64:
try:
base64_data = voice.base64
if ',' in base64_data:
base64_data = base64_data.split(',', 1)[1]
padding = 4 - (len(base64_data) % 4) if len(base64_data) % 4 else 0
padded_base64 = base64_data + '=' * padding
file_bytes = base64.b64decode(padded_base64)
except binascii.Error as e:
raise ValueError(f'Invalid base64 string: {str(e)}')
else:
await self.logger.error('Voice对象出错')
raise ValueError('voice对象出错')
boundary = '-------------------------acebdf13572468'
headers = {'Content-Type': f'multipart/form-data; boundary={boundary}'}
body = (
(
f'--{boundary}\r\n'
f'Content-Disposition: form-data; name="media"; filename="{file_name}"; filelength={len(file_bytes)}\r\n'
f'Content-Type: application/octet-stream\r\n\r\n'
).encode('utf-8')
+ file_bytes
+ f'\r\n--{boundary}--\r\n'.encode('utf-8')
)
# print(body)
async with httpx.AsyncClient() as client:
response = await client.post(url, headers=headers, content=body)
data = response.json()
if data['errcode'] == 40014 or data['errcode'] == 42001:
self.access_token = await self.get_access_token(self.secret)
media_id = await self.upload_voice_to_work(voice)
if data.get('errcode', 0) != 0:
await self.logger.error(f'上传语音文件失败:{data}')
raise Exception('failed to upload file')
media_id = data.get('media_id')
return media_id
async def upload_file_to_work(self, file: platform_message.File):
"""
上传文件到企业微信
"""
if not await self.check_access_token():
self.access_token = await self.get_access_token(self.secret)
url = self.base_url + '/media/upload?access_token=' + self.access_token + '&type=file'
file_bytes = None
file_name = 'file.txt'
if file.path:
async with aiofiles.open(file.path, 'rb') as f:
file_bytes = await f.read()
file_name = file.path.split('/')[-1]
elif file.url:
file_bytes = await self.download_media_to_bytes(file.url)
file_name = file.url.split('/')[-1]
elif file.base64:
try:
base64_data = file.base64
if ',' in base64_data:
base64_data = base64_data.split(',', 1)[1]
padding = 4 - (len(base64_data) % 4) if len(base64_data) % 4 else 0
padded_base64 = base64_data + '=' * padding
file_bytes = base64.b64decode(padded_base64)
except binascii.Error as e:
raise ValueError(f'Invalid base64 string: {str(e)}')
else:
await self.logger.error('File对象出错')
raise ValueError('file对象出错')
boundary = '-------------------------acebdf13572468'
headers = {'Content-Type': f'multipart/form-data; boundary={boundary}'}
body = (
(
f'--{boundary}\r\n'
f'Content-Disposition: form-data; name="media"; filename="{file_name}"; filelength={len(file_bytes)}\r\n'
f'Content-Type: application/octet-stream\r\n\r\n'
).encode('utf-8')
+ file_bytes
+ f'\r\n--{boundary}--\r\n'.encode('utf-8')
)
async with httpx.AsyncClient() as client:
response = await client.post(url, headers=headers, content=body)
data = response.json()
if data['errcode'] == 40014 or data['errcode'] == 42001:
self.access_token = await self.get_access_token(self.secret)
media_id = await self.upload_file_to_work(file)
if data.get('errcode', 0) != 0:
await self.logger.error(f'上传文件失败:{data}')
raise Exception('failed to upload file')
media_id = data.get('media_id')
return media_id
async def download_media_to_bytes(self, url: str) -> bytes:
async with httpx.AsyncClient() as client:
response = await client.get(url)
response.raise_for_status()
return response.content
# 进行media_id的获取
async def get_media_id(self, image: platform_message.Image):
media_id = await self.upload_to_work(image=image)
async def get_media_id(self, media: platform_message.Image | platform_message.Voice | platform_message.File):
if isinstance(media, platform_message.Image):
media_id = await self.upload_image_to_work(image=media)
elif isinstance(media, platform_message.Voice):
media_id = await self.upload_voice_to_work(voice=media)
elif isinstance(media, platform_message.File):
media_id = await self.upload_file_to_work(file=media)
else:
raise ValueError('Unsupported media type')
return media_id

View File

@@ -13,19 +13,33 @@ import aiofiles
class WecomCSClient:
def __init__(self, corpid: str, secret: str, token: str, EncodingAESKey: str, logger: None):
def __init__(
self,
corpid: str,
secret: str,
token: str,
EncodingAESKey: str,
logger: None,
unified_mode: bool = False,
api_base_url: str = 'https://qyapi.weixin.qq.com/cgi-bin',
):
self.corpid = corpid
self.secret = secret
self.access_token_for_contacts = ''
self.token = token
self.aes = EncodingAESKey
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
self.base_url = api_base_url
self.access_token = ''
self.logger = logger
self.unified_mode = unified_mode
self.app = Quart(__name__)
self.app.add_url_rule(
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
)
# 只有在非统一模式下才注册独立路由
if not self.unified_mode:
self.app.add_url_rule(
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
)
self._message_handlers = {
'example': [],
}
@@ -61,7 +75,7 @@ class WecomCSClient:
return bool(self.access_token_for_contacts and self.access_token_for_contacts.strip())
async def get_access_token(self, secret):
url = f'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={self.corpid}&corpsecret={secret}'
url = f'{self.base_url}/gettoken?corpid={self.corpid}&corpsecret={secret}'
async with httpx.AsyncClient() as client:
response = await client.get(url)
data = response.json()
@@ -167,7 +181,7 @@ class WecomCSClient:
if not await self.check_access_token():
self.access_token = await self.get_access_token(self.secret)
url = f'https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg?access_token={self.access_token}'
url = f'{self.base_url}/kf/send_msg?access_token={self.access_token}'
payload = {
'touser': external_userid,
@@ -192,27 +206,45 @@ class WecomCSClient:
return data
async def handle_callback_request(self):
"""处理回调请求(独立端口模式,使用全局 request"""
return await self._handle_callback_internal(request)
async def handle_unified_webhook(self, req):
"""处理回调请求(统一 webhook 模式,显式传递 request
Args:
req: Quart Request 对象
Returns:
响应数据
"""
处理回调请求,包括 GET 验证和 POST 消息接收。
return await self._handle_callback_internal(req)
async def _handle_callback_internal(self, req):
"""
处理回调请求的内部实现,包括 GET 验证和 POST 消息接收。
Args:
req: Quart Request 对象
"""
try:
msg_signature = request.args.get('msg_signature')
timestamp = request.args.get('timestamp')
nonce = request.args.get('nonce')
msg_signature = req.args.get('msg_signature')
timestamp = req.args.get('timestamp')
nonce = req.args.get('nonce')
try:
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
except Exception as e:
raise Exception(f'初始化失败,错误码: {e}')
if request.method == 'GET':
echostr = request.args.get('echostr')
if req.method == 'GET':
echostr = req.args.get('echostr')
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
if ret != 0:
raise Exception(f'验证失败,错误码: {ret}')
return reply_echo_str
elif request.method == 'POST':
encrypt_msg = await request.data
elif req.method == 'POST':
encrypt_msg = await req.data
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
if ret != 0:
raise Exception(f'消息解密失败,错误码: {ret}')

View File

@@ -28,8 +28,56 @@ class FilesRouterGroup(group.RouterGroup):
return quart.Response(image_bytes, mimetype=mime_type)
@self.route('/documents', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> quart.Response:
@self.route('/images', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def upload_image() -> quart.Response:
request = quart.request
# Check file size limit before reading the file
content_length = request.content_length
if content_length and content_length > group.MAX_FILE_SIZE:
return self.fail(400, 'Image size exceeds 10MB limit.')
# get file bytes from 'file'
files = await request.files
if 'file' not in files:
return self.fail(400, 'No image file provided')
file = files['file']
assert isinstance(file, quart.datastructures.FileStorage)
file_bytes = await asyncio.to_thread(file.stream.read)
# Double-check actual file size after reading
if len(file_bytes) > group.MAX_FILE_SIZE:
return self.fail(400, 'Image size exceeds 10MB limit.')
# Validate image file extension
allowed_extensions = {'jpg', 'jpeg', 'png', 'gif', 'webp'}
if '.' in file.filename:
file_name, extension = file.filename.rsplit('.', 1)
extension = extension.lower()
else:
return self.fail(400, 'Invalid image file: no file extension')
if extension not in allowed_extensions:
return self.fail(400, f'Invalid image format. Allowed formats: {", ".join(allowed_extensions)}')
# check if file name contains '/' or '\'
if '/' in file_name or '\\' in file_name:
return self.fail(400, 'File name contains invalid characters')
file_key = file_name + '_' + str(uuid.uuid4())[:8] + '.' + extension
# save file to storage
await self.ap.storage_mgr.storage_provider.save(file_key, file_bytes)
return self.success(
data={
'file_key': file_key,
}
)
@self.route('/documents', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def upload_document() -> quart.Response:
request = quart.request
# Check file size limit before reading the file

View File

@@ -5,7 +5,7 @@ from ... import group
@group.group_class('knowledge_base', '/api/v1/knowledge/bases')
class KnowledgeBaseRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('', methods=['POST', 'GET'])
@self.route('', methods=['POST', 'GET'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def handle_knowledge_bases() -> quart.Response:
if quart.request.method == 'GET':
knowledge_bases = await self.ap.knowledge_service.get_knowledge_bases()
@@ -13,7 +13,10 @@ class KnowledgeBaseRouterGroup(group.RouterGroup):
elif quart.request.method == 'POST':
json_data = await quart.request.json
knowledge_base_uuid = await self.ap.knowledge_service.create_knowledge_base(json_data)
try:
knowledge_base_uuid = await self.ap.knowledge_service.create_knowledge_base(json_data)
except ValueError as e:
return self.http_status(400, -1, str(e))
return self.success(data={'uuid': knowledge_base_uuid})
return self.http_status(405, -1, 'Method not allowed')
@@ -21,6 +24,7 @@ class KnowledgeBaseRouterGroup(group.RouterGroup):
@self.route(
'/<knowledge_base_uuid>',
methods=['GET', 'DELETE', 'PUT'],
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
)
async def handle_specific_knowledge_base(knowledge_base_uuid: str) -> quart.Response:
if quart.request.method == 'GET':
@@ -38,7 +42,7 @@ class KnowledgeBaseRouterGroup(group.RouterGroup):
elif quart.request.method == 'PUT':
json_data = await quart.request.json
await self.ap.knowledge_service.update_knowledge_base(knowledge_base_uuid, json_data)
return self.success({})
return self.success(data={'uuid': knowledge_base_uuid})
elif quart.request.method == 'DELETE':
await self.ap.knowledge_service.delete_knowledge_base(knowledge_base_uuid)
@@ -47,6 +51,7 @@ class KnowledgeBaseRouterGroup(group.RouterGroup):
@self.route(
'/<knowledge_base_uuid>/files',
methods=['GET', 'POST'],
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
)
async def get_knowledge_base_files(knowledge_base_uuid: str) -> str:
if quart.request.method == 'GET':
@@ -63,8 +68,12 @@ class KnowledgeBaseRouterGroup(group.RouterGroup):
if not file_id:
return self.http_status(400, -1, 'File ID is required')
parser_plugin_id = json_data.get('parser_plugin_id')
# 调用服务层方法将文件与知识库关联
task_id = await self.ap.knowledge_service.store_file(knowledge_base_uuid, file_id)
task_id = await self.ap.knowledge_service.store_file(
knowledge_base_uuid, file_id, parser_plugin_id=parser_plugin_id
)
return self.success(
{
'task_id': task_id,
@@ -74,6 +83,7 @@ class KnowledgeBaseRouterGroup(group.RouterGroup):
@self.route(
'/<knowledge_base_uuid>/files/<file_id>',
methods=['DELETE'],
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
)
async def delete_specific_file_in_kb(file_id: str, knowledge_base_uuid: str) -> str:
await self.ap.knowledge_service.delete_file(knowledge_base_uuid, file_id)
@@ -82,9 +92,18 @@ class KnowledgeBaseRouterGroup(group.RouterGroup):
@self.route(
'/<knowledge_base_uuid>/retrieve',
methods=['POST'],
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
)
async def retrieve_knowledge_base(knowledge_base_uuid: str) -> str:
json_data = await quart.request.json
query = json_data.get('query')
results = await self.ap.knowledge_service.retrieve_knowledge_base(knowledge_base_uuid, query)
if not query or not query.strip():
return self.http_status(400, -1, 'Query is required and cannot be empty')
# Extract retrieval_settings to allow dynamic control over Knowledge Engine behavior (e.g. top_k, filters)
retrieval_settings = json_data.get('retrieval_settings', {})
results = await self.ap.knowledge_service.retrieve_knowledge_base(
knowledge_base_uuid, query, retrieval_settings
)
return self.success(data={'results': results})

View File

@@ -0,0 +1,45 @@
import quart
from urllib.parse import unquote
from ... import group
@group.group_class('knowledge_engines', '/api/v1/knowledge/engines')
class KnowledgeEnginesRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def list_knowledge_engines() -> quart.Response:
"""List all available Knowledge Engines from plugins.
Returns a list of Knowledge Engines with their capabilities and configuration schemas.
This is used by the frontend to render the knowledge base creation wizard.
"""
engines = await self.ap.knowledge_service.list_knowledge_engines()
return self.success(data={'engines': engines})
@self.route(
'/<path:plugin_id>/creation-schema', methods=['GET'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY
)
async def get_engine_creation_schema(plugin_id: str) -> quart.Response:
"""Get creation settings schema for a specific Knowledge Engine.
plugin_id is in 'author/name' format, captured via <path:> converter.
"""
plugin_id = unquote(plugin_id)
if '/' not in plugin_id:
return self.http_status(400, -1, 'Invalid plugin_id format. Expected author/name.')
schema = await self.ap.knowledge_service.get_engine_creation_schema(plugin_id)
return self.success(data={'schema': schema})
@self.route(
'/<path:plugin_id>/retrieval-schema', methods=['GET'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY
)
async def get_engine_retrieval_schema(plugin_id: str) -> quart.Response:
"""Get retrieval settings schema for a specific Knowledge Engine.
plugin_id is in 'author/name' format, captured via <path:> converter.
"""
plugin_id = unquote(plugin_id)
if '/' not in plugin_id:
return self.http_status(400, -1, 'Invalid plugin_id format. Expected author/name.')
schema = await self.ap.knowledge_service.get_engine_retrieval_schema(plugin_id)
return self.success(data={'schema': schema})

View File

@@ -0,0 +1,16 @@
import quart
from ... import group
@group.group_class('parsers', '/api/v1/knowledge/parsers')
class ParsersRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def list_parsers() -> quart.Response:
"""List all available parsers from plugins.
Optional query parameter `mime_type` to filter parsers by supported MIME type.
"""
mime_type = quart.request.args.get('mime_type')
parsers = await self.ap.knowledge_service.list_parsers(mime_type)
return self.success(data={'parsers': parsers})

View File

@@ -0,0 +1,488 @@
from __future__ import annotations
import datetime
import quart
from .. import group
def parse_iso_datetime(datetime_str: str | None) -> datetime.datetime | None:
"""Parse ISO 8601 datetime string, handling 'Z' suffix for UTC timezone"""
if not datetime_str:
return None
# Replace 'Z' with '+00:00' for Python 3.10 compatibility
if datetime_str.endswith('Z'):
datetime_str = datetime_str[:-1] + '+00:00'
dt = datetime.datetime.fromisoformat(datetime_str)
# Convert to UTC and remove timezone info to match database storage (which stores UTC as naive datetime)
if dt.tzinfo is not None:
# Convert to UTC and remove timezone info
dt = dt.astimezone(datetime.timezone.utc).replace(tzinfo=None)
return dt
@group.group_class('monitoring', '/api/v1/monitoring')
class MonitoringRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('/overview', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def get_overview() -> str:
"""Get overview metrics"""
# Parse query parameters
bot_ids = quart.request.args.getlist('botId')
pipeline_ids = quart.request.args.getlist('pipelineId')
start_time_str = quart.request.args.get('startTime')
end_time_str = quart.request.args.get('endTime')
# Parse datetime
start_time = parse_iso_datetime(start_time_str)
end_time = parse_iso_datetime(end_time_str)
metrics = await self.ap.monitoring_service.get_overview_metrics(
bot_ids=bot_ids if bot_ids else None,
pipeline_ids=pipeline_ids if pipeline_ids else None,
start_time=start_time,
end_time=end_time,
)
return self.success(data=metrics)
@self.route('/messages', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def get_messages() -> str:
"""Get message logs"""
# Parse query parameters
bot_ids = quart.request.args.getlist('botId')
pipeline_ids = quart.request.args.getlist('pipelineId')
session_ids = quart.request.args.getlist('sessionId')
start_time_str = quart.request.args.get('startTime')
end_time_str = quart.request.args.get('endTime')
limit = int(quart.request.args.get('limit', 100))
offset = int(quart.request.args.get('offset', 0))
# Parse datetime
start_time = parse_iso_datetime(start_time_str)
end_time = parse_iso_datetime(end_time_str)
messages, total = await self.ap.monitoring_service.get_messages(
bot_ids=bot_ids if bot_ids else None,
pipeline_ids=pipeline_ids if pipeline_ids else None,
session_ids=session_ids if session_ids else None,
start_time=start_time,
end_time=end_time,
limit=limit,
offset=offset,
)
return self.success(
data={
'messages': messages,
'total': total,
'limit': limit,
'offset': offset,
}
)
@self.route('/llm-calls', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def get_llm_calls() -> str:
"""Get LLM call records"""
# Parse query parameters
bot_ids = quart.request.args.getlist('botId')
pipeline_ids = quart.request.args.getlist('pipelineId')
start_time_str = quart.request.args.get('startTime')
end_time_str = quart.request.args.get('endTime')
limit = int(quart.request.args.get('limit', 100))
offset = int(quart.request.args.get('offset', 0))
# Parse datetime
start_time = parse_iso_datetime(start_time_str)
end_time = parse_iso_datetime(end_time_str)
llm_calls, total = await self.ap.monitoring_service.get_llm_calls(
bot_ids=bot_ids if bot_ids else None,
pipeline_ids=pipeline_ids if pipeline_ids else None,
start_time=start_time,
end_time=end_time,
limit=limit,
offset=offset,
)
return self.success(
data={
'llm_calls': llm_calls,
'total': total,
'limit': limit,
'offset': offset,
}
)
@self.route('/embedding-calls', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def get_embedding_calls() -> str:
"""Get embedding call records"""
# Parse query parameters
start_time_str = quart.request.args.get('startTime')
end_time_str = quart.request.args.get('endTime')
knowledge_base_id = quart.request.args.get('knowledgeBaseId')
limit = int(quart.request.args.get('limit', 100))
offset = int(quart.request.args.get('offset', 0))
# Parse datetime
start_time = parse_iso_datetime(start_time_str)
end_time = parse_iso_datetime(end_time_str)
embedding_calls, total = await self.ap.monitoring_service.get_embedding_calls(
start_time=start_time,
end_time=end_time,
knowledge_base_id=knowledge_base_id if knowledge_base_id else None,
limit=limit,
offset=offset,
)
return self.success(
data={
'embedding_calls': embedding_calls,
'total': total,
'limit': limit,
'offset': offset,
}
)
@self.route('/sessions', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def get_sessions() -> str:
"""Get session information"""
# Parse query parameters
bot_ids = quart.request.args.getlist('botId')
pipeline_ids = quart.request.args.getlist('pipelineId')
start_time_str = quart.request.args.get('startTime')
end_time_str = quart.request.args.get('endTime')
is_active_str = quart.request.args.get('isActive')
limit = int(quart.request.args.get('limit', 100))
offset = int(quart.request.args.get('offset', 0))
# Parse datetime
start_time = parse_iso_datetime(start_time_str)
end_time = parse_iso_datetime(end_time_str)
# Parse is_active
is_active = None
if is_active_str:
is_active = is_active_str.lower() == 'true'
sessions, total = await self.ap.monitoring_service.get_sessions(
bot_ids=bot_ids if bot_ids else None,
pipeline_ids=pipeline_ids if pipeline_ids else None,
start_time=start_time,
end_time=end_time,
is_active=is_active,
limit=limit,
offset=offset,
)
return self.success(
data={
'sessions': sessions,
'total': total,
'limit': limit,
'offset': offset,
}
)
@self.route('/errors', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def get_errors() -> str:
"""Get error logs"""
# Parse query parameters
bot_ids = quart.request.args.getlist('botId')
pipeline_ids = quart.request.args.getlist('pipelineId')
start_time_str = quart.request.args.get('startTime')
end_time_str = quart.request.args.get('endTime')
limit = int(quart.request.args.get('limit', 100))
offset = int(quart.request.args.get('offset', 0))
# Parse datetime
start_time = parse_iso_datetime(start_time_str)
end_time = parse_iso_datetime(end_time_str)
errors, total = await self.ap.monitoring_service.get_errors(
bot_ids=bot_ids if bot_ids else None,
pipeline_ids=pipeline_ids if pipeline_ids else None,
start_time=start_time,
end_time=end_time,
limit=limit,
offset=offset,
)
return self.success(
data={
'errors': errors,
'total': total,
'limit': limit,
'offset': offset,
}
)
@self.route('/data', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def get_all_data() -> str:
"""Get all monitoring data in a single request"""
# Parse query parameters
bot_ids = quart.request.args.getlist('botId')
pipeline_ids = quart.request.args.getlist('pipelineId')
start_time_str = quart.request.args.get('startTime')
end_time_str = quart.request.args.get('endTime')
limit = int(quart.request.args.get('limit', 50))
# Parse datetime
start_time = parse_iso_datetime(start_time_str)
end_time = parse_iso_datetime(end_time_str)
# Get overview metrics
overview = await self.ap.monitoring_service.get_overview_metrics(
bot_ids=bot_ids if bot_ids else None,
pipeline_ids=pipeline_ids if pipeline_ids else None,
start_time=start_time,
end_time=end_time,
)
# Get messages
messages, messages_total = await self.ap.monitoring_service.get_messages(
bot_ids=bot_ids if bot_ids else None,
pipeline_ids=pipeline_ids if pipeline_ids else None,
start_time=start_time,
end_time=end_time,
limit=limit,
offset=0,
)
# Get LLM calls
llm_calls, llm_calls_total = await self.ap.monitoring_service.get_llm_calls(
bot_ids=bot_ids if bot_ids else None,
pipeline_ids=pipeline_ids if pipeline_ids else None,
start_time=start_time,
end_time=end_time,
limit=limit,
offset=0,
)
# Get sessions
sessions, sessions_total = await self.ap.monitoring_service.get_sessions(
bot_ids=bot_ids if bot_ids else None,
pipeline_ids=pipeline_ids if pipeline_ids else None,
start_time=start_time,
end_time=end_time,
is_active=None,
limit=limit,
offset=0,
)
# Get errors
errors, errors_total = await self.ap.monitoring_service.get_errors(
bot_ids=bot_ids if bot_ids else None,
pipeline_ids=pipeline_ids if pipeline_ids else None,
start_time=start_time,
end_time=end_time,
limit=limit,
offset=0,
)
# Get embedding calls
embedding_calls, embedding_calls_total = await self.ap.monitoring_service.get_embedding_calls(
start_time=start_time,
end_time=end_time,
limit=limit,
offset=0,
)
return self.success(
data={
'overview': overview,
'messages': messages,
'llmCalls': llm_calls,
'embeddingCalls': embedding_calls,
'sessions': sessions,
'errors': errors,
'totalCount': {
'messages': messages_total,
'llmCalls': llm_calls_total,
'embeddingCalls': embedding_calls_total,
'sessions': sessions_total,
'errors': errors_total,
},
}
)
@self.route('/sessions/<session_id>/analysis', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def get_session_analysis(session_id: str) -> str:
"""Get detailed analysis for a specific session"""
analysis = await self.ap.monitoring_service.get_session_analysis(session_id)
# Always return success with the analysis data
# The frontend will handle the 'found: false' case
return self.success(data=analysis)
@self.route('/messages/<message_id>/details', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def get_message_details(message_id: str) -> str:
"""Get detailed information for a specific message"""
details = await self.ap.monitoring_service.get_message_details(message_id)
if not details.get('found'):
return self.error(message=f'Message {message_id} not found', code=404)
return self.success(data=details)
@self.route('/export', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def export_data() -> tuple[str, int]:
"""Export monitoring data as CSV"""
# Parse query parameters
export_type = quart.request.args.get('type', 'messages')
bot_ids = quart.request.args.getlist('botId')
pipeline_ids = quart.request.args.getlist('pipelineId')
start_time_str = quart.request.args.get('startTime')
end_time_str = quart.request.args.get('endTime')
limit = int(quart.request.args.get('limit', 100000))
# Parse datetime
start_time = parse_iso_datetime(start_time_str)
end_time = parse_iso_datetime(end_time_str)
# Get data based on export type
if export_type == 'messages':
data = await self.ap.monitoring_service.export_messages(
bot_ids=bot_ids if bot_ids else None,
pipeline_ids=pipeline_ids if pipeline_ids else None,
start_time=start_time,
end_time=end_time,
limit=limit,
)
headers = [
'id',
'timestamp',
'bot_id',
'bot_name',
'pipeline_id',
'pipeline_name',
'runner_name',
'message_content',
'message_text',
'session_id',
'status',
'level',
'platform',
'user_id',
]
elif export_type == 'llm-calls':
data = await self.ap.monitoring_service.export_llm_calls(
bot_ids=bot_ids if bot_ids else None,
pipeline_ids=pipeline_ids if pipeline_ids else None,
start_time=start_time,
end_time=end_time,
limit=limit,
)
headers = [
'id',
'timestamp',
'model_name',
'input_tokens',
'output_tokens',
'total_tokens',
'duration_ms',
'cost',
'status',
'bot_id',
'bot_name',
'pipeline_id',
'pipeline_name',
'session_id',
'message_id',
'error_message',
]
elif export_type == 'embedding-calls':
data = await self.ap.monitoring_service.export_embedding_calls(
start_time=start_time,
end_time=end_time,
limit=limit,
)
headers = [
'id',
'timestamp',
'model_name',
'prompt_tokens',
'total_tokens',
'duration_ms',
'input_count',
'status',
'error_message',
'knowledge_base_id',
'query_text',
'session_id',
'message_id',
'call_type',
]
elif export_type == 'errors':
data = await self.ap.monitoring_service.export_errors(
bot_ids=bot_ids if bot_ids else None,
pipeline_ids=pipeline_ids if pipeline_ids else None,
start_time=start_time,
end_time=end_time,
limit=limit,
)
headers = [
'id',
'timestamp',
'error_type',
'error_message',
'bot_id',
'bot_name',
'pipeline_id',
'pipeline_name',
'session_id',
'message_id',
'stack_trace',
]
elif export_type == 'sessions':
data = await self.ap.monitoring_service.export_sessions(
bot_ids=bot_ids if bot_ids else None,
pipeline_ids=pipeline_ids if pipeline_ids else None,
start_time=start_time,
end_time=end_time,
limit=limit,
)
headers = [
'session_id',
'bot_id',
'bot_name',
'pipeline_id',
'pipeline_name',
'message_count',
'start_time',
'last_activity',
'is_active',
'platform',
'user_id',
]
else:
return self.error(message=f'Invalid export type: {export_type}', code=400)
# Generate CSV content with UTF-8 BOM for Excel compatibility
import io
output = io.StringIO()
# Write UTF-8 BOM for Excel
output.write('\ufeff')
# Write header
output.write(','.join(headers) + '\n')
# Escape and write each row
for row in data:
escaped_values = []
for header in headers:
value = row.get(header, '')
escaped_values.append(self.ap.monitoring_service._escape_csv_field(value))
output.write(','.join(escaped_values) + '\n')
csv_content = output.getvalue()
# Return as file download
response = await quart.make_response(csv_content)
response.headers['Content-Type'] = 'text/csv; charset=utf-8'
response.headers['Content-Disposition'] = (
f'attachment; filename="monitoring-{export_type}-{int(datetime.datetime.now().timestamp())}.csv"'
)
return response, 200

View File

@@ -49,6 +49,14 @@ class PipelinesRouterGroup(group.RouterGroup):
return self.success()
@self.route('/<pipeline_uuid>/copy', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def _(pipeline_uuid: str) -> str:
try:
new_uuid = await self.ap.pipeline_service.copy_pipeline(pipeline_uuid)
return self.success(data={'uuid': new_uuid})
except ValueError as e:
return self.http_status(404, -1, str(e))
@self.route(
'/<pipeline_uuid>/extensions', methods=['GET', 'PUT'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY
)
@@ -59,25 +67,33 @@ class PipelinesRouterGroup(group.RouterGroup):
if pipeline is None:
return self.http_status(404, -1, 'pipeline not found')
plugins = await self.ap.plugin_connector.list_plugins()
# Only include plugins with pipeline-related components (Command, EventListener, Tool)
# Plugins that only have KnowledgeEngine components are not suitable for pipeline extensions
pipeline_component_kinds = ['Command', 'EventListener', 'Tool']
plugins = await self.ap.plugin_connector.list_plugins(component_kinds=pipeline_component_kinds)
mcp_servers = await self.ap.mcp_service.get_mcp_servers(contain_runtime_info=True)
extensions_prefs = pipeline.get('extensions_preferences', {})
return self.success(
data={
'bound_plugins': pipeline.get('extensions_preferences', {}).get('plugins', []),
'enable_all_plugins': extensions_prefs.get('enable_all_plugins', True),
'enable_all_mcp_servers': extensions_prefs.get('enable_all_mcp_servers', True),
'bound_plugins': extensions_prefs.get('plugins', []),
'available_plugins': plugins,
'bound_mcp_servers': pipeline.get('extensions_preferences', {}).get('mcp_servers', []),
'bound_mcp_servers': extensions_prefs.get('mcp_servers', []),
'available_mcp_servers': mcp_servers,
}
)
elif quart.request.method == 'PUT':
# Update bound plugins and MCP servers for this pipeline
json_data = await quart.request.json
enable_all_plugins = json_data.get('enable_all_plugins', True)
enable_all_mcp_servers = json_data.get('enable_all_mcp_servers', True)
bound_plugins = json_data.get('bound_plugins', [])
bound_mcp_servers = json_data.get('bound_mcp_servers', [])
await self.ap.pipeline_service.update_pipeline_extensions(
pipeline_uuid, bound_plugins, bound_mcp_servers
pipeline_uuid, bound_plugins, bound_mcp_servers, enable_all_plugins, enable_all_mcp_servers
)
return self.success()

View File

@@ -1,109 +0,0 @@
import json
import quart
from ... import group
@group.group_class('webchat', '/api/v1/pipelines/<pipeline_uuid>/chat')
class WebChatDebugRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('/send', methods=['POST'])
async def send_message(pipeline_uuid: str) -> str:
"""Send a message to the pipeline for debugging"""
async def stream_generator(generator):
yield 'data: {"type": "start"}\n\n'
async for message in generator:
yield f'data: {json.dumps({"message": message})}\n\n'
yield 'data: {"type": "end"}\n\n'
try:
data = await quart.request.get_json()
session_type = data.get('session_type', 'person')
message_chain_obj = data.get('message', [])
is_stream = data.get('is_stream', False)
if not message_chain_obj:
return self.http_status(400, -1, 'message is required')
if session_type not in ['person', 'group']:
return self.http_status(400, -1, 'session_type must be person or group')
webchat_adapter = self.ap.platform_mgr.webchat_proxy_bot.adapter
if not webchat_adapter:
return self.http_status(404, -1, 'WebChat adapter not found')
if is_stream:
generator = webchat_adapter.send_webchat_message(
pipeline_uuid, session_type, message_chain_obj, is_stream
)
# 设置正确的响应头
headers = {
'Content-Type': 'text/event-stream',
'Transfer-Encoding': 'chunked',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
}
return quart.Response(stream_generator(generator), mimetype='text/event-stream', headers=headers)
else: # non-stream
result = None
async for message in webchat_adapter.send_webchat_message(
pipeline_uuid, session_type, message_chain_obj
):
result = message
if result is not None:
return self.success(
data={
'message': result,
}
)
else:
return self.http_status(400, -1, 'message is required')
except Exception as e:
return self.http_status(500, -1, f'Internal server error: {str(e)}')
@self.route('/messages/<session_type>', methods=['GET'])
async def get_messages(pipeline_uuid: str, session_type: str) -> str:
"""Get the message history of the pipeline for debugging"""
try:
if session_type not in ['person', 'group']:
return self.http_status(400, -1, 'session_type must be person or group')
webchat_adapter = self.ap.platform_mgr.webchat_proxy_bot.adapter
if not webchat_adapter:
return self.http_status(404, -1, 'WebChat adapter not found')
messages = webchat_adapter.get_webchat_messages(pipeline_uuid, session_type)
return self.success(data={'messages': messages})
except Exception as e:
return self.http_status(500, -1, f'Internal server error: {str(e)}')
@self.route('/reset/<session_type>', methods=['POST'])
async def reset_session(session_type: str) -> str:
"""Reset the debug session"""
try:
if session_type not in ['person', 'group']:
return self.http_status(400, -1, 'session_type must be person or group')
webchat_adapter = None
for bot in self.ap.platform_mgr.bots:
if hasattr(bot.adapter, '__class__') and bot.adapter.__class__.__name__ == 'WebChatAdapter':
webchat_adapter = bot.adapter
break
if not webchat_adapter:
return self.http_status(404, -1, 'WebChat adapter not found')
webchat_adapter.reset_debug_session(session_type)
return self.success(data={'message': 'Session reset successfully'})
except Exception as e:
return self.http_status(500, -1, f'Internal server error: {str(e)}')

View File

@@ -0,0 +1,243 @@
"""WebSocket聊天路由 - 支持双向实时通信"""
import asyncio
import datetime
import json
import logging
import quart
from ... import group
from ......platform.sources.websocket_manager import ws_connection_manager
logger = logging.getLogger(__name__)
@group.group_class('websocket_chat', '/api/v1/pipelines/<pipeline_uuid>/ws')
class WebSocketChatRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
# 直接使用 quart_app 注册 WebSocket 路由
@self.quart_app.websocket(self.path + '/connect')
async def websocket_connect(pipeline_uuid: str):
"""
建立WebSocket连接
URL参数:
- pipeline_uuid: 流水线UUID
- session_type: 会话类型 (person/group)
"""
try:
# 获取参数 - 在WebSocket上下文中使用 quart.websocket.args
session_type = quart.websocket.args.get('session_type', 'person')
if session_type not in ['person', 'group']:
await quart.websocket.send(
json.dumps({'type': 'error', 'message': 'session_type must be person or group'})
)
return
# 获取WebSocket适配器
websocket_adapter = self.ap.platform_mgr.websocket_proxy_bot.adapter
if not websocket_adapter:
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'WebSocket adapter not found'}))
return
# 注册连接
connection = await ws_connection_manager.add_connection(
websocket=quart.websocket._get_current_object(),
pipeline_uuid=pipeline_uuid,
session_type=session_type,
metadata={'user_agent': quart.websocket.headers.get('User-Agent', '')},
)
# 发送连接成功消息
await quart.websocket.send(
json.dumps(
{
'type': 'connected',
'connection_id': connection.connection_id,
'pipeline_uuid': pipeline_uuid,
'session_type': session_type,
'timestamp': connection.created_at.isoformat(),
}
)
)
logger.debug(
f'WebSocket connection established: {connection.connection_id} '
f'(pipeline={pipeline_uuid}, session_type={session_type})'
)
# 创建接收和发送任务
receive_task = asyncio.create_task(self._handle_receive(connection, websocket_adapter))
send_task = asyncio.create_task(self._handle_send(connection))
# 等待任务完成
try:
await asyncio.gather(receive_task, send_task)
except Exception as e:
logger.error(f'WebSocket task execution error: {e}')
finally:
# 清理连接
await ws_connection_manager.remove_connection(connection.connection_id)
logger.debug(f'WebSocket connection cleaned: {connection.connection_id}')
except Exception as e:
logger.error(f'WebSocket connection error: {e}', exc_info=True)
try:
await quart.websocket.send(json.dumps({'type': 'error', 'message': str(e)}))
except:
pass
@self.route('/messages/<session_type>', methods=['GET'])
async def get_messages(pipeline_uuid: str, session_type: str) -> str:
"""获取消息历史"""
try:
if session_type not in ['person', 'group']:
return self.http_status(400, -1, 'session_type must be person or group')
websocket_adapter = self.ap.platform_mgr.websocket_proxy_bot.adapter
if not websocket_adapter:
return self.http_status(404, -1, 'WebSocket adapter not found')
messages = websocket_adapter.get_websocket_messages(pipeline_uuid, session_type)
return self.success(data={'messages': messages})
except Exception as e:
return self.http_status(500, -1, f'Internal server error: {str(e)}')
@self.route('/reset/<session_type>', methods=['POST'])
async def reset_session(pipeline_uuid: str, session_type: str) -> str:
"""重置会话"""
try:
if session_type not in ['person', 'group']:
return self.http_status(400, -1, 'session_type must be person or group')
websocket_adapter = self.ap.platform_mgr.websocket_proxy_bot.adapter
if not websocket_adapter:
return self.http_status(404, -1, 'WebSocket adapter not found')
websocket_adapter.reset_session(pipeline_uuid, session_type)
return self.success(data={'message': 'Session reset successfully'})
except Exception as e:
return self.http_status(500, -1, f'Internal server error: {str(e)}')
@self.route('/connections', methods=['GET'])
async def get_connections(pipeline_uuid: str) -> str:
"""获取当前连接统计"""
try:
stats = ws_connection_manager.get_stats()
connections = await ws_connection_manager.get_connections_by_pipeline(pipeline_uuid)
return self.success(
data={
'stats': stats,
'connections': [
{
'connection_id': conn.connection_id,
'session_type': conn.session_type,
'created_at': conn.created_at.isoformat(),
'last_active': conn.last_active.isoformat(),
'is_active': conn.is_active,
}
for conn in connections
],
}
)
except Exception as e:
return self.http_status(500, -1, f'Internal server error: {str(e)}')
@self.route('/broadcast', methods=['POST'])
async def broadcast_message(pipeline_uuid: str) -> str:
"""向所有连接广播消息(后端主动推送)"""
try:
data = await quart.request.get_json()
message = data.get('message')
if not message:
return self.http_status(400, -1, 'message is required')
# 广播消息
broadcast_data = {
'type': 'broadcast',
'message': message,
'timestamp': datetime.datetime.now().isoformat(),
}
await ws_connection_manager.broadcast_to_pipeline(pipeline_uuid, broadcast_data)
return self.success(data={'message': 'Broadcast sent successfully'})
except Exception as e:
return self.http_status(500, -1, f'Internal server error: {str(e)}')
async def _handle_receive(self, connection, websocket_adapter):
"""处理接收消息的任务"""
try:
while connection.is_active:
# 接收消息
message = await quart.websocket.receive()
# 更新活跃时间
await ws_connection_manager.update_activity(connection.connection_id)
try:
data = json.loads(message)
message_type = data.get('type', 'message')
if message_type == 'ping':
# 心跳响应
await connection.send_queue.put(
{'type': 'pong', 'timestamp': datetime.datetime.now().isoformat()}
)
elif message_type == 'message':
# 处理用户消息
logger.debug(f'收到消息: {data} from {connection.connection_id}')
# 处理消息不等待响应响应会通过broadcast异步发送
await websocket_adapter.handle_websocket_message(connection, data)
elif message_type == 'disconnect':
# 客户端主动断开
logger.debug(f'Client disconnected: {connection.connection_id}')
break
else:
logger.warning(f'Unknown message type: {message_type}')
except json.JSONDecodeError:
logger.error(f'Invalid JSON message: {message}')
await connection.send_queue.put({'type': 'error', 'message': 'Invalid JSON format'})
except Exception as e:
logger.error(f'Receive message error: {e}', exc_info=True)
finally:
connection.is_active = False
async def _handle_send(self, connection):
"""处理发送消息的任务"""
try:
while connection.is_active:
# 从队列获取消息
try:
message = await asyncio.wait_for(connection.send_queue.get(), timeout=1.0)
# 发送消息
await quart.websocket.send(json.dumps(message))
except asyncio.TimeoutError:
# 超时继续循环
continue
except Exception as e:
logger.error(f'Send message error: {e}', exc_info=True)
finally:
connection.is_active = False

View File

@@ -18,7 +18,8 @@ class BotsRouterGroup(group.RouterGroup):
@self.route('/<bot_uuid>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def _(bot_uuid: str) -> str:
if quart.request.method == 'GET':
bot = await self.ap.bot_service.get_bot(bot_uuid)
# 返回运行时信息包括webhook地址等
bot = await self.ap.bot_service.get_runtime_bot_info(bot_uuid)
if bot is None:
return self.http_status(404, -1, 'bot not found')
return self.success(data={'bot': bot})
@@ -42,3 +43,32 @@ class BotsRouterGroup(group.RouterGroup):
'total_count': total_count,
}
)
@self.route('/<bot_uuid>/send_message', methods=['POST'], auth_type=group.AuthType.API_KEY)
async def _(bot_uuid: str) -> str:
"""Send message to a specific target via bot"""
json_data = await quart.request.json
target_type = json_data.get('target_type')
target_id = json_data.get('target_id')
message_chain_data = json_data.get('message_chain')
# Validate required fields
if not target_type:
return self.http_status(400, -1, 'target_type is required')
if not target_id:
return self.http_status(400, -1, 'target_id is required')
if not message_chain_data:
return self.http_status(400, -1, 'message_chain is required')
# Validate target_type
if target_type not in ['person', 'group']:
return self.http_status(400, -1, 'target_type must be either "person" or "group"')
try:
await self.ap.bot_service.send_message(bot_uuid, target_type, target_id, message_chain_data)
return self.success(data={'sent': True})
except Exception as e:
import traceback
traceback.print_exc()
return self.http_status(500, -1, f'Failed to send message: {str(e)}')

View File

@@ -14,17 +14,45 @@ from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
@group.group_class('plugins', '/api/v1/plugins')
class PluginsRouterGroup(group.RouterGroup):
async def _check_extensions_limit(self) -> str | None:
"""Check if extensions limit is reached. Returns error response if limit exceeded, None otherwise."""
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
max_extensions = limitation.get('max_extensions', -1)
if max_extensions >= 0:
plugins = await self.ap.plugin_connector.list_plugins()
mcp_servers = await self.ap.mcp_service.get_mcp_servers()
total_extensions = len(plugins) + len(mcp_servers)
if total_extensions >= max_extensions:
return self.http_status(400, -1, f'Maximum number of extensions ({max_extensions}) reached')
return None
async def initialize(self) -> None:
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def _() -> str:
plugins = await self.ap.plugin_connector.list_plugins()
return self.success(data={'plugins': plugins})
@self.route('/debug-info', methods=['GET'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def _() -> str:
"""Get plugin debug information including debug URL and key"""
debug_info = await self.ap.plugin_connector.get_debug_info()
# Get debug URL from config
plugin_config = self.ap.instance_config.data.get('plugin', {})
debug_url = plugin_config.get('display_plugin_debug_url', 'http://localhost:5401')
return self.success(
data={
'debug_url': debug_url,
'plugin_debug_key': debug_info.get('plugin_debug_key', ''),
}
)
@self.route(
'/<author>/<plugin_name>/upgrade',
methods=['POST'],
auth_type=group.AuthType.USER_TOKEN,
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
)
async def _(author: str, plugin_name: str) -> str:
ctx = taskmgr.TaskContext.new()
@@ -40,7 +68,7 @@ class PluginsRouterGroup(group.RouterGroup):
@self.route(
'/<author>/<plugin_name>',
methods=['GET', 'DELETE'],
auth_type=group.AuthType.USER_TOKEN,
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
)
async def _(author: str, plugin_name: str) -> str:
if quart.request.method == 'GET':
@@ -66,7 +94,7 @@ class PluginsRouterGroup(group.RouterGroup):
@self.route(
'/<author>/<plugin_name>/config',
methods=['GET', 'PUT'],
auth_type=group.AuthType.USER_TOKEN,
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
)
async def _(author: str, plugin_name: str) -> quart.Response:
plugin = await self.ap.plugin_connector.get_plugin_info(author, plugin_name)
@@ -82,6 +110,16 @@ class PluginsRouterGroup(group.RouterGroup):
return self.success(data={})
@self.route(
'/<author>/<plugin_name>/readme',
methods=['GET'],
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
)
async def _(author: str, plugin_name: str) -> quart.Response:
language = quart.request.args.get('language', 'en')
readme = await self.ap.plugin_connector.get_plugin_readme(author, plugin_name, language=language)
return self.success(data={'readme': readme})
@self.route(
'/<author>/<plugin_name>/icon',
methods=['GET'],
@@ -96,7 +134,18 @@ class PluginsRouterGroup(group.RouterGroup):
return quart.Response(icon_data, mimetype=mime_type)
@self.route('/github/releases', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
@self.route(
'/<author>/<plugin_name>/assets/<filepath>',
methods=['GET'],
auth_type=group.AuthType.NONE,
)
async def _(author: str, plugin_name: str, filepath: str) -> quart.Response:
asset_data = await self.ap.plugin_connector.get_plugin_assets(author, plugin_name, filepath)
asset_bytes = base64.b64decode(asset_data['asset_base64'])
mime_type = asset_data['mime_type']
return quart.Response(asset_bytes, mimetype=mime_type)
@self.route('/github/releases', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def _() -> str:
"""Get releases from a GitHub repository URL"""
data = await quart.request.json
@@ -145,7 +194,7 @@ class PluginsRouterGroup(group.RouterGroup):
@self.route(
'/github/release-assets',
methods=['POST'],
auth_type=group.AuthType.USER_TOKEN,
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
)
async def _() -> str:
"""Get assets from a specific GitHub release"""
@@ -199,9 +248,13 @@ class PluginsRouterGroup(group.RouterGroup):
except httpx.RequestError as e:
return self.http_status(500, -1, f'Failed to fetch release assets: {str(e)}')
@self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
@self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def _() -> str:
"""Install plugin from GitHub release asset"""
limit_error = await self._check_extensions_limit()
if limit_error is not None:
return limit_error
data = await quart.request.json
asset_url = data.get('asset_url', '')
owner = data.get('owner', '')
@@ -233,9 +286,13 @@ class PluginsRouterGroup(group.RouterGroup):
@self.route(
'/install/marketplace',
methods=['POST'],
auth_type=group.AuthType.USER_TOKEN,
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
)
async def _() -> str:
limit_error = await self._check_extensions_limit()
if limit_error is not None:
return limit_error
data = await quart.request.json
ctx = taskmgr.TaskContext.new()
@@ -249,8 +306,12 @@ class PluginsRouterGroup(group.RouterGroup):
return self.success(data={'task_id': wrapper.id})
@self.route('/install/local', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
@self.route('/install/local', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def _() -> str:
limit_error = await self._check_extensions_limit()
if limit_error is not None:
return limit_error
file = (await quart.request.files).get('file')
if file is None:
return self.http_status(400, -1, 'file is required')

View File

@@ -9,12 +9,15 @@ class LLMModelsRouterGroup(group.RouterGroup):
@self.route('', methods=['GET', 'POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def _() -> str:
if quart.request.method == 'GET':
provider_uuid = quart.request.args.get('provider_uuid')
if provider_uuid:
return self.success(
data={'models': await self.ap.llm_model_service.get_llm_models_by_provider(provider_uuid)}
)
return self.success(data={'models': await self.ap.llm_model_service.get_llm_models()})
elif quart.request.method == 'POST':
json_data = await quart.request.json
model_uuid = await self.ap.llm_model_service.create_llm_model(json_data)
return self.success(data={'uuid': model_uuid})
@self.route('/<model_uuid>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
@@ -52,12 +55,19 @@ class EmbeddingModelsRouterGroup(group.RouterGroup):
@self.route('', methods=['GET', 'POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def _() -> str:
if quart.request.method == 'GET':
provider_uuid = quart.request.args.get('provider_uuid')
if provider_uuid:
return self.success(
data={
'models': await self.ap.embedding_models_service.get_embedding_models_by_provider(
provider_uuid
)
}
)
return self.success(data={'models': await self.ap.embedding_models_service.get_embedding_models()})
elif quart.request.method == 'POST':
json_data = await quart.request.json
model_uuid = await self.ap.embedding_models_service.create_embedding_model(json_data)
return self.success(data={'uuid': model_uuid})
@self.route('/<model_uuid>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)

View File

@@ -0,0 +1,45 @@
import quart
from ... import group
@group.group_class('models/providers', '/api/v1/provider/providers')
class ModelProvidersRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('', methods=['GET', 'POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def _() -> str:
if quart.request.method == 'GET':
providers = await self.ap.provider_service.get_providers()
# Add model counts
for provider in providers:
counts = await self.ap.provider_service.get_provider_model_counts(provider['uuid'])
provider['llm_count'] = counts['llm_count']
provider['embedding_count'] = counts['embedding_count']
return self.success(data={'providers': providers})
elif quart.request.method == 'POST':
json_data = await quart.request.json
provider_uuid = await self.ap.provider_service.create_provider(json_data)
return self.success(data={'uuid': provider_uuid})
@self.route(
'/<provider_uuid>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY
)
async def _(provider_uuid: str) -> str:
if quart.request.method == 'GET':
provider = await self.ap.provider_service.get_provider(provider_uuid)
if provider is None:
return self.http_status(404, -1, 'provider not found')
counts = await self.ap.provider_service.get_provider_model_counts(provider_uuid)
provider['llm_count'] = counts['llm_count']
provider['embedding_count'] = counts['embedding_count']
return self.success(data={'provider': provider})
elif quart.request.method == 'PUT':
json_data = await quart.request.json
await self.ap.provider_service.update_provider(provider_uuid, json_data)
return self.success()
elif quart.request.method == 'DELETE':
try:
await self.ap.provider_service.delete_provider(provider_uuid)
return self.success()
except ValueError as e:
return self.http_status(400, -1, str(e))

View File

@@ -0,0 +1,47 @@
import quart
from .. import group
@group.group_class('survey', '/api/v1/survey')
class SurveyRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('/pending', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def _get_pending() -> str:
"""Get pending survey for the frontend to display."""
survey = self.ap.survey.get_pending_survey() if self.ap.survey else None
return self.success(data={'survey': survey})
@self.route('/respond', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _respond() -> str:
"""Submit survey response."""
json_data = await quart.request.json
survey_id = json_data.get('survey_id')
answers = json_data.get('answers', {})
completed = json_data.get('completed', True)
if not survey_id:
return self.fail(1, 'survey_id required')
if self.ap.survey:
ok = await self.ap.survey.submit_response(survey_id, answers, completed)
if ok:
return self.success()
return self.fail(2, 'Failed to submit response')
return self.fail(3, 'Survey not available')
@self.route('/dismiss', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _dismiss() -> str:
"""Dismiss survey."""
json_data = await quart.request.json
survey_id = json_data.get('survey_id')
if not survey_id:
return self.fail(1, 'survey_id required')
if self.ap.survey:
ok = await self.ap.survey.dismiss_survey(survey_id)
if ok:
return self.success()
return self.fail(2, 'Failed to dismiss')
return self.fail(3, 'Survey not available')

View File

@@ -13,16 +13,20 @@ class SystemRouterGroup(group.RouterGroup):
data={
'version': constants.semantic_version,
'debug': constants.debug_mode,
'edition': constants.edition,
'enable_marketplace': self.ap.instance_config.data.get('plugin', {}).get(
'enable_marketplace', True
),
'cloud_service_url': (
self.ap.instance_config.data.get('plugin', {}).get(
'cloud_service_url', 'https://space.langbot.app'
)
if 'cloud_service_url' in self.ap.instance_config.data.get('plugin', {})
else 'https://space.langbot.app'
self.ap.instance_config.data.get('space', {}).get('url', 'https://space.langbot.app')
),
'allow_modify_login_info': self.ap.instance_config.data.get('system', {}).get(
'allow_modify_login_info', True
),
'disable_models_service': self.ap.instance_config.data.get('space', {}).get(
'disable_models_service', False
),
'limitation': self.ap.instance_config.data.get('system', {}).get('limitation', {}),
}
)
@@ -55,17 +59,6 @@ class SystemRouterGroup(group.RouterGroup):
return self.success(data=exec(py_code, {'ap': ap}))
@self.route('/debug/tools/call', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str:
if not constants.debug_mode:
return self.http_status(403, 403, 'Forbidden')
data = await quart.request.json
return self.success(
data=await self.ap.tool_mgr.execute_func_call(data['tool_name'], data['tool_parameters'])
)
@self.route(
'/debug/plugin/action',
methods=['POST'],

View File

@@ -1,8 +1,10 @@
import quart
import argon2
import asyncio
import traceback
from .. import group
from .....entity.errors import account as account_errors
@group.group_class('user', '/api/v1/user')
@@ -33,6 +35,8 @@ class UserRouterGroup(group.RouterGroup):
token = await self.ap.user_service.authenticate(json_data['user'], json_data['password'])
except argon2.exceptions.VerifyMismatchError:
return self.fail(1, 'Invalid username or password')
except ValueError as e:
return self.fail(1, str(e))
return self.success(data={'token': token})
@@ -70,6 +74,13 @@ class UserRouterGroup(group.RouterGroup):
@self.route('/change-password', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _(user_email: str) -> str:
# Check if password change is allowed
allow_modify_login_info = self.ap.instance_config.data.get('system', {}).get(
'allow_modify_login_info', True
)
if not allow_modify_login_info:
return self.http_status(403, -1, 'Modifying login info is disabled')
json_data = await quart.request.json
current_password = json_data['current_password']
@@ -83,3 +94,169 @@ class UserRouterGroup(group.RouterGroup):
return self.http_status(400, -1, str(e))
return self.success(data={'user': user_email})
# Space OAuth endpoints (redirect flow)
@self.route('/space/authorize-url', methods=['GET'], auth_type=group.AuthType.NONE)
async def _() -> str:
"""Get Space OAuth authorization URL for redirect"""
redirect_uri = quart.request.args.get('redirect_uri', '')
state = quart.request.args.get('state', '')
if not redirect_uri:
return self.fail(1, 'Missing redirect_uri parameter')
try:
authorize_url = self.ap.space_service.get_oauth_authorize_url(redirect_uri, state)
return self.success(data={'authorize_url': authorize_url})
except Exception as e:
return self.fail(1, str(e))
@self.route('/space/callback', methods=['POST'], auth_type=group.AuthType.NONE)
async def _() -> str:
"""Handle OAuth callback - exchange code for tokens and authenticate"""
json_data = await quart.request.json
code = json_data.get('code')
if not code:
return self.fail(1, 'Missing authorization code')
try:
# Exchange code for tokens
token_data = await self.ap.space_service.exchange_oauth_code(code)
access_token = token_data.get('access_token')
refresh_token = token_data.get('refresh_token')
expires_in = token_data.get('expires_in', 0)
if not access_token:
return self.fail(1, 'Failed to get access token from Space')
# Authenticate and create/update local user
jwt_token, user_obj = await self.ap.user_service.authenticate_space_user(
access_token, refresh_token, expires_in
)
return self.success(
data={
'token': jwt_token,
'user': user_obj.user,
}
)
except account_errors.AccountEmailMismatchError as e:
return self.fail(3, str(e))
except ValueError as e:
traceback.print_exc()
return self.fail(1, str(e))
except Exception as e:
traceback.print_exc()
return self.fail(2, f'OAuth callback failed: {str(e)}')
@self.route('/info', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def _(user_email: str) -> str:
"""Get current user information including account type"""
user_obj = await self.ap.user_service.get_user_by_email(user_email)
if user_obj is None:
return self.http_status(404, -1, 'User not found')
return self.success(
data={
'user': user_obj.user,
'account_type': user_obj.account_type,
'has_password': bool(user_obj.password and user_obj.password.strip()),
}
)
@self.route('/space-credits', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def _(user_email: str) -> str:
"""Get Space credits balance for current user"""
credits = await self.ap.space_service.get_credits(user_email)
return self.success(data={'credits': credits})
@self.route('/account-info', methods=['GET'], auth_type=group.AuthType.NONE)
async def _() -> str:
"""Get account info for login page (account type and has_password)"""
if not await self.ap.user_service.is_initialized():
return self.success(data={'initialized': False})
user_obj = await self.ap.user_service.get_first_user()
if user_obj is None:
return self.success(data={'initialized': False})
return self.success(
data={
'initialized': True,
'account_type': user_obj.account_type,
'has_password': bool(user_obj.password and user_obj.password.strip()),
}
)
@self.route('/set-password', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _(user_email: str) -> str:
"""Set password for Space account (first time) or change password"""
json_data = await quart.request.json
new_password = json_data.get('new_password')
current_password = json_data.get('current_password')
if not new_password:
return self.http_status(400, -1, 'New password is required')
user_obj = await self.ap.user_service.get_user_by_email(user_email)
if user_obj is None:
return self.http_status(404, -1, 'User not found')
try:
await self.ap.user_service.set_password(user_email, new_password, current_password)
return self.success(data={'user': user_email})
except ValueError as e:
return self.http_status(400, -1, str(e))
except argon2.exceptions.VerifyMismatchError:
return self.http_status(400, -1, 'Current password is incorrect')
@self.route('/bind-space', methods=['POST'], auth_type=group.AuthType.NONE)
async def _() -> str:
"""Bind Space account to existing local account"""
# Check if modifying login info is allowed
allow_modify_login_info = self.ap.instance_config.data.get('system', {}).get(
'allow_modify_login_info', True
)
if not allow_modify_login_info:
return self.http_status(403, -1, 'Modifying login info is disabled')
json_data = await quart.request.json
code = json_data.get('code')
state = json_data.get('state') # JWT token passed as state
if not code:
return self.http_status(400, -1, 'Missing authorization code')
if not state:
return self.http_status(400, -1, 'Missing state parameter')
# Verify state is a valid JWT token
try:
user_email = await self.ap.user_service.verify_jwt_token(state)
except Exception:
return self.http_status(401, -1, 'Invalid or expired state')
user_obj = await self.ap.user_service.get_user_by_email(user_email)
if user_obj is None:
return self.http_status(404, -1, 'User not found')
if user_obj.account_type != 'local':
return self.http_status(400, -1, 'Only local accounts can bind to Space')
try:
updated_user = await self.ap.user_service.bind_space_account(user_email, code)
jwt_token = await self.ap.user_service.generate_jwt_token(updated_user.user)
return self.success(
data={
'token': jwt_token,
'user': updated_user.user,
'account_type': updated_user.account_type,
}
)
except ValueError as e:
return self.http_status(400, -1, str(e))
except Exception as e:
return self.http_status(500, -1, f'Failed to bind Space account: {str(e)}')

View File

@@ -0,0 +1,49 @@
import quart
from .. import group
@group.group_class('webhook_mgmt', '/api/v1/webhooks')
class WebhookManagementRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('', methods=['GET', 'POST'])
async def _() -> str:
if quart.request.method == 'GET':
webhooks = await self.ap.webhook_service.get_webhooks()
return self.success(data={'webhooks': webhooks})
elif quart.request.method == 'POST':
json_data = await quart.request.json
name = json_data.get('name', '')
url = json_data.get('url', '')
description = json_data.get('description', '')
enabled = json_data.get('enabled', True)
if not name:
return self.http_status(400, -1, 'Name is required')
if not url:
return self.http_status(400, -1, 'URL is required')
webhook = await self.ap.webhook_service.create_webhook(name, url, description, enabled)
return self.success(data={'webhook': webhook})
@self.route('/<int:webhook_id>', methods=['GET', 'PUT', 'DELETE'])
async def _(webhook_id: int) -> str:
if quart.request.method == 'GET':
webhook = await self.ap.webhook_service.get_webhook(webhook_id)
if webhook is None:
return self.http_status(404, -1, 'Webhook not found')
return self.success(data={'webhook': webhook})
elif quart.request.method == 'PUT':
json_data = await quart.request.json
name = json_data.get('name')
url = json_data.get('url')
description = json_data.get('description')
enabled = json_data.get('enabled')
await self.ap.webhook_service.update_webhook(webhook_id, name, url, description, enabled)
return self.success()
elif quart.request.method == 'DELETE':
await self.ap.webhook_service.delete_webhook(webhook_id)
return self.success()

View File

@@ -1,49 +1,54 @@
from __future__ import annotations
import quart
import traceback
from .. import group
@group.group_class('webhooks', '/api/v1/webhooks')
class WebhooksRouterGroup(group.RouterGroup):
@group.group_class('webhooks', '/bots')
class WebhookRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('', methods=['GET', 'POST'])
async def _() -> str:
if quart.request.method == 'GET':
webhooks = await self.ap.webhook_service.get_webhooks()
return self.success(data={'webhooks': webhooks})
elif quart.request.method == 'POST':
json_data = await quart.request.json
name = json_data.get('name', '')
url = json_data.get('url', '')
description = json_data.get('description', '')
enabled = json_data.get('enabled', True)
@self.route('/<bot_uuid>', methods=['GET', 'POST'], auth_type=group.AuthType.NONE)
async def handle_webhook(bot_uuid: str):
"""处理 bot webhook 回调(无子路径)"""
return await self._dispatch_webhook(bot_uuid, '')
if not name:
return self.http_status(400, -1, 'Name is required')
if not url:
return self.http_status(400, -1, 'URL is required')
@self.route('/<bot_uuid>/<path:path>', methods=['GET', 'POST'], auth_type=group.AuthType.NONE)
async def handle_webhook_with_path(bot_uuid: str, path: str):
"""处理 bot webhook 回调(带子路径)"""
return await self._dispatch_webhook(bot_uuid, path)
webhook = await self.ap.webhook_service.create_webhook(name, url, description, enabled)
return self.success(data={'webhook': webhook})
async def _dispatch_webhook(self, bot_uuid: str, path: str):
"""分发 webhook 请求到对应的 bot adapter
@self.route('/<int:webhook_id>', methods=['GET', 'PUT', 'DELETE'])
async def _(webhook_id: int) -> str:
if quart.request.method == 'GET':
webhook = await self.ap.webhook_service.get_webhook(webhook_id)
if webhook is None:
return self.http_status(404, -1, 'Webhook not found')
return self.success(data={'webhook': webhook})
Args:
bot_uuid: Bot 的 UUID
path: 子路径(如果有的话)
elif quart.request.method == 'PUT':
json_data = await quart.request.json
name = json_data.get('name')
url = json_data.get('url')
description = json_data.get('description')
enabled = json_data.get('enabled')
Returns:
适配器返回的响应
"""
try:
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
await self.ap.webhook_service.update_webhook(webhook_id, name, url, description, enabled)
return self.success()
if not runtime_bot:
return quart.jsonify({'error': 'Bot not found'}), 404
elif quart.request.method == 'DELETE':
await self.ap.webhook_service.delete_webhook(webhook_id)
return self.success()
if not runtime_bot.enable:
return quart.jsonify({'error': 'Bot is disabled'}), 403
if not hasattr(runtime_bot.adapter, 'handle_unified_webhook'):
return quart.jsonify({'error': 'Adapter does not support unified webhook'}), 501
response = await runtime_bot.adapter.handle_unified_webhook(
bot_uuid=bot_uuid,
path=path,
request=quart.request,
)
return response
except Exception as e:
self.ap.logger.error(f'Webhook dispatch error for bot {bot_uuid}: {traceback.format_exc()}')
return quart.jsonify({'error': str(e)}), 500

View File

@@ -92,7 +92,11 @@ class HTTPController:
@self.quart_app.route('/')
async def index():
return await quart.send_from_directory(frontend_path, 'index.html', mimetype='text/html')
response = await quart.send_from_directory(frontend_path, 'index.html', mimetype='text/html')
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response
@self.quart_app.route('/<path:path>')
async def static_file(path: str):

View File

@@ -58,12 +58,39 @@ class BotService:
if runtime_bot is not None:
adapter_runtime_values['bot_account_id'] = runtime_bot.adapter.bot_account_id
# Webhook URL for unified webhook adapters (independent of bot running state)
if persistence_bot['adapter'] in [
'wecom',
'wecombot',
'officialaccount',
'qqofficial',
'slack',
'wecomcs',
'LINE',
'lark',
]:
webhook_prefix = self.ap.instance_config.data['api'].get('webhook_prefix', 'http://127.0.0.1:5300')
webhook_url = f'/bots/{bot_uuid}'
adapter_runtime_values['webhook_url'] = webhook_url
adapter_runtime_values['webhook_full_url'] = f'{webhook_prefix}{webhook_url}'
else:
adapter_runtime_values['webhook_url'] = None
adapter_runtime_values['webhook_full_url'] = None
persistence_bot['adapter_runtime_values'] = adapter_runtime_values
return persistence_bot
async def create_bot(self, bot_data: dict) -> str:
"""Create bot"""
# Check limitation
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
max_bots = limitation.get('max_bots', -1)
if max_bots >= 0:
existing_bots = await self.get_bots()
if len(existing_bots) >= max_bots:
raise ValueError(f'Maximum number of bots ({max_bots}) reached')
# TODO: 检查配置信息格式
bot_data['uuid'] = str(uuid.uuid4())
@@ -139,3 +166,29 @@ class BotService:
logs, total_count = await runtime_bot.logger.get_logs(from_index, max_count)
return [log.to_json() for log in logs], total_count
async def send_message(self, bot_uuid: str, target_type: str, target_id: str, message_chain_data: dict) -> None:
"""Send message to a specific target via bot
Args:
bot_uuid: The UUID of the bot
target_type: The type of the target, can be "group", "person"
target_id: The ID of the target
message_chain_data: The message chain data in dict format
"""
# Import here to avoid circular imports
import langbot_plugin.api.entities.builtin.platform.message as platform_message
# Get runtime bot
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
if runtime_bot is None:
raise Exception(f'Bot not found: {bot_uuid}')
# Validate and convert message chain
try:
message_chain = platform_message.MessageChain.model_validate(message_chain_data)
except Exception as e:
raise Exception(f'Invalid message_chain format: {str(e)}')
# Send message via adapter
await runtime_bot.adapter.send_message(target_type, str(target_id), message_chain)

View File

@@ -1,6 +1,5 @@
from __future__ import annotations
import uuid
import sqlalchemy
from ....core import app
@@ -17,70 +16,99 @@ class KnowledgeService:
async def get_knowledge_bases(self) -> list[dict]:
"""获取所有知识库"""
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_rag.KnowledgeBase))
knowledge_bases = result.all()
return [
self.ap.persistence_mgr.serialize_model(persistence_rag.KnowledgeBase, knowledge_base)
for knowledge_base in knowledge_bases
]
return await self.ap.rag_mgr.get_all_knowledge_base_details()
async def get_knowledge_base(self, kb_uuid: str) -> dict | None:
"""获取知识库"""
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_rag.KnowledgeBase).where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
)
knowledge_base = result.first()
if knowledge_base is None:
return None
return self.ap.persistence_mgr.serialize_model(persistence_rag.KnowledgeBase, knowledge_base)
return await self.ap.rag_mgr.get_knowledge_base_details(kb_uuid)
async def create_knowledge_base(self, kb_data: dict) -> str:
"""创建知识库"""
kb_data['uuid'] = str(uuid.uuid4())
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_rag.KnowledgeBase).values(kb_data))
# In new architecture, we delegate entirely to RAGManager which uses plugins.
# Legacy internal KB creation is removed.
kb = await self.get_knowledge_base(kb_data['uuid'])
knowledge_engine_plugin_id = kb_data.get('knowledge_engine_plugin_id')
if not knowledge_engine_plugin_id:
raise ValueError('knowledge_engine_plugin_id is required')
await self.ap.rag_mgr.load_knowledge_base(kb)
return kb_data['uuid']
kb = await self.ap.rag_mgr.create_knowledge_base(
name=kb_data.get('name', 'Untitled'),
knowledge_engine_plugin_id=knowledge_engine_plugin_id,
creation_settings=kb_data.get('creation_settings', {}),
retrieval_settings=kb_data.get('retrieval_settings', {}),
description=kb_data.get('description', ''),
)
return kb.uuid
async def update_knowledge_base(self, kb_uuid: str, kb_data: dict) -> None:
"""更新知识库"""
if 'uuid' in kb_data:
del kb_data['uuid']
# Filter to only mutable fields
filtered_data = {k: v for k, v in kb_data.items() if k in persistence_rag.KnowledgeBase.MUTABLE_FIELDS}
if 'embedding_model_uuid' in kb_data:
del kb_data['embedding_model_uuid']
if not filtered_data:
return
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_rag.KnowledgeBase)
.values(kb_data)
.values(filtered_data)
.where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
)
await self.ap.rag_mgr.remove_knowledge_base_from_runtime(kb_uuid)
kb = await self.get_knowledge_base(kb_uuid)
if kb is None:
raise Exception('Knowledge base not found after update')
await self.ap.rag_mgr.load_knowledge_base(kb)
async def store_file(self, kb_uuid: str, file_id: str) -> int:
async def _check_doc_capability(self, kb_uuid: str, operation: str) -> None:
"""Check if the KB's Knowledge Engine supports document operations.
Args:
kb_uuid: Knowledge base UUID.
operation: Human-readable operation name for error messages.
Raises:
Exception: If the KB does not support doc_ingestion.
"""
kb_info = await self.ap.rag_mgr.get_knowledge_base_details(kb_uuid)
if not kb_info:
raise Exception('Knowledge base not found')
capabilities = kb_info.get('knowledge_engine', {}).get('capabilities', [])
if 'doc_ingestion' not in capabilities:
raise Exception(f'This knowledge base does not support {operation}')
async def store_file(self, kb_uuid: str, file_id: str, parser_plugin_id: str | None = None) -> str:
"""存储文件"""
# await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_rag.File).values(kb_id=kb_uuid, file_id=file_id))
# await self.ap.rag_mgr.store_file(file_id)
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
if runtime_kb is None:
raise Exception('Knowledge base not found')
return await runtime_kb.store_file(file_id)
async def retrieve_knowledge_base(self, kb_uuid: str, query: str) -> list[dict]:
await self._check_doc_capability(kb_uuid, 'document upload')
result = await runtime_kb.store_file(file_id, parser_plugin_id=parser_plugin_id)
# Update the KB's updated_at timestamp
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_rag.KnowledgeBase)
.values(updated_at=sqlalchemy.func.now())
.where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
)
return result
async def retrieve_knowledge_base(
self, kb_uuid: str, query: str, retrieval_settings: dict | None = None
) -> list[dict]:
"""检索知识库"""
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
if runtime_kb is None:
raise Exception('Knowledge base not found')
return [
result.model_dump() for result in await runtime_kb.retrieve(query, runtime_kb.knowledge_base_entity.top_k)
]
# Pass retrieval_settings
results = await runtime_kb.retrieve(query, settings=retrieval_settings)
return [result.model_dump() for result in results]
async def get_files_by_knowledge_base(self, kb_uuid: str) -> list[dict]:
"""获取知识库文件"""
@@ -95,17 +123,28 @@ class KnowledgeService:
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
if runtime_kb is None:
raise Exception('Knowledge base not found')
await self._check_doc_capability(kb_uuid, 'document deletion')
await runtime_kb.delete_file(file_id)
# Update the KB's updated_at timestamp
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_rag.KnowledgeBase)
.values(updated_at=sqlalchemy.func.now())
.where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
)
async def delete_knowledge_base(self, kb_uuid: str) -> None:
"""删除知识库"""
await self.ap.rag_mgr.delete_knowledge_base(kb_uuid)
# Delete from DB first to commit the deletion, then clean up runtime/plugin (best-effort)
await self.ap.persistence_mgr.execute_async(
sqlalchemy.delete(persistence_rag.KnowledgeBase).where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
)
# delete files
# NOTE: Chunk cleanup is for legacy (pre-plugin) KBs that stored chunks locally.
# For plugin-based Knowledge Engines, the Chunk table is not populated, so this is a no-op.
files = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_rag.File).where(persistence_rag.File.kb_id == kb_uuid)
)
@@ -118,3 +157,53 @@ class KnowledgeService:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.delete(persistence_rag.File).where(persistence_rag.File.uuid == file.uuid)
)
# Remove from runtime and notify plugin (best-effort, DB is already cleaned up)
await self.ap.rag_mgr.delete_knowledge_base(kb_uuid)
# ================= Knowledge Engine Discovery =================
async def list_knowledge_engines(self) -> list[dict]:
"""List all available Knowledge Engines from plugins."""
engines = []
if not self.ap.plugin_connector.is_enable_plugin:
return engines
# Get KnowledgeEngine plugins
try:
knowledge_engines = await self.ap.plugin_connector.list_knowledge_engines()
engines.extend(knowledge_engines)
except Exception as e:
self.ap.logger.warning(f'Failed to list Knowledge Engines from plugins: {e}')
return engines
async def list_parsers(self, mime_type: str | None = None) -> list[dict]:
"""List available parsers, optionally filtered by MIME type."""
if not self.ap.plugin_connector.is_enable_plugin:
return []
try:
parsers = await self.ap.plugin_connector.list_parsers()
if mime_type:
parsers = [p for p in parsers if mime_type in p.get('supported_mime_types', [])]
return parsers
except Exception as e:
self.ap.logger.warning(f'Failed to list parsers: {e}')
return []
async def get_engine_creation_schema(self, plugin_id: str) -> dict:
"""Get creation settings schema for a specific Knowledge Engine."""
try:
return await self.ap.plugin_connector.get_rag_creation_schema(plugin_id)
except Exception as e:
self.ap.logger.warning(f'Failed to get creation schema for {plugin_id}: {e}')
return {}
async def get_engine_retrieval_schema(self, plugin_id: str) -> dict:
"""Get retrieval settings schema for a specific Knowledge Engine."""
try:
return await self.ap.plugin_connector.get_rag_retrieval_schema(plugin_id)
except Exception as e:
self.ap.logger.warning(f'Failed to get retrieval schema for {plugin_id}: {e}')
return {}

View File

@@ -38,6 +38,16 @@ class MCPService:
return serialized_servers
async def create_mcp_server(self, server_data: dict) -> str:
# Check limitation (extensions = MCP servers + plugins)
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
max_extensions = limitation.get('max_extensions', -1)
if max_extensions >= 0:
existing_mcp_servers = await self.get_mcp_servers()
plugins = await self.ap.plugin_connector.list_plugins()
total_extensions = len(existing_mcp_servers) + len(plugins)
if total_extensions >= max_extensions:
raise ValueError(f'Maximum number of extensions ({max_extensions}) reached')
server_data['uuid'] = str(uuid.uuid4())
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_mcp.MCPServer).values(server_data))

View File

@@ -11,6 +11,18 @@ from ....entity.persistence import pipeline as persistence_pipeline
from ....provider.modelmgr import requester as model_requester
def _parse_provider_api_keys(provider_dict: dict) -> dict:
"""Parse api_keys if it's a JSON string"""
if isinstance(provider_dict.get('api_keys'), str):
import json
try:
provider_dict['api_keys'] = json.loads(provider_dict['api_keys'])
except Exception:
provider_dict['api_keys'] = []
return provider_dict
class LLMModelsService:
ap: app.Application
@@ -18,59 +30,131 @@ class LLMModelsService:
self.ap = ap
async def get_llm_models(self, include_secret: bool = True) -> list[dict]:
"""Get all LLM models with provider info"""
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.LLMModel))
models = result.all()
masked_columns = []
if not include_secret:
masked_columns = ['api_keys']
# Get all providers for lookup
providers_result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_model.ModelProvider)
)
providers = {p.uuid: p for p in providers_result.all()}
return [
self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model, masked_columns)
for model in models
]
models_list = []
for model in models:
model_dict = self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model)
provider = providers.get(model.provider_uuid)
if provider:
provider_dict = self.ap.persistence_mgr.serialize_model(persistence_model.ModelProvider, provider)
provider_dict = _parse_provider_api_keys(provider_dict)
if not include_secret:
provider_dict['api_keys'] = ['***'] * len(provider_dict.get('api_keys', []))
model_dict['provider'] = provider_dict
models_list.append(model_dict)
async def create_llm_model(self, model_data: dict) -> str:
model_data['uuid'] = str(uuid.uuid4())
return models_list
async def get_llm_models_by_provider(self, provider_uuid: str) -> list[dict]:
"""Get LLM models by provider UUID"""
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_model.LLMModel).where(
persistence_model.LLMModel.provider_uuid == provider_uuid
)
)
models = result.all()
return [self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, m) for m in models]
async def create_llm_model(
self, model_data: dict, preserve_uuid: bool = False, auto_set_to_default_pipeline: bool = True
) -> str:
"""Create a new LLM model"""
if not preserve_uuid:
model_data['uuid'] = str(uuid.uuid4())
# Handle provider creation if needed
if 'provider' in model_data:
provider_data = model_data.pop('provider')
if provider_data.get('uuid'):
model_data['provider_uuid'] = provider_data['uuid']
else:
# Create new provider
provider_uuid = await self.ap.provider_service.find_or_create_provider(
requester=provider_data.get('requester', ''),
base_url=provider_data.get('base_url', ''),
api_keys=provider_data.get('api_keys', []),
)
model_data['provider_uuid'] = provider_uuid
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_model.LLMModel).values(**model_data))
llm_model = await self.get_llm_model(model_data['uuid'])
runtime_provider = self.ap.model_mgr.provider_dict.get(model_data['provider_uuid'])
if runtime_provider is None:
raise Exception('provider not found')
await self.ap.model_mgr.load_llm_model(llm_model)
# check if default pipeline has no model bound
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
persistence_pipeline.LegacyPipeline.is_default == True
)
runtime_llm_model = await self.ap.model_mgr.load_llm_model_with_provider(
persistence_model.LLMModel(**model_data),
runtime_provider,
)
pipeline = result.first()
if pipeline is not None and pipeline.config['ai']['local-agent']['model'] == '':
pipeline_config = pipeline.config
pipeline_config['ai']['local-agent']['model'] = model_data['uuid']
pipeline_data = {'config': pipeline_config}
await self.ap.pipeline_service.update_pipeline(pipeline.uuid, pipeline_data)
self.ap.model_mgr.llm_models.append(runtime_llm_model)
if auto_set_to_default_pipeline:
# set the default pipeline model to this model
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
persistence_pipeline.LegacyPipeline.is_default == True
)
)
pipeline = result.first()
if pipeline is not None and pipeline.config['ai']['local-agent']['model'] == '':
pipeline_config = pipeline.config
pipeline_config['ai']['local-agent']['model'] = model_data['uuid']
pipeline_data = {'config': pipeline_config}
await self.ap.pipeline_service.update_pipeline(pipeline.uuid, pipeline_data)
return model_data['uuid']
async def get_llm_model(self, model_uuid: str) -> dict | None:
"""Get a single LLM model with provider info"""
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_model.LLMModel).where(persistence_model.LLMModel.uuid == model_uuid)
)
model = result.first()
if model is None:
return None
return self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model)
model_dict = self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model)
# Get provider
provider_result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_model.ModelProvider).where(
persistence_model.ModelProvider.uuid == model.provider_uuid
)
)
provider = provider_result.first()
if provider:
provider_dict = self.ap.persistence_mgr.serialize_model(persistence_model.ModelProvider, provider)
model_dict['provider'] = _parse_provider_api_keys(provider_dict)
return model_dict
async def update_llm_model(self, model_uuid: str, model_data: dict) -> None:
"""Update an existing LLM model"""
if 'uuid' in model_data:
del model_data['uuid']
# Handle provider update if needed
if 'provider' in model_data:
provider_data = model_data.pop('provider')
if provider_data.get('uuid'):
model_data['provider_uuid'] = provider_data['uuid']
else:
provider_uuid = await self.ap.provider_service.find_or_create_provider(
requester=provider_data.get('requester', ''),
base_url=provider_data.get('base_url', ''),
api_keys=provider_data.get('api_keys', []),
)
model_data['provider_uuid'] = provider_uuid
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_model.LLMModel)
.where(persistence_model.LLMModel.uuid == model_uuid)
@@ -79,18 +163,25 @@ class LLMModelsService:
await self.ap.model_mgr.remove_llm_model(model_uuid)
llm_model = await self.get_llm_model(model_uuid)
runtime_provider = self.ap.model_mgr.provider_dict.get(model_data['provider_uuid'])
if runtime_provider is None:
raise Exception('provider not found')
await self.ap.model_mgr.load_llm_model(llm_model)
runtime_llm_model = await self.ap.model_mgr.load_llm_model_with_provider(
persistence_model.LLMModel(**model_data),
runtime_provider,
)
self.ap.model_mgr.llm_models.append(runtime_llm_model)
async def delete_llm_model(self, model_uuid: str) -> None:
"""Delete an LLM model"""
await self.ap.persistence_mgr.execute_async(
sqlalchemy.delete(persistence_model.LLMModel).where(persistence_model.LLMModel.uuid == model_uuid)
)
await self.ap.model_mgr.remove_llm_model(model_uuid)
async def test_llm_model(self, model_uuid: str, model_data: dict) -> None:
"""Test an LLM model"""
runtime_llm_model: model_requester.RuntimeLLMModel | None = None
if model_uuid != '_':
@@ -98,25 +189,18 @@ class LLMModelsService:
if model.model_entity.uuid == model_uuid:
runtime_llm_model = model
break
if runtime_llm_model is None:
raise Exception('model not found')
else:
runtime_llm_model = await self.ap.model_mgr.init_runtime_llm_model(model_data)
runtime_llm_model = await self.ap.model_mgr.init_temporary_runtime_llm_model(model_data)
# Mon Nov 10 2025: Commented for some providers may not support thinking parameter
# # 有些模型厂商默认开启了思考功能,测试容易延迟
# extra_args = model_data.get('extra_args', {})
# if not extra_args or 'thinking' not in extra_args:
# extra_args['thinking'] = {'type': 'disabled'}
await runtime_llm_model.requester.invoke_llm(
extra_args = model_data.get('extra_args', {})
await runtime_llm_model.provider.invoke_llm(
query=None,
model=runtime_llm_model,
messages=[provider_message.Message(role='user', content='Hello, world! Please just reply a "Hello".')],
funcs=[],
# extra_args=extra_args,
extra_args=extra_args,
)
@@ -127,42 +211,111 @@ class EmbeddingModelsService:
self.ap = ap
async def get_embedding_models(self) -> list[dict]:
"""Get all embedding models with provider info"""
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.EmbeddingModel))
models = result.all()
return [self.ap.persistence_mgr.serialize_model(persistence_model.EmbeddingModel, model) for model in models]
async def create_embedding_model(self, model_data: dict) -> str:
model_data['uuid'] = str(uuid.uuid4())
providers_result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_model.ModelProvider)
)
providers = {p.uuid: p for p in providers_result.all()}
models_list = []
for model in models:
model_dict = self.ap.persistence_mgr.serialize_model(persistence_model.EmbeddingModel, model)
provider = providers.get(model.provider_uuid)
if provider:
provider_dict = self.ap.persistence_mgr.serialize_model(persistence_model.ModelProvider, provider)
model_dict['provider'] = _parse_provider_api_keys(provider_dict)
models_list.append(model_dict)
return models_list
async def get_embedding_models_by_provider(self, provider_uuid: str) -> list[dict]:
"""Get embedding models by provider UUID"""
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_model.EmbeddingModel).where(
persistence_model.EmbeddingModel.provider_uuid == provider_uuid
)
)
models = result.all()
return [self.ap.persistence_mgr.serialize_model(persistence_model.EmbeddingModel, m) for m in models]
async def create_embedding_model(self, model_data: dict, preserve_uuid: bool = False) -> str:
"""Create a new embedding model"""
if not preserve_uuid:
model_data['uuid'] = str(uuid.uuid4())
if 'provider' in model_data:
provider_data = model_data.pop('provider')
if provider_data.get('uuid'):
model_data['provider_uuid'] = provider_data['uuid']
else:
provider_uuid = await self.ap.provider_service.find_or_create_provider(
requester=provider_data.get('requester', ''),
base_url=provider_data.get('base_url', ''),
api_keys=provider_data.get('api_keys', []),
)
model_data['provider_uuid'] = provider_uuid
await self.ap.persistence_mgr.execute_async(
sqlalchemy.insert(persistence_model.EmbeddingModel).values(**model_data)
)
embedding_model = await self.get_embedding_model(model_data['uuid'])
runtime_provider = self.ap.model_mgr.provider_dict.get(model_data['provider_uuid'])
if runtime_provider is None:
raise Exception('provider not found')
await self.ap.model_mgr.load_embedding_model(embedding_model)
runtime_embedding_model = await self.ap.model_mgr.load_embedding_model_with_provider(
persistence_model.EmbeddingModel(**model_data),
runtime_provider,
)
self.ap.model_mgr.embedding_models.append(runtime_embedding_model)
return model_data['uuid']
async def get_embedding_model(self, model_uuid: str) -> dict | None:
"""Get a single embedding model with provider info"""
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_model.EmbeddingModel).where(
persistence_model.EmbeddingModel.uuid == model_uuid
)
)
model = result.first()
if model is None:
return None
return self.ap.persistence_mgr.serialize_model(persistence_model.EmbeddingModel, model)
model_dict = self.ap.persistence_mgr.serialize_model(persistence_model.EmbeddingModel, model)
provider_result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_model.ModelProvider).where(
persistence_model.ModelProvider.uuid == model.provider_uuid
)
)
provider = provider_result.first()
if provider:
provider_dict = self.ap.persistence_mgr.serialize_model(persistence_model.ModelProvider, provider)
model_dict['provider'] = _parse_provider_api_keys(provider_dict)
return model_dict
async def update_embedding_model(self, model_uuid: str, model_data: dict) -> None:
"""Update an existing embedding model"""
if 'uuid' in model_data:
del model_data['uuid']
if 'provider' in model_data:
provider_data = model_data.pop('provider')
if provider_data.get('uuid'):
model_data['provider_uuid'] = provider_data['uuid']
else:
provider_uuid = await self.ap.provider_service.find_or_create_provider(
requester=provider_data.get('requester', ''),
base_url=provider_data.get('base_url', ''),
api_keys=provider_data.get('api_keys', []),
)
model_data['provider_uuid'] = provider_uuid
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_model.EmbeddingModel)
.where(persistence_model.EmbeddingModel.uuid == model_uuid)
@@ -171,20 +324,27 @@ class EmbeddingModelsService:
await self.ap.model_mgr.remove_embedding_model(model_uuid)
embedding_model = await self.get_embedding_model(model_uuid)
runtime_provider = self.ap.model_mgr.provider_dict.get(model_data['provider_uuid'])
if runtime_provider is None:
raise Exception('provider not found')
await self.ap.model_mgr.load_embedding_model(embedding_model)
runtime_embedding_model = await self.ap.model_mgr.load_embedding_model_with_provider(
persistence_model.EmbeddingModel(**model_data),
runtime_provider,
)
self.ap.model_mgr.embedding_models.append(runtime_embedding_model)
async def delete_embedding_model(self, model_uuid: str) -> None:
"""Delete an embedding model"""
await self.ap.persistence_mgr.execute_async(
sqlalchemy.delete(persistence_model.EmbeddingModel).where(
persistence_model.EmbeddingModel.uuid == model_uuid
)
)
await self.ap.model_mgr.remove_embedding_model(model_uuid)
async def test_embedding_model(self, model_uuid: str, model_data: dict) -> None:
"""Test an embedding model"""
runtime_embedding_model: model_requester.RuntimeEmbeddingModel | None = None
if model_uuid != '_':
@@ -192,14 +352,12 @@ class EmbeddingModelsService:
if model.model_entity.uuid == model_uuid:
runtime_embedding_model = model
break
if runtime_embedding_model is None:
raise Exception('model not found')
else:
runtime_embedding_model = await self.ap.model_mgr.init_runtime_embedding_model(model_data)
runtime_embedding_model = await self.ap.model_mgr.init_temporary_runtime_embedding_model(model_data)
await runtime_embedding_model.requester.invoke_embedding(
await runtime_embedding_model.provider.invoke_embedding(
model=runtime_embedding_model,
input_text=['Hello, world!'],
extra_args={},

File diff suppressed because it is too large Load Diff

View File

@@ -76,6 +76,14 @@ class PipelineService:
async def create_pipeline(self, pipeline_data: dict, default: bool = False) -> str:
from ....utils import paths as path_utils
# Check limitation
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
max_pipelines = limitation.get('max_pipelines', -1)
if max_pipelines >= 0:
existing_pipelines = await self.get_pipelines()
if len(existing_pipelines) >= max_pipelines:
raise ValueError(f'Maximum number of pipelines ({max_pipelines}) reached')
pipeline_data['uuid'] = str(uuid.uuid4())
pipeline_data['for_version'] = self.ap.ver_mgr.get_current_version()
pipeline_data['stages'] = default_stage_order.copy()
@@ -85,6 +93,15 @@ class PipelineService:
with open(template_path, 'r', encoding='utf-8') as f:
pipeline_data['config'] = json.load(f)
# Ensure extensions_preferences is set with enable_all_plugins and enable_all_mcp_servers=True by default
if 'extensions_preferences' not in pipeline_data:
pipeline_data['extensions_preferences'] = {
'enable_all_plugins': True,
'enable_all_mcp_servers': True,
'plugins': [],
'mcp_servers': [],
}
await self.ap.persistence_mgr.execute_async(
sqlalchemy.insert(persistence_pipeline.LegacyPipeline).values(**pipeline_data)
)
@@ -142,8 +159,67 @@ class PipelineService:
)
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
async def copy_pipeline(self, pipeline_uuid: str) -> str:
"""Copy a pipeline with all its configurations"""
# Check limitation
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
max_pipelines = limitation.get('max_pipelines', -1)
if max_pipelines >= 0:
existing_pipelines = await self.get_pipelines()
if len(existing_pipelines) >= max_pipelines:
raise ValueError(f'Maximum number of pipelines ({max_pipelines}) reached')
# Get the original pipeline
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid
)
)
original_pipeline = result.first()
if original_pipeline is None:
raise ValueError(f'Pipeline {pipeline_uuid} not found')
# Create new pipeline data
new_uuid = str(uuid.uuid4())
new_pipeline_data = {
'uuid': new_uuid,
'name': f'{original_pipeline.name} (Copy)',
'description': original_pipeline.description,
'for_version': self.ap.ver_mgr.get_current_version(),
'stages': original_pipeline.stages.copy() if original_pipeline.stages else default_stage_order.copy(),
'config': original_pipeline.config.copy() if original_pipeline.config else {},
'is_default': False,
'extensions_preferences': (
original_pipeline.extensions_preferences.copy()
if original_pipeline.extensions_preferences
else {
'enable_all_plugins': True,
'enable_all_mcp_servers': True,
'plugins': [],
'mcp_servers': [],
}
),
}
# Insert the new pipeline
await self.ap.persistence_mgr.execute_async(
sqlalchemy.insert(persistence_pipeline.LegacyPipeline).values(**new_pipeline_data)
)
# Load the new pipeline
pipeline = await self.get_pipeline(new_uuid)
await self.ap.pipeline_mgr.load_pipeline(pipeline)
return new_uuid
async def update_pipeline_extensions(
self, pipeline_uuid: str, bound_plugins: list[dict], bound_mcp_servers: list[str] = None
self,
pipeline_uuid: str,
bound_plugins: list[dict],
bound_mcp_servers: list[str] = None,
enable_all_plugins: bool = True,
enable_all_mcp_servers: bool = True,
) -> None:
"""Update the bound plugins and MCP servers for a pipeline"""
# Get current pipeline
@@ -159,6 +235,8 @@ class PipelineService:
# Update extensions_preferences
extensions_preferences = pipeline.extensions_preferences or {}
extensions_preferences['enable_all_plugins'] = enable_all_plugins
extensions_preferences['enable_all_mcp_servers'] = enable_all_mcp_servers
extensions_preferences['plugins'] = bound_plugins
if bound_mcp_servers is not None:
extensions_preferences['mcp_servers'] = bound_mcp_servers

View File

@@ -0,0 +1,166 @@
from __future__ import annotations
import uuid
import sqlalchemy
from ....core import app
from ....entity.persistence import model as persistence_model
class ModelProviderService:
"""Service for managing model providers"""
ap: app.Application
def __init__(self, ap: app.Application) -> None:
self.ap = ap
async def get_providers(self) -> list[dict]:
"""Get all providers"""
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.ModelProvider))
providers = result.all()
providers_list = []
for p in providers:
provider_dict = self.ap.persistence_mgr.serialize_model(persistence_model.ModelProvider, p)
# Parse api_keys if it's a JSON string
if isinstance(provider_dict.get('api_keys'), str):
import json
try:
provider_dict['api_keys'] = json.loads(provider_dict['api_keys'])
except Exception:
provider_dict['api_keys'] = []
providers_list.append(provider_dict)
return providers_list
async def get_provider(self, provider_uuid: str) -> dict | None:
"""Get a single provider by UUID"""
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_model.ModelProvider).where(
persistence_model.ModelProvider.uuid == provider_uuid
)
)
provider = result.first()
if provider is None:
return None
provider_dict = self.ap.persistence_mgr.serialize_model(persistence_model.ModelProvider, provider)
# Parse api_keys if it's a JSON string
if isinstance(provider_dict.get('api_keys'), str):
import json
try:
provider_dict['api_keys'] = json.loads(provider_dict['api_keys'])
except Exception:
provider_dict['api_keys'] = []
return provider_dict
async def create_provider(self, provider_data: dict) -> str:
"""Create a new provider"""
provider_data['uuid'] = str(uuid.uuid4())
await self.ap.persistence_mgr.execute_async(
sqlalchemy.insert(persistence_model.ModelProvider).values(**provider_data)
)
# load to runtime
runtime_provider = await self.ap.model_mgr.load_provider(provider_data)
self.ap.model_mgr.provider_dict[runtime_provider.provider_entity.uuid] = runtime_provider
return provider_data['uuid']
async def update_provider(self, provider_uuid: str, provider_data: dict) -> None:
"""Update an existing provider"""
if 'uuid' in provider_data:
del provider_data['uuid']
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_model.ModelProvider)
.where(persistence_model.ModelProvider.uuid == provider_uuid)
.values(**provider_data)
)
await self.ap.model_mgr.reload_provider(provider_uuid)
async def delete_provider(self, provider_uuid: str) -> None:
"""Delete a provider (only if no models reference it)"""
# Check if any models use this provider
llm_result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_model.LLMModel).where(
persistence_model.LLMModel.provider_uuid == provider_uuid
)
)
if llm_result.first() is not None:
raise ValueError('Cannot delete provider: LLM models still reference it')
embedding_result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_model.EmbeddingModel).where(
persistence_model.EmbeddingModel.provider_uuid == provider_uuid
)
)
if embedding_result.first() is not None:
raise ValueError('Cannot delete provider: Embedding models still reference it')
await self.ap.persistence_mgr.execute_async(
sqlalchemy.delete(persistence_model.ModelProvider).where(
persistence_model.ModelProvider.uuid == provider_uuid
)
)
await self.ap.model_mgr.remove_provider(provider_uuid)
async def get_provider_model_counts(self, provider_uuid: str) -> dict:
"""Get count of models using this provider"""
llm_result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(sqlalchemy.func.count())
.select_from(persistence_model.LLMModel)
.where(persistence_model.LLMModel.provider_uuid == provider_uuid)
)
llm_count = llm_result.scalar() or 0
embedding_result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(sqlalchemy.func.count())
.select_from(persistence_model.EmbeddingModel)
.where(persistence_model.EmbeddingModel.provider_uuid == provider_uuid)
)
embedding_count = embedding_result.scalar() or 0
return {'llm_count': llm_count, 'embedding_count': embedding_count}
async def find_or_create_provider(self, requester: str, base_url: str, api_keys: list) -> str:
"""Find existing provider or create new one"""
# Try to find existing provider with same config
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_model.ModelProvider).where(
persistence_model.ModelProvider.requester == requester,
persistence_model.ModelProvider.base_url == base_url,
)
)
for provider in result.all():
if sorted(provider.api_keys or []) == sorted(api_keys or []):
return provider.uuid
# Create new provider
provider_name = requester
if base_url:
try:
from urllib.parse import urlparse
parsed = urlparse(base_url)
provider_name = parsed.netloc or requester
except Exception:
pass
return await self.create_provider(
{
'name': provider_name,
'requester': requester,
'base_url': base_url,
'api_keys': api_keys or [],
}
)
async def update_space_model_provider_api_keys(self, api_key: str) -> None:
"""Update Space model provider API keys"""
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_model.ModelProvider)
.where(persistence_model.ModelProvider.uuid == '00000000-0000-0000-0000-000000000000')
.values(api_keys=[api_key])
)
await self.ap.model_mgr.reload_provider('00000000-0000-0000-0000-000000000000')

View File

@@ -0,0 +1,189 @@
from __future__ import annotations
from langbot.pkg.utils import httpclient
import typing
import datetime
import time
import sqlalchemy
from ....core import app
from ....entity.persistence import user
from ....entity.dto.space_model import SpaceModel
class SpaceService:
"""Service for interacting with LangBot Space API"""
ap: app.Application
_credits_cache: typing.Dict[str, typing.Tuple[int, float]] # {user_email: (credits, timestamp)}
def __init__(self, ap: app.Application) -> None:
self.ap = ap
self._credits_cache = {}
def _get_space_config(self) -> typing.Dict[str, str]:
"""Get Space configuration from config file"""
space_config = self.ap.instance_config.data.get('space', {})
return {
'url': space_config.get('url', 'https://space.langbot.app'),
'oauth_authorize_url': space_config.get('oauth_authorize_url', 'https://space.langbot.app/auth/authorize'),
}
async def _get_user_by_email(self, user_email: str) -> user.User | None:
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(user.User).where(user.User.user == user_email)
)
result_list = result.all()
return result_list[0] if result_list else None
async def _ensure_valid_token(self, user_email: str) -> str | None:
"""Ensure access token is valid, refresh if expired. Returns valid access_token or None."""
user_obj = await self._get_user_by_email(user_email)
if not user_obj or user_obj.account_type != 'space':
return None
if not user_obj.space_access_token:
return None
# Check if token is expired (with 60s buffer)
if user_obj.space_access_token_expires_at:
if datetime.datetime.now() >= user_obj.space_access_token_expires_at - datetime.timedelta(seconds=60):
# Token expired, try to refresh
if user_obj.space_refresh_token:
try:
new_token = await self._refresh_and_save_token(user_obj)
return new_token
except Exception:
return None
return None
return user_obj.space_access_token
async def _refresh_and_save_token(self, user_obj: user.User) -> str:
"""Refresh token and save to database"""
token_data = await self.refresh_token(user_obj.space_refresh_token)
access_token = token_data.get('access_token')
expires_in = token_data.get('expires_in', 0)
if not access_token:
raise ValueError('Failed to refresh token')
expires_at = datetime.datetime.now() + datetime.timedelta(seconds=expires_in) if expires_in > 0 else None
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(user.User)
.where(user.User.user == user_obj.user)
.values(
space_access_token=access_token,
space_access_token_expires_at=expires_at,
)
)
return access_token
# === Raw API calls (no token validation) ===
def get_oauth_authorize_url(self, redirect_uri: str, state: str = '') -> str:
"""Get the Space OAuth authorization URL for redirect"""
space_config = self._get_space_config()
authorize_url = space_config['oauth_authorize_url']
params = f'redirect_uri={redirect_uri}'
if state:
params += f'&state={state}'
return f'{authorize_url}?{params}'
async def exchange_oauth_code(self, code: str) -> typing.Dict:
"""Exchange OAuth authorization code for tokens"""
from langbot.pkg.utils import constants
space_config = self._get_space_config()
space_url = space_config['url']
session = httpclient.get_session()
async with session.post(
f'{space_url}/api/v1/accounts/oauth/token',
json={'code': code, 'instance_id': constants.instance_id},
) as response:
if response.status != 200:
raise ValueError(f'Failed to exchange OAuth code: {await response.text()}')
data = await response.json()
if data.get('code') != 0:
raise ValueError(f'Failed to exchange OAuth code: {data.get("msg")}')
return data.get('data', {})
async def refresh_token(self, refresh_token: str) -> typing.Dict:
"""Refresh Space access token"""
space_config = self._get_space_config()
space_url = space_config['url']
session = httpclient.get_session()
async with session.post(
f'{space_url}/api/v1/accounts/token/refresh', json={'refresh_token': refresh_token}
) as response:
if response.status != 200:
raise ValueError(f'Failed to refresh token: {await response.text()}')
data = await response.json()
if data.get('code') != 0:
raise ValueError(f'Failed to refresh token: {data.get("msg")}')
return data.get('data', {})
async def get_user_info_raw(self, access_token: str) -> typing.Dict:
"""Get user info from Space using access token (no validation)"""
space_config = self._get_space_config()
space_url = space_config['url']
session = httpclient.get_session()
async with session.get(
f'{space_url}/api/v1/accounts/me', headers={'Authorization': f'Bearer {access_token}'}
) as response:
if response.status != 200:
raise ValueError(f'Failed to get user info: {await response.text()}')
data = await response.json()
if data.get('code') != 0:
raise ValueError(f'Failed to get user info: {data.get("msg")}')
return data.get('data', {})
# === API calls with token validation ===
async def get_user_info(self, user_email: str) -> typing.Dict | None:
"""Get user info from Space (with token validation)"""
access_token = await self._ensure_valid_token(user_email)
if not access_token:
return None
return await self.get_user_info_raw(access_token)
async def get_credits(self, user_email: str, force_refresh: bool = False) -> int | None:
"""Get Space credits for user with caching (60s TTL)"""
cache_ttl = 60
if not force_refresh and user_email in self._credits_cache:
credits, ts = self._credits_cache[user_email]
if time.time() - ts < cache_ttl:
return credits
try:
info = await self.get_user_info(user_email)
if info is None:
return None
credits = info.get('credits')
if credits is not None:
self._credits_cache[user_email] = (credits, time.time())
return credits
except Exception:
return self._credits_cache.get(user_email, (None, 0))[0]
async def get_models(self) -> typing.List[SpaceModel]:
"""Get models from Space"""
space_config = self._get_space_config()
space_url = space_config['url']
session = httpclient.get_session()
async with session.get(f'{space_url}/api/v1/models') as response:
if response.status != 200:
raise ValueError(f'Failed to get models: {await response.text()}')
data = await response.json()
if data.get('code') != 0:
raise ValueError(f'Failed to get models: {data.get("msg")}')
models_data = data.get('data', {}).get('models', [])
return [SpaceModel.model_validate(model_dict) for model_dict in models_data]

View File

@@ -4,17 +4,22 @@ import sqlalchemy
import argon2
import jwt
import datetime
import typing
import asyncio
from ....core import app
from ....entity.persistence import user
from ....utils import constants
from ....entity.errors import account as account_errors
class UserService:
ap: app.Application
_create_user_lock: asyncio.Lock
def __init__(self, ap: app.Application) -> None:
self.ap = ap
self._create_user_lock = asyncio.Lock()
async def is_initialized(self) -> bool:
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(user.User).limit(1))
@@ -28,7 +33,7 @@ class UserService:
hashed_password = ph.hash(password)
await self.ap.persistence_mgr.execute_async(
sqlalchemy.insert(user.User).values(user=user_email, password=hashed_password)
sqlalchemy.insert(user.User).values(user=user_email, password=hashed_password, account_type='local')
)
async def get_user_by_email(self, user_email: str) -> user.User | None:
@@ -39,6 +44,15 @@ class UserService:
result_list = result.all()
return result_list[0] if result_list is not None and len(result_list) > 0 else None
async def get_user_by_space_account_uuid(self, space_account_uuid: str) -> user.User | None:
"""Get user by Space account UUID"""
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(user.User).where(user.User.space_account_uuid == space_account_uuid)
)
result_list = result.all()
return result_list[0] if result_list is not None and len(result_list) > 0 else None
async def authenticate(self, user_email: str, password: str) -> str | None:
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(user.User).where(user.User.user == user_email)
@@ -51,6 +65,10 @@ class UserService:
user_obj = result_list[0]
# Check if this is a Space account
if user_obj.account_type == 'space':
raise ValueError('请使用 Space 账户登录')
ph = argon2.PasswordHasher()
ph.verify(user_obj.password, password)
@@ -90,6 +108,10 @@ class UserService:
if user_obj is None:
raise ValueError('User not found')
# Space accounts cannot change password locally
if user_obj.account_type == 'space':
raise ValueError('Space account cannot change password locally')
ph.verify(user_obj.password, current_password)
hashed_password = ph.hash(new_password)
@@ -97,3 +119,183 @@ class UserService:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
)
# Space user management
async def create_or_update_space_user(
self,
space_account_uuid: str,
email: str,
access_token: str,
refresh_token: str,
api_key: str,
expires_in: int = 0,
) -> user.User:
"""Create or update a Space user account (only if system not initialized or user exists)"""
expires_at = datetime.datetime.now() + datetime.timedelta(seconds=expires_in) if expires_in > 0 else None
async with self._create_user_lock:
# Check if user with this Space UUID already exists
existing_user = await self.get_user_by_space_account_uuid(space_account_uuid)
if existing_user:
# Update existing user's tokens
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(user.User)
.where(user.User.space_account_uuid == space_account_uuid)
.values(
space_access_token=access_token,
space_refresh_token=refresh_token,
space_api_key=api_key,
space_access_token_expires_at=expires_at,
)
)
await self.ap.provider_service.update_space_model_provider_api_keys(api_key)
return await self.get_user_by_space_account_uuid(space_account_uuid)
# Check if user with same email exists
existing_email_user = await self.get_user_by_email(email)
if existing_email_user:
# Update existing user to link with Space account
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(user.User)
.where(user.User.user == email)
.values(
account_type='space',
space_account_uuid=space_account_uuid,
space_access_token=access_token,
space_refresh_token=refresh_token,
space_api_key=api_key,
space_access_token_expires_at=expires_at,
)
)
await self.ap.provider_service.update_space_model_provider_api_keys(api_key)
return await self.get_user_by_email(email)
# Check if system is already initialized
is_initialized = await self.is_initialized()
if is_initialized:
raise account_errors.AccountEmailMismatchError()
# Create new Space user (first time initialization)
await self.ap.persistence_mgr.execute_async(
sqlalchemy.insert(user.User).values(
user=email,
password='', # Space users don't have local password
account_type='space',
space_account_uuid=space_account_uuid,
space_access_token=access_token,
space_refresh_token=refresh_token,
space_api_key=api_key,
space_access_token_expires_at=expires_at,
)
)
await self.ap.provider_service.update_space_model_provider_api_keys(api_key)
return await self.get_user_by_space_account_uuid(space_account_uuid)
async def authenticate_space_user(
self, access_token: str, refresh_token: str, expires_in: int = 0
) -> typing.Tuple[str, user.User]:
"""Authenticate with Space and return JWT token"""
# Get user info from Space using raw API (token just obtained, no need to validate)
user_info = await self.ap.space_service.get_user_info_raw(access_token)
account = user_info.get('account', {})
api_key = user_info.get('api_key', '')
space_account_uuid = account.get('uuid')
email = account.get('email')
if not space_account_uuid or not email:
raise ValueError('Invalid Space user info')
# Create or update Space user in local database
user_obj = await self.create_or_update_space_user(
space_account_uuid=space_account_uuid,
email=email,
access_token=access_token,
refresh_token=refresh_token,
api_key=api_key,
expires_in=expires_in,
)
# Generate JWT token
jwt_token = await self.generate_jwt_token(email)
return jwt_token, user_obj
async def get_first_user(self) -> user.User | None:
"""Get the first user (for single-user mode)"""
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(user.User).limit(1))
result_list = result.all()
return result_list[0] if result_list else None
async def set_password(self, user_email: str, new_password: str, current_password: str | None = None) -> None:
"""Set or change password for a user"""
ph = argon2.PasswordHasher()
user_obj = await self.get_user_by_email(user_email)
if user_obj is None:
raise ValueError('User not found')
# If user already has a password, verify current password
has_password = bool(user_obj.password and user_obj.password.strip())
if has_password:
if not current_password:
raise ValueError('Current password is required')
ph.verify(user_obj.password, current_password)
hashed_password = ph.hash(new_password)
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
)
async def bind_space_account(self, user_email: str, code: str) -> user.User:
"""Bind Space account to existing local account"""
# Exchange code for tokens
token_data = await self.ap.space_service.exchange_oauth_code(code)
access_token = token_data.get('access_token')
refresh_token = token_data.get('refresh_token')
expires_in = token_data.get('expires_in', 0)
if not access_token:
raise ValueError('Failed to get access token from Space')
expires_at = datetime.datetime.now() + datetime.timedelta(seconds=expires_in) if expires_in > 0 else None
# Get Space user info (token just obtained, use raw API)
user_info = await self.ap.space_service.get_user_info_raw(access_token)
account = user_info.get('account', {})
api_key = user_info.get('api_key', '')
space_account_uuid = account.get('uuid')
space_email = account.get('email')
if not space_account_uuid or not space_email:
raise ValueError('Invalid Space user info')
# Check if this Space account is already bound to another user
existing_space_user = await self.get_user_by_space_account_uuid(space_account_uuid)
if existing_space_user and existing_space_user.user != user_email:
raise ValueError('This Space account is already bound to another user')
# Update local account to Space account
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(user.User)
.where(user.User.user == user_email)
.values(
user=space_email, # Update email to Space email
account_type='space',
space_account_uuid=space_account_uuid,
space_access_token=access_token,
space_refresh_token=refresh_token,
space_api_key=api_key,
space_access_token_expires_at=expires_at,
)
)
# Update Space model provider API keys
await self.ap.provider_service.update_space_model_provider_api_keys(api_key)
return await self.get_user_by_email(space_email)

View File

@@ -15,24 +15,31 @@ from ..command import cmdmgr
from ..plugin import connector as plugin_connector
from ..pipeline import pool
from ..pipeline import controller, pipelinemgr
from ..pipeline import aggregator as message_aggregator
from ..utils import version as version_mgr, proxy as proxy_mgr
from ..persistence import mgr as persistencemgr
from ..api.http.controller import main as http_controller
from ..api.http.service import user as user_service
from ..api.http.service import space as space_service
from ..api.http.service import model as model_service
from ..api.http.service import provider as provider_service
from ..api.http.service import pipeline as pipeline_service
from ..api.http.service import bot as bot_service
from ..api.http.service import knowledge as knowledge_service
from ..api.http.service import mcp as mcp_service
from ..api.http.service import apikey as apikey_service
from ..api.http.service import webhook as webhook_service
from ..api.http.service import monitoring as monitoring_service
from ..discover import engine as discover_engine
from ..storage import mgr as storagemgr
from ..utils import logcache
from . import taskmgr
from . import entities as core_entities
from ..rag.knowledge import kbmgr as rag_mgr
from ..rag.service import RAGRuntimeService
from ..vector import mgr as vectordb_mgr
from ..telemetry import telemetry as telemetry_module
from ..survey import manager as survey_module
class Application:
@@ -56,6 +63,7 @@ class Application:
model_mgr: llm_model_mgr.ModelManager = None
rag_mgr: rag_mgr.RAGManager = None
rag_runtime_service: RAGRuntimeService = None
# TODO move to pipeline
tool_mgr: llm_tool_mgr.ToolManager = None
@@ -74,6 +82,8 @@ class Application:
instance_config: config_mgr.ConfigManager = None
instance_id: config_mgr.ConfigManager = None # used to identify the instance
# ======= Metadata config manager =======
sensitive_meta: config_mgr.ConfigManager = None
@@ -89,6 +99,8 @@ class Application:
query_pool: pool.QueryPool = None
msg_aggregator: message_aggregator.MessageAggregator = None
ctrl: controller.Controller = None
pipeline_mgr: pipelinemgr.PipelineManager = None
@@ -113,10 +125,14 @@ class Application:
user_service: user_service.UserService = None
space_service: space_service.SpaceService = None
llm_model_service: model_service.LLMModelsService = None
embedding_models_service: model_service.EmbeddingModelsService = None
provider_service: provider_service.ModelProviderService = None
pipeline_service: pipeline_service.PipelineService = None
bot_service: bot_service.BotService = None
@@ -129,6 +145,12 @@ class Application:
webhook_service: webhook_service.WebhookService = None
telemetry: telemetry_module.TelemetryManager = None
survey: survey_module.SurveyManager = None
monitoring_service: monitoring_service.MonitoringService = None
def __init__(self):
pass

View File

@@ -1,3 +1,4 @@
import importlib.util
import pip
import os
from ...utils import pkgmgr
@@ -49,9 +50,10 @@ async def check_deps() -> list[str]:
missing_deps = []
for dep in required_deps:
try:
__import__(dep)
except ImportError:
# Use find_spec instead of __import__ to avoid actually loading
# all modules into memory. find_spec only checks if the module
# can be found, without executing module-level code.
if importlib.util.find_spec(dep) is None:
missing_deps.append(dep)
return missing_deps

View File

@@ -1,4 +1,5 @@
import logging
import logging.handlers
import sys
import time
@@ -15,6 +16,10 @@ log_colors_config = {
'CRITICAL': 'cyan',
}
# Log rotation configuration to prevent unbounded log file growth
LOG_FILE_MAX_BYTES = 10 * 1024 * 1024 # 10MB per file
LOG_FILE_BACKUP_COUNT = 5 # Keep 5 backup files (total ~50MB max)
async def init_logging(extra_handlers: list[logging.Handler] = None) -> logging.Logger:
# Remove all existing loggers
@@ -43,9 +48,17 @@ async def init_logging(extra_handlers: list[logging.Handler] = None) -> logging.
# stream_handler.setFormatter(color_formatter)
stream_handler.stream = open(sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1)
# Use RotatingFileHandler to prevent unbounded log file growth
rotating_file_handler = logging.handlers.RotatingFileHandler(
log_file_name,
encoding='utf-8',
maxBytes=LOG_FILE_MAX_BYTES,
backupCount=LOG_FILE_BACKUP_COUNT,
)
log_handlers: list[logging.Handler] = [
stream_handler,
logging.FileHandler(log_file_name, encoding='utf-8'),
rotating_file_handler,
]
log_handlers += extra_handlers if extra_handlers is not None else []

View File

@@ -0,0 +1,17 @@
from __future__ import annotations
from .. import migration
@migration.migration_class('dingtalk_card_auto_layout', 41)
class DingTalkCardAutoLayoutMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return True
async def run(self):
"""执行迁移"""
self.ap.platform_cfg.data['platform-adapters']['app']['dingtalk']['card_auto_layout'] = False
await self.ap.platform_cfg.dump_config()

View File

@@ -5,29 +5,36 @@ import asyncio
from .. import stage, app
from ...utils import version, proxy
from ...pipeline import pool, controller, pipelinemgr
from ...pipeline import aggregator as message_aggregator
from ...plugin import connector as plugin_connector
from ...command import cmdmgr
from ...provider.session import sessionmgr as llm_session_mgr
from ...provider.modelmgr import modelmgr as llm_model_mgr
from ...provider.tools import toolmgr as llm_tool_mgr
from ...rag.knowledge import kbmgr as rag_mgr
from ...rag.service import RAGRuntimeService
from ...platform import botmgr as im_mgr
from ...platform.webhook_pusher import WebhookPusher
from ...persistence import mgr as persistencemgr
from ...api.http.controller import main as http_controller
from ...api.http.service import user as user_service
from ...api.http.service import space as space_service
from ...api.http.service import model as model_service
from ...api.http.service import provider as provider_service
from ...api.http.service import pipeline as pipeline_service
from ...api.http.service import bot as bot_service
from ...api.http.service import knowledge as knowledge_service
from ...api.http.service import mcp as mcp_service
from ...api.http.service import apikey as apikey_service
from ...api.http.service import webhook as webhook_service
from ...api.http.service import monitoring as monitoring_service
from ...discover import engine as discover_engine
from ...storage import mgr as storagemgr
from ...utils import logcache
from ...vector import mgr as vectordb_mgr
from .. import taskmgr
from ...telemetry import telemetry as telemetry_module
from ...survey import manager as survey_module
@stage.stage_class('BuildAppStage')
@@ -42,6 +49,39 @@ class BuildAppStage(stage.BootingStage):
discover.discover_blueprint('templates/components.yaml')
ap.discover = discover
user_service_inst = user_service.UserService(ap)
ap.user_service = user_service_inst
space_service_inst = space_service.SpaceService(ap)
ap.space_service = space_service_inst
llm_model_service_inst = model_service.LLMModelsService(ap)
ap.llm_model_service = llm_model_service_inst
embedding_models_service_inst = model_service.EmbeddingModelsService(ap)
ap.embedding_models_service = embedding_models_service_inst
provider_service_inst = provider_service.ModelProviderService(ap)
ap.provider_service = provider_service_inst
pipeline_service_inst = pipeline_service.PipelineService(ap)
ap.pipeline_service = pipeline_service_inst
bot_service_inst = bot_service.BotService(ap)
ap.bot_service = bot_service_inst
knowledge_service_inst = knowledge_service.KnowledgeService(ap)
ap.knowledge_service = knowledge_service_inst
mcp_service_inst = mcp_service.MCPService(ap)
ap.mcp_service = mcp_service_inst
apikey_service_inst = apikey_service.ApiKeyService(ap)
ap.apikey_service = apikey_service_inst
webhook_service_inst = webhook_service.WebhookService(ap)
ap.webhook_service = webhook_service_inst
proxy_mgr = proxy.ProxyManager(ap)
await proxy_mgr.initialize()
ap.proxy_mgr = proxy_mgr
@@ -63,21 +103,23 @@ class BuildAppStage(stage.BootingStage):
ap.persistence_mgr = persistence_mgr_inst
await persistence_mgr_inst.initialize()
async def runtime_disconnect_callback(connector: plugin_connector.PluginRuntimeConnector) -> None:
await asyncio.sleep(3)
await plugin_connector_inst.initialize()
# Telemetry manager: attach to app so other components can call via self.ap.telemetry
telemetry_inst = telemetry_module.TelemetryManager(ap)
await telemetry_inst.initialize()
ap.telemetry = telemetry_inst
plugin_connector_inst = plugin_connector.PluginRuntimeConnector(ap, runtime_disconnect_callback)
await plugin_connector_inst.initialize()
ap.plugin_connector = plugin_connector_inst
# Survey manager
survey_inst = survey_module.SurveyManager(ap)
await survey_inst.initialize()
ap.survey = survey_inst
cmd_mgr_inst = cmdmgr.CommandManager(ap)
await cmd_mgr_inst.initialize()
ap.cmd_mgr = cmd_mgr_inst
llm_model_mgr_inst = llm_model_mgr.ModelManager(ap)
await llm_model_mgr_inst.initialize()
ap.model_mgr = llm_model_mgr_inst
await llm_model_mgr_inst.initialize()
llm_session_mgr_inst = llm_session_mgr.SessionManager(ap)
await llm_session_mgr_inst.initialize()
@@ -99,10 +141,17 @@ class BuildAppStage(stage.BootingStage):
await pipeline_mgr.initialize()
ap.pipeline_mgr = pipeline_mgr
# Initialize message aggregator (after pipeline_mgr, as it needs pipeline config)
msg_aggregator_inst = message_aggregator.MessageAggregator(ap)
ap.msg_aggregator = msg_aggregator_inst
rag_mgr_inst = rag_mgr.RAGManager(ap)
await rag_mgr_inst.initialize()
ap.rag_mgr = rag_mgr_inst
# Initialize RAG Runtime Service for plugins
ap.rag_runtime_service = RAGRuntimeService(ap)
# 初始化向量数据库管理器
vectordb_mgr_inst = vectordb_mgr.VectorDBManager(ap)
await vectordb_mgr_inst.initialize()
@@ -112,32 +161,16 @@ class BuildAppStage(stage.BootingStage):
await http_ctrl.initialize()
ap.http_ctrl = http_ctrl
user_service_inst = user_service.UserService(ap)
ap.user_service = user_service_inst
monitoring_service_inst = monitoring_service.MonitoringService(ap)
ap.monitoring_service = monitoring_service_inst
llm_model_service_inst = model_service.LLMModelsService(ap)
ap.llm_model_service = llm_model_service_inst
async def runtime_disconnect_callback(connector: plugin_connector.PluginRuntimeConnector) -> None:
await asyncio.sleep(3)
await plugin_connector_inst.initialize()
embedding_models_service_inst = model_service.EmbeddingModelsService(ap)
ap.embedding_models_service = embedding_models_service_inst
pipeline_service_inst = pipeline_service.PipelineService(ap)
ap.pipeline_service = pipeline_service_inst
bot_service_inst = bot_service.BotService(ap)
ap.bot_service = bot_service_inst
knowledge_service_inst = knowledge_service.KnowledgeService(ap)
ap.knowledge_service = knowledge_service_inst
mcp_service_inst = mcp_service.MCPService(ap)
ap.mcp_service = mcp_service_inst
apikey_service_inst = apikey_service.ApiKeyService(ap)
ap.apikey_service = apikey_service_inst
webhook_service_inst = webhook_service.WebhookService(ap)
ap.webhook_service = webhook_service_inst
plugin_connector_inst = plugin_connector.PluginRuntimeConnector(ap, runtime_disconnect_callback)
await plugin_connector_inst.initialize()
ap.plugin_connector = plugin_connector_inst
ctrl = controller.Controller(ap)
ap.ctrl = ctrl

View File

@@ -2,8 +2,11 @@ from __future__ import annotations
import os
from typing import Any
from langbot.pkg.utils import constants
import yaml
import importlib.resources as resources
import uuid
import time
from .. import stage, app
from ..bootutils import config
@@ -142,6 +145,24 @@ class LoadConfigStage(stage.BootingStage):
await ap.instance_config.dump_config()
# load or generate instance id
ap.instance_id = await config.load_json_config(
'data/labels/instance_id.json',
template_data={
'instance_id': f'instance_{str(uuid.uuid4())}',
'instance_create_ts': int(time.time()),
},
completion=False,
)
constants.instance_id = ap.instance_id.data['instance_id']
constants.edition = ap.instance_config.data.get('system', {}).get('edition', 'community')
print(f'LangBot instance id: {constants.instance_id}')
print(f'LangBot edition: {constants.edition}')
await ap.instance_id.dump_config()
ap.sensitive_meta = await config.load_json_config(
'data/metadata/sensitive-words.json',
'metadata/sensitive-words.json',

View File

@@ -0,0 +1,49 @@
# [
# {
# "uuid": "7652ebdb-54dc-412c-a830-e9268ac88471",
# "model_id": "claude-opus-4-5-20251101",
# "display_name": {
# "en_US": "claude-opus-4-5-20251101",
# "zh_Hans": "claude-opus-4-5-20251101"
# },
# "description": {},
# "provider": "anthropic",
# "category": "chat",
# "icon_url": "Claude.Color",
# "tags": {},
# "is_featured": true,
# "featured_order": 999,
# "model_ratio": 2.5,
# "completion_ratio": 5,
# "quota_type": 0,
# "model_price": 0,
# "input_credits": 500,
# "output_credits": 2500,
# "vendor_id": 1,
# "vendor_name": "Anthropic",
# "vendor_icon": "Claude.Color",
# "supported_endpoints": [
# "anthropic",
# "openai"
# ],
# "status": "active",
# "metadata": null,
# "created_at": "2025-12-30T22:23:38.337207+08:00",
# "updated_at": "2025-12-30T22:23:38.337207+08:00"
# }
# ]
import pydantic
class SpaceModel(pydantic.BaseModel):
uuid: str
model_id: str
provider: str
category: str # chat / embedding
llm_abilities: list[str] | None = None
is_featured: bool = False
featured_order: int = 0
status: str
created_at: str | None = None
updated_at: str | None = None

View File

@@ -0,0 +1,6 @@
from __future__ import annotations
class AccountEmailMismatchError(Exception):
def __str__(self):
return 'Account email mismatch'

View File

@@ -7,3 +7,11 @@ class RequesterNotFoundError(Exception):
def __str__(self):
return f'Requester {self.requester_name} not found'
class ProviderNotFoundError(Exception):
def __init__(self, provider_name: str):
self.provider_name = provider_name
def __str__(self):
return f'Provider {self.provider_name} not found'

View File

@@ -9,7 +9,7 @@ class MCPServer(Base):
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
enable = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False)
mode = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) # stdio, sse
mode = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) # stdio, sse, http
extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
updated_at = sqlalchemy.Column(

View File

@@ -3,6 +3,25 @@ import sqlalchemy
from .base import Base
class ModelProvider(Base):
"""Model provider"""
__tablename__ = 'model_providers'
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
requester = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
base_url = sqlalchemy.Column(sqlalchemy.String(512), nullable=False)
api_keys = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=[])
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
updated_at = sqlalchemy.Column(
sqlalchemy.DateTime,
nullable=False,
server_default=sqlalchemy.func.now(),
onupdate=sqlalchemy.func.now(),
)
class LLMModel(Base):
"""LLM model"""
@@ -10,12 +29,10 @@ class LLMModel(Base):
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
description = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
requester = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
requester_config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
api_keys = sqlalchemy.Column(sqlalchemy.JSON, nullable=False)
provider_uuid = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
abilities = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=[])
extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
prefered_ranking = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, default=0)
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
updated_at = sqlalchemy.Column(
sqlalchemy.DateTime,
@@ -26,17 +43,15 @@ class LLMModel(Base):
class EmbeddingModel(Base):
"""Embedding 模型"""
"""Embedding model"""
__tablename__ = 'embedding_models'
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
description = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
requester = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
requester_config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
api_keys = sqlalchemy.Column(sqlalchemy.JSON, nullable=False)
provider_uuid = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
prefered_ranking = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, default=0)
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
updated_at = sqlalchemy.Column(
sqlalchemy.DateTime,

View File

@@ -0,0 +1,106 @@
import sqlalchemy
from .base import Base
class MonitoringMessage(Base):
"""Monitoring message records"""
__tablename__ = 'monitoring_messages'
id = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True)
timestamp = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, index=True)
bot_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
bot_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
pipeline_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
pipeline_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
message_content = sqlalchemy.Column(sqlalchemy.Text, nullable=False)
session_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
status = sqlalchemy.Column(sqlalchemy.String(50), nullable=False) # success, error, pending
level = sqlalchemy.Column(sqlalchemy.String(50), nullable=False) # info, warning, error, debug
platform = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
user_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
runner_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=True) # Runner name for this query
variables = sqlalchemy.Column(sqlalchemy.Text, nullable=True) # Query variables as JSON string
role = sqlalchemy.Column(sqlalchemy.String(50), nullable=True, default='user') # user, assistant
class MonitoringLLMCall(Base):
"""LLM call records"""
__tablename__ = 'monitoring_llm_calls'
id = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True)
timestamp = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, index=True)
model_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
input_tokens = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
output_tokens = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
total_tokens = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
duration = sqlalchemy.Column(sqlalchemy.Integer, nullable=False) # milliseconds
cost = sqlalchemy.Column(sqlalchemy.Float, nullable=True)
status = sqlalchemy.Column(sqlalchemy.String(50), nullable=False) # success, error
bot_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
bot_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
pipeline_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
pipeline_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
session_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
error_message = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
message_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True) # Associated message ID
class MonitoringSession(Base):
"""Session tracking records"""
__tablename__ = 'monitoring_sessions'
session_id = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True)
bot_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
bot_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
pipeline_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
pipeline_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
message_count = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, default=0)
start_time = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, index=True)
last_activity = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, index=True)
is_active = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=True, index=True)
platform = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
user_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
class MonitoringError(Base):
"""Error log records"""
__tablename__ = 'monitoring_errors'
id = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True)
timestamp = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, index=True)
error_type = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
error_message = sqlalchemy.Column(sqlalchemy.Text, nullable=False)
bot_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
bot_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
pipeline_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
pipeline_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
session_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
stack_trace = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
message_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True) # Associated message ID
class MonitoringEmbeddingCall(Base):
"""Embedding call records"""
__tablename__ = 'monitoring_embedding_calls'
id = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True)
timestamp = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, index=True)
model_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
prompt_tokens = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
total_tokens = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
duration = sqlalchemy.Column(sqlalchemy.Integer, nullable=False) # milliseconds
input_count = sqlalchemy.Column(sqlalchemy.Integer, nullable=False) # Number of input texts
status = sqlalchemy.Column(sqlalchemy.String(50), nullable=False) # success, error
error_message = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
# Optional context fields
knowledge_base_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
query_text = sqlalchemy.Column(sqlalchemy.Text, nullable=True) # For retrieval calls
session_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
message_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
call_type = sqlalchemy.Column(sqlalchemy.String(50), nullable=True) # embedding, retrieve

View File

@@ -11,6 +11,7 @@ class LegacyPipeline(Base):
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
description = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
emoji = sqlalchemy.Column(sqlalchemy.String(10), nullable=True, default='⚙️')
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
updated_at = sqlalchemy.Column(
sqlalchemy.DateTime,
@@ -22,7 +23,11 @@ class LegacyPipeline(Base):
is_default = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False)
stages = sqlalchemy.Column(sqlalchemy.JSON, nullable=False)
config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False)
extensions_preferences = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
extensions_preferences = sqlalchemy.Column(
sqlalchemy.JSON,
nullable=False,
default={'enable_all_plugins': True, 'enable_all_mcp_servers': True, 'plugins': [], 'mcp_servers': []},
)
class PipelineRunRecord(Base):

View File

@@ -1,29 +1,30 @@
import sqlalchemy
from .base import Base
# Base = declarative_base()
# DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///./rag_knowledge.db')
# print("Using database URL:", DATABASE_URL)
# engine = create_engine(DATABASE_URL, connect_args={'check_same_thread': False})
# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# def create_db_and_tables():
# """Creates all database tables defined in the Base."""
# Base.metadata.create_all(bind=engine)
# print('Database tables created or already exist.')
class KnowledgeBase(Base):
__tablename__ = 'knowledge_bases'
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
name = sqlalchemy.Column(sqlalchemy.String, index=True)
description = sqlalchemy.Column(sqlalchemy.Text)
emoji = sqlalchemy.Column(sqlalchemy.String(10), nullable=True, default='📚')
created_at = sqlalchemy.Column(sqlalchemy.DateTime, default=sqlalchemy.func.now())
embedding_model_uuid = sqlalchemy.Column(sqlalchemy.String, default='')
top_k = sqlalchemy.Column(sqlalchemy.Integer, default=5)
updated_at = sqlalchemy.Column(sqlalchemy.DateTime, default=sqlalchemy.func.now(), onupdate=sqlalchemy.func.now())
# New fields for plugin-based RAG
knowledge_engine_plugin_id = sqlalchemy.Column(sqlalchemy.String, nullable=True)
collection_id = sqlalchemy.Column(sqlalchemy.String, nullable=True)
creation_settings = sqlalchemy.Column(sqlalchemy.JSON, nullable=True, default=None)
retrieval_settings = sqlalchemy.Column(sqlalchemy.JSON, nullable=True, default=None)
# Field sets for different operations
MUTABLE_FIELDS = {'name', 'description', 'retrieval_settings'}
"""Fields that can be updated after creation."""
CREATE_FIELDS = MUTABLE_FIELDS | {'uuid', 'knowledge_engine_plugin_id', 'collection_id', 'creation_settings'}
"""Fields used when creating a new knowledge base."""
ALL_DB_FIELDS = CREATE_FIELDS | {'emoji', 'created_at', 'updated_at'}
"""All fields stored in database (for loading from DB row)."""
class File(Base):
@@ -41,10 +42,3 @@ class Chunk(Base):
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
file_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
text = sqlalchemy.Column(sqlalchemy.Text)
# class Vector(Base):
# __tablename__ = 'knowledge_base_vectors'
# uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
# chunk_id = sqlalchemy.Column(sqlalchemy.String, nullable=True)
# embedding = sqlalchemy.Column(sqlalchemy.LargeBinary)

View File

@@ -9,6 +9,17 @@ class User(Base):
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
user = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
password = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
# Account type: 'local' (default) or 'space'
account_type = sqlalchemy.Column(sqlalchemy.String(32), nullable=False, server_default='local')
# Space account fields (nullable, only used when account_type='space')
space_account_uuid = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
space_access_token = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
space_refresh_token = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
space_access_token_expires_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=True)
space_api_key = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
updated_at = sqlalchemy.Column(
sqlalchemy.DateTime,

View File

@@ -1,13 +0,0 @@
from __future__ import annotations
import pydantic
from typing import Any
class RetrieveResultEntry(pydantic.BaseModel):
id: str
metadata: dict[str, Any]
distance: float

View File

@@ -9,7 +9,7 @@ import sqlalchemy.ext.asyncio as sqlalchemy_asyncio
import sqlalchemy
from . import database, migration
from ..entity.persistence import base, pipeline, metadata
from ..entity.persistence import base, pipeline, metadata, model as persistence_model
from ..entity import persistence
from ..core import app
from ..utils import constants, importutil
@@ -79,6 +79,7 @@ class PersistenceManager:
self.ap.logger.info(f'Successfully upgraded database to version {last_migration_number}.')
await self.write_default_pipeline()
await self.write_space_model_providers()
async def create_tables(self):
# create tables
@@ -123,7 +124,42 @@ class PersistenceManager:
await self.execute_async(sqlalchemy.insert(pipeline.LegacyPipeline).values(pipeline_data))
# =================================
async def write_space_model_providers(self):
space_models_gateway_api_url = self.ap.instance_config.data.get('space', {}).get(
'models_gateway_api_url', 'https://api.langbot.cloud/v1'
)
# write space model providers
result = await self.execute_async(
sqlalchemy.select(persistence_model.ModelProvider).where(
persistence_model.ModelProvider.requester == 'space-chat-completions'
)
)
exists_space_chat_completions_model_provider = result.first()
# api keys will be set/updated when the oauth callback
if exists_space_chat_completions_model_provider is None:
self.ap.logger.info('Creating space model providers...')
space_chat_completions_model_provider = {
'uuid': '00000000-0000-0000-0000-000000000000',
'name': 'LangBot Models',
'requester': 'space-chat-completions',
'base_url': space_models_gateway_api_url,
'api_keys': [],
}
await self.execute_async(
sqlalchemy.insert(persistence_model.ModelProvider).values(space_chat_completions_model_provider)
)
else:
if exists_space_chat_completions_model_provider.base_url != space_models_gateway_api_url:
await self.execute_async(
sqlalchemy.update(persistence_model.ModelProvider)
.where(persistence_model.ModelProvider.uuid == exists_space_chat_completions_model_provider.uuid)
.values({'base_url': space_models_gateway_api_url})
)
# =================================
async def execute_async(self, *args, **kwargs) -> sqlalchemy.engine.cursor.CursorResult:
async with self.get_db_engine().connect() as conn:

View File

@@ -1,8 +1,7 @@
from .. import migration
import sqlalchemy
from ...entity.persistence import pipeline as persistence_pipeline
import json
@migration.migration_class(2)
@@ -11,30 +10,45 @@ class DBMigrateCombineQuoteMsgConfig(migration.DBMigration):
async def upgrade(self):
"""Upgrade"""
# read all pipelines
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
# Read all pipelines using raw SQL
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
)
pipelines = result.fetchall()
for pipeline in pipelines:
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
current_version = self.ap.ver_mgr.get_current_version()
config = serialized_pipeline['config']
for pipeline_row in pipelines:
uuid = pipeline_row[0]
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
# Ensure 'trigger' exists
if 'trigger' not in config:
config['trigger'] = {}
# Ensure 'misc' exists in 'trigger'
if 'misc' not in config['trigger']:
config['trigger']['misc'] = {}
# Add 'combine-quote-message' if not exists
if 'combine-quote-message' not in config['trigger']['misc']:
config['trigger']['misc']['combine-quote-message'] = False
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
.values(
{
'config': config,
'for_version': self.ap.ver_mgr.get_current_version(),
}
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
)
async def downgrade(self):
"""Downgrade"""

View File

@@ -1,8 +1,7 @@
from .. import migration
import sqlalchemy
from ...entity.persistence import pipeline as persistence_pipeline
import json
@migration.migration_class(3)
@@ -11,14 +10,23 @@ class DBMigrateN8nConfig(migration.DBMigration):
async def upgrade(self):
"""Upgrade"""
# read all pipelines
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
# Read all pipelines using raw SQL
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
)
pipelines = result.fetchall()
for pipeline in pipelines:
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
current_version = self.ap.ver_mgr.get_current_version()
config = serialized_pipeline['config']
for pipeline_row in pipelines:
uuid = pipeline_row[0]
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
# Ensure 'ai' exists
if 'ai' not in config:
config['ai'] = {}
# Add 'n8n-service-api' if not exists
if 'n8n-service-api' not in config['ai']:
config['ai']['n8n-service-api'] = {
'webhook-url': 'http://your-n8n-webhook-url',
@@ -33,16 +41,21 @@ class DBMigrateN8nConfig(migration.DBMigration):
'output-key': 'response',
}
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
.values(
{
'config': config,
'for_version': self.ap.ver_mgr.get_current_version(),
}
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
)
async def downgrade(self):
"""Downgrade"""

View File

@@ -1,8 +1,7 @@
from .. import migration
import sqlalchemy
from ...entity.persistence import pipeline as persistence_pipeline
import json
@migration.migration_class(4)
@@ -11,27 +10,43 @@ class DBMigrateRAGKBUUID(migration.DBMigration):
async def upgrade(self):
"""升级"""
# read all pipelines
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
# Read all pipelines using raw SQL
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
)
pipelines = result.fetchall()
for pipeline in pipelines:
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
current_version = self.ap.ver_mgr.get_current_version()
config = serialized_pipeline['config']
for pipeline_row in pipelines:
uuid = pipeline_row[0]
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
# Ensure nested structure exists
if 'ai' not in config:
config['ai'] = {}
if 'local-agent' not in config['ai']:
config['ai']['local-agent'] = {}
# Add 'knowledge-base' if not exists
if 'knowledge-base' not in config['ai']['local-agent']:
config['ai']['local-agent']['knowledge-base'] = ''
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
.values(
{
'config': config,
'for_version': self.ap.ver_mgr.get_current_version(),
}
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
)
async def downgrade(self):
"""降级"""

View File

@@ -1,8 +1,7 @@
from .. import migration
import sqlalchemy
from ...entity.persistence import pipeline as persistence_pipeline
import json
@migration.migration_class(5)
@@ -11,27 +10,43 @@ class DBMigratePipelineRemoveCotConfig(migration.DBMigration):
async def upgrade(self):
"""Upgrade"""
# read all pipelines
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
# Read all pipelines using raw SQL
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
)
pipelines = result.fetchall()
for pipeline in pipelines:
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
current_version = self.ap.ver_mgr.get_current_version()
config = serialized_pipeline['config']
for pipeline_row in pipelines:
uuid = pipeline_row[0]
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
# Ensure nested structure exists
if 'output' not in config:
config['output'] = {}
if 'misc' not in config['output']:
config['output']['misc'] = {}
# Add 'remove-think' if not exists
if 'remove-think' not in config['output']['misc']:
config['output']['misc']['remove-think'] = False
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
.values(
{
'config': config,
'for_version': self.ap.ver_mgr.get_current_version(),
}
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
)
async def downgrade(self):
"""Downgrade"""

View File

@@ -1,8 +1,7 @@
from .. import migration
import sqlalchemy
from ...entity.persistence import pipeline as persistence_pipeline
import json
@migration.migration_class(6)
@@ -11,14 +10,23 @@ class DBMigrateLangflowApiConfig(migration.DBMigration):
async def upgrade(self):
"""Upgrade"""
# read all pipelines
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
# Read all pipelines using raw SQL
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
)
pipelines = result.fetchall()
for pipeline in pipelines:
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
current_version = self.ap.ver_mgr.get_current_version()
config = serialized_pipeline['config']
for pipeline_row in pipelines:
uuid = pipeline_row[0]
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
# Ensure 'ai' exists
if 'ai' not in config:
config['ai'] = {}
# Add 'langflow-api' if not exists
if 'langflow-api' not in config['ai']:
config['ai']['langflow-api'] = {
'base-url': 'http://localhost:7860',
@@ -29,16 +37,21 @@ class DBMigrateLangflowApiConfig(migration.DBMigration):
'tweaks': '{}',
}
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
.values(
{
'config': config,
'for_version': self.ap.ver_mgr.get_current_version(),
}
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
)
async def downgrade(self):
"""Downgrade"""

View File

@@ -1,8 +1,7 @@
from .. import migration
import sqlalchemy
from ...entity.persistence import pipeline as persistence_pipeline
import json
@migration.migration_class(10)
@@ -11,16 +10,20 @@ class DBMigratePipelineMultiKnowledgeBase(migration.DBMigration):
async def upgrade(self):
"""Upgrade"""
# read all pipelines
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
# Read all pipelines using raw SQL
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
)
pipelines = result.fetchall()
for pipeline in pipelines:
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
current_version = self.ap.ver_mgr.get_current_version()
config = serialized_pipeline['config']
for pipeline_row in pipelines:
uuid = pipeline_row[0]
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
# Convert knowledge-base from string to array
if 'local-agent' in config['ai']:
if 'ai' in config and 'local-agent' in config['ai']:
current_kb = config['ai']['local-agent'].get('knowledge-base', '')
# If it's already a list, skip
@@ -37,29 +40,38 @@ class DBMigratePipelineMultiKnowledgeBase(migration.DBMigration):
if 'knowledge-base' in config['ai']['local-agent']:
del config['ai']['local-agent']['knowledge-base']
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
.values(
{
'config': config,
'for_version': self.ap.ver_mgr.get_current_version(),
}
)
)
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
async def downgrade(self):
"""Downgrade"""
# read all pipelines
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
# Read all pipelines using raw SQL
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
)
pipelines = result.fetchall()
for pipeline in pipelines:
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
current_version = self.ap.ver_mgr.get_current_version()
config = serialized_pipeline['config']
for pipeline_row in pipelines:
uuid = pipeline_row[0]
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
# Convert knowledge-bases from array back to string
if 'local-agent' in config['ai']:
if 'ai' in config and 'local-agent' in config['ai']:
current_kbs = config['ai']['local-agent'].get('knowledge-bases', [])
# If it's already a string, skip
@@ -76,13 +88,18 @@ class DBMigratePipelineMultiKnowledgeBase(migration.DBMigration):
if 'knowledge-bases' in config['ai']['local-agent']:
del config['ai']['local-agent']['knowledge-bases']
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
.values(
{
'config': config,
'for_version': self.ap.ver_mgr.get_current_version(),
}
)
)
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)

View File

@@ -1,39 +1,54 @@
from .. import migration
import sqlalchemy
from ...entity.persistence import pipeline as persistence_pipeline
import json
@migration.migration_class(11)
class DBMigrateDifyApiConfig(migration.DBMigration):
"""Langflow API config"""
"""Dify base prompt config"""
async def upgrade(self):
"""Upgrade"""
# read all pipelines
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
# Read all pipelines using raw SQL
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
)
pipelines = result.fetchall()
for pipeline in pipelines:
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
current_version = self.ap.ver_mgr.get_current_version()
config = serialized_pipeline['config']
for pipeline_row in pipelines:
uuid = pipeline_row[0]
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
# Ensure nested structure exists
if 'ai' not in config:
config['ai'] = {}
if 'dify-service-api' not in config['ai']:
config['ai']['dify-service-api'] = {}
# Add 'base-prompt' if not exists
if 'base-prompt' not in config['ai']['dify-service-api']:
config['ai']['dify-service-api']['base-prompt'] = (
'When the file content is readable, please read the content of this file. When the file is an image, describe the content of this image.',
)
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
.values(
{
'config': config,
'for_version': self.ap.ver_mgr.get_current_version(),
}
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
),
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
)
)
async def downgrade(self):
"""Downgrade"""

View File

@@ -0,0 +1,73 @@
from .. import migration
import sqlalchemy
import json
@migration.migration_class(12)
class DBMigratePipelineExtensionsEnableAll(migration.DBMigration):
"""Pipeline extensions enable all"""
async def upgrade(self):
"""Upgrade"""
# Read all pipelines using raw SQL
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('SELECT uuid, extensions_preferences FROM legacy_pipelines')
)
pipelines = result.fetchall()
current_version = self.ap.ver_mgr.get_current_version()
for pipeline_row in pipelines:
uuid = pipeline_row[0]
extensions_preferences = (
json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
)
# Ensure extensions_preferences is a dict
if extensions_preferences is None:
extensions_preferences = {}
# Add 'enable_all_plugins' if not exists
if 'enable_all_plugins' not in extensions_preferences:
if 'plugins' in extensions_preferences:
extensions_preferences['enable_all_plugins'] = False
else:
extensions_preferences['enable_all_plugins'] = True
extensions_preferences['plugins'] = []
# Add 'enable_all_mcp_servers' if not exists
if 'enable_all_mcp_servers' not in extensions_preferences:
if 'mcp_servers' in extensions_preferences:
extensions_preferences['enable_all_mcp_servers'] = False
else:
extensions_preferences['enable_all_mcp_servers'] = True
extensions_preferences['mcp_servers'] = []
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET extensions_preferences = :extensions_preferences::jsonb, for_version = :for_version WHERE uuid = :uuid'
),
{
'extensions_preferences': json.dumps(extensions_preferences),
'for_version': current_version,
'uuid': uuid,
},
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'UPDATE legacy_pipelines SET extensions_preferences = :extensions_preferences, for_version = :for_version WHERE uuid = :uuid'
),
{
'extensions_preferences': json.dumps(extensions_preferences),
'for_version': current_version,
'uuid': uuid,
},
)
async def downgrade(self):
"""Downgrade"""
pass

View File

@@ -0,0 +1,49 @@
import sqlalchemy
from .. import migration
@migration.migration_class(13)
class DBMigrateKnowledgeBaseUpdatedAt(migration.DBMigration):
"""Add updated_at field to knowledge_bases table"""
async def upgrade(self):
"""Upgrade"""
# Get all column names from the table
columns = []
if self.ap.persistence_mgr.db.name == 'postgresql':
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
"SELECT column_name FROM information_schema.columns WHERE table_name = 'knowledge_bases';"
)
)
all_result = result.fetchall()
columns = [row[0] for row in all_result]
else:
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text('PRAGMA table_info(knowledge_bases);'))
all_result = result.fetchall()
columns = [row[1] for row in all_result]
# Check and add updated_at column
if 'updated_at' not in columns:
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
'ALTER TABLE knowledge_bases ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP'
)
)
else:
# SQLite doesn't support DEFAULT CURRENT_TIMESTAMP in ALTER TABLE
# Add column without default first
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE knowledge_bases ADD COLUMN updated_at DATETIME')
)
# Set initial updated_at values to created_at for existing records
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('UPDATE knowledge_bases SET updated_at = created_at WHERE updated_at IS NULL')
)
async def downgrade(self):
"""Downgrade"""
pass

View File

@@ -0,0 +1,94 @@
import sqlalchemy
from .. import migration
@migration.migration_class(14)
class DBMigrateSpaceAccountSupport(migration.DBMigration):
"""Add Space account support fields to users table"""
async def upgrade(self):
"""Upgrade"""
# Get all column names from the users table
columns = []
if self.ap.persistence_mgr.db.name == 'postgresql':
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text("SELECT column_name FROM information_schema.columns WHERE table_name = 'users';")
)
all_result = result.fetchall()
columns = [row[0] for row in all_result]
else:
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text('PRAGMA table_info(users);'))
all_result = result.fetchall()
columns = [row[1] for row in all_result]
# Add account_type column
if 'account_type' not in columns:
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text("ALTER TABLE users ADD COLUMN account_type VARCHAR(32) DEFAULT 'local' NOT NULL")
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text("ALTER TABLE users ADD COLUMN account_type VARCHAR(32) DEFAULT 'local' NOT NULL")
)
# Add space_account_uuid column
if 'space_account_uuid' not in columns:
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_account_uuid VARCHAR(255)')
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_account_uuid VARCHAR(255)')
)
# Add space_access_token column
if 'space_access_token' not in columns:
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_access_token TEXT')
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_access_token TEXT')
)
# Add space_refresh_token column
if 'space_refresh_token' not in columns:
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_refresh_token TEXT')
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_refresh_token TEXT')
)
# Add space_access_token_expires_at column
if 'space_access_token_expires_at' not in columns:
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_access_token_expires_at TIMESTAMP')
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_access_token_expires_at DATETIME')
)
# Add space_api_key column
if 'space_api_key' not in columns:
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_api_key VARCHAR(255)')
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_api_key VARCHAR(255)')
)
async def downgrade(self):
"""Downgrade"""
pass

View File

@@ -0,0 +1,15 @@
from .. import migration
# this is a deprecated migration
@migration.migration_class(15)
class DBMigrateModelSourceTracking(migration.DBMigration):
"""Add source tracking fields to models tables for Space integration"""
async def upgrade(self):
"""Upgrade"""
pass
async def downgrade(self):
"""Downgrade"""
pass

View File

@@ -0,0 +1,305 @@
import uuid as uuid_lib
import sqlalchemy
from .. import migration
@migration.migration_class(16)
class DBMigrateModelProviderRefactor(migration.DBMigration):
"""Refactor model structure: create providers from existing models and update references"""
async def upgrade(self):
"""Upgrade"""
# Step 1: Create model_providers table if not exists
await self._create_providers_table()
# Step 2: Migrate existing models to use providers
await self._migrate_llm_models()
await self._migrate_embedding_models()
# Step 3: Remove deprecated columns
await self._cleanup_columns()
async def _create_providers_table(self):
"""Create model_providers table"""
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text("""
CREATE TABLE IF NOT EXISTS model_providers (
uuid VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
requester VARCHAR(255) NOT NULL,
base_url VARCHAR(512) NOT NULL,
api_keys JSONB NOT NULL DEFAULT '[]',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
""")
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text("""
CREATE TABLE IF NOT EXISTS model_providers (
uuid VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
requester VARCHAR(255) NOT NULL,
base_url VARCHAR(512) NOT NULL,
api_keys JSON NOT NULL DEFAULT '[]',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)
""")
)
async def _migrate_llm_models(self):
"""Migrate LLM models to use providers"""
llm_columns = await self._get_columns('llm_models')
# Add provider_uuid column if not exists
if 'provider_uuid' not in llm_columns:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE llm_models ADD COLUMN provider_uuid VARCHAR(255)')
)
# Add prefered_ranking column if not exists
if 'prefered_ranking' not in llm_columns:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE llm_models ADD COLUMN prefered_ranking INTEGER NOT NULL DEFAULT 0')
)
# Only migrate if old columns exist
if 'requester' not in llm_columns:
return
# Get all LLM models with old structure
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('SELECT uuid, name, requester, requester_config, api_keys FROM llm_models')
)
models = result.fetchall()
# Create providers and update models
provider_cache = {} # (requester, base_url, api_keys_str) -> provider_uuid
for model in models:
model_uuid, model_name, requester, requester_config, api_keys = model
# Extract base_url from requester_config
base_url = ''
if requester_config:
if isinstance(requester_config, str):
import json
requester_config = json.loads(requester_config)
base_url = requester_config.get('base_url', '') or requester_config.get('base-url', '')
# Parse api_keys if it's a string
if isinstance(api_keys, str):
import json
try:
api_keys = json.loads(api_keys)
except Exception:
api_keys = []
if not api_keys:
api_keys = []
# Create cache key
api_keys_str = str(sorted(api_keys)) if api_keys else '[]'
cache_key = (requester, base_url, api_keys_str)
if cache_key in provider_cache:
provider_uuid = provider_cache[cache_key]
else:
# Create new provider
provider_uuid = str(uuid_lib.uuid4())
provider_name = f'{requester}'
if base_url:
# Extract domain for name
try:
from urllib.parse import urlparse
parsed = urlparse(base_url)
provider_name = parsed.netloc or requester
except Exception:
pass
import json
api_keys_json = json.dumps(api_keys) if api_keys else '[]'
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text("""
INSERT INTO model_providers (uuid, name, requester, base_url, api_keys)
VALUES (:uuid, :name, :requester, :base_url, :api_keys)
"""),
{
'uuid': provider_uuid,
'name': provider_name,
'requester': requester,
'base_url': base_url,
'api_keys': api_keys_json,
},
)
provider_cache[cache_key] = provider_uuid
# Update model with provider_uuid
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('UPDATE llm_models SET provider_uuid = :provider_uuid WHERE uuid = :uuid'),
{'provider_uuid': provider_uuid, 'uuid': model_uuid},
)
async def _migrate_embedding_models(self):
"""Migrate embedding models to use providers"""
embedding_columns = await self._get_columns('embedding_models')
# Add provider_uuid column if not exists
if 'provider_uuid' not in embedding_columns:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE embedding_models ADD COLUMN provider_uuid VARCHAR(255)')
)
# Add prefered_ranking column if not exists
if 'prefered_ranking' not in embedding_columns:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('ALTER TABLE embedding_models ADD COLUMN prefered_ranking INTEGER NOT NULL DEFAULT 0')
)
# Only migrate if old columns exist
if 'requester' not in embedding_columns:
return
# Get all embedding models with old structure
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('SELECT uuid, name, requester, requester_config, api_keys FROM embedding_models')
)
models = result.fetchall()
# Get existing providers
provider_result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('SELECT uuid, requester, base_url, api_keys FROM model_providers')
)
existing_providers = provider_result.fetchall()
provider_cache = {}
for p in existing_providers:
p_uuid, p_requester, p_base_url, p_api_keys = p
api_keys_str = str(sorted(p_api_keys)) if p_api_keys else '[]'
provider_cache[(p_requester, p_base_url, api_keys_str)] = p_uuid
for model in models:
model_uuid, model_name, requester, requester_config, api_keys = model
base_url = ''
if requester_config:
if isinstance(requester_config, str):
import json
requester_config = json.loads(requester_config)
base_url = requester_config.get('base_url', '') or requester_config.get('base-url', '')
# Parse api_keys if it's a string
if isinstance(api_keys, str):
import json
try:
api_keys = json.loads(api_keys)
except Exception:
api_keys = []
if not api_keys:
api_keys = []
api_keys_str = str(sorted(api_keys)) if api_keys else '[]'
cache_key = (requester, base_url, api_keys_str)
if cache_key in provider_cache:
provider_uuid = provider_cache[cache_key]
else:
provider_uuid = str(uuid_lib.uuid4())
provider_name = f'{requester}'
if base_url:
try:
from urllib.parse import urlparse
parsed = urlparse(base_url)
provider_name = parsed.netloc or requester
except Exception:
pass
import json
api_keys_json = json.dumps(api_keys) if api_keys else '[]'
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text("""
INSERT INTO model_providers (uuid, name, requester, base_url, api_keys)
VALUES (:uuid, :name, :requester, :base_url, :api_keys)
"""),
{
'uuid': provider_uuid,
'name': provider_name,
'requester': requester,
'base_url': base_url,
'api_keys': api_keys_json,
},
)
provider_cache[cache_key] = provider_uuid
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text('UPDATE embedding_models SET provider_uuid = :provider_uuid WHERE uuid = :uuid'),
{'provider_uuid': provider_uuid, 'uuid': model_uuid},
)
async def _cleanup_columns(self):
"""Remove deprecated columns from model tables"""
llm_columns = await self._get_columns('llm_models')
deprecated_llm_cols = ['requester', 'requester_config', 'api_keys', 'description', 'source', 'space_model_id']
for col in deprecated_llm_cols:
if col in llm_columns:
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(f'ALTER TABLE llm_models DROP COLUMN IF EXISTS {col}')
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(f'ALTER TABLE llm_models DROP COLUMN {col}')
)
embedding_columns = await self._get_columns('embedding_models')
deprecated_embedding_cols = [
'requester',
'requester_config',
'api_keys',
'description',
'source',
'space_model_id',
]
for col in deprecated_embedding_cols:
if col in embedding_columns:
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(f'ALTER TABLE embedding_models DROP COLUMN IF EXISTS {col}')
)
else:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(f'ALTER TABLE embedding_models DROP COLUMN {col}')
)
async def _get_columns(self, table_name: str) -> list:
"""Get column names for a table"""
if self.ap.persistence_mgr.db.name == 'postgresql':
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
f"SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}';"
)
)
all_result = result.fetchall()
return [row[0] for row in all_result]
else:
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text(f'PRAGMA table_info({table_name});'))
all_result = result.fetchall()
return [row[1] for row in all_result]
async def downgrade(self):
"""Downgrade"""
pass

View File

@@ -0,0 +1,25 @@
from .. import migration
@migration.migration_class(17)
class MoveCloudServiceUrl(migration.DBMigration):
"""迁移云服务 URL 配置"""
async def upgrade(self):
"""升级"""
if 'space' not in self.ap.instance_config.data:
self.ap.instance_config.data['space'] = {
'url': 'https://space.langbot.app',
'models_gateway_api_url': 'https://api.langbot.cloud/v1',
'oauth_authorize_url': 'https://space.langbot.app/auth/authorize',
'disable_models_service': False,
}
if 'plugin' in self.ap.instance_config.data:
self.ap.instance_config.data['plugin'].pop('cloud_service_url', None)
await self.ap.instance_config.dump_config()
async def downgrade(self):
"""降级"""
pass

View File

@@ -0,0 +1,58 @@
import sqlalchemy
from .. import migration
@migration.migration_class(18)
class DBMigrateAddEmojiSupport(migration.DBMigration):
"""Add emoji field to knowledge_bases, external_knowledge_bases and legacy_pipelines tables"""
async def upgrade(self):
"""Upgrade"""
# Add emoji field to knowledge_bases
await self._add_emoji_to_table('knowledge_bases', '📚')
# Add emoji field to external_knowledge_bases
await self._add_emoji_to_table('external_knowledge_bases', '🔗')
# Add emoji field to legacy_pipelines
await self._add_emoji_to_table('legacy_pipelines', '⚙️')
async def _add_emoji_to_table(self, table_name: str, default_emoji: str):
"""Add emoji column to specified table if it doesn't exist"""
# Get all column names from the table
columns = []
if self.ap.persistence_mgr.db.name == 'postgresql':
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
f"SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}';"
)
)
all_result = result.fetchall()
columns = [row[0] for row in all_result]
else:
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text(f'PRAGMA table_info({table_name});'))
all_result = result.fetchall()
columns = [row[1] for row in all_result]
# Check and add emoji column
if 'emoji' not in columns:
if self.ap.persistence_mgr.db.name == 'postgresql':
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(f"ALTER TABLE {table_name} ADD COLUMN emoji VARCHAR(10) DEFAULT '{default_emoji}'")
)
else:
# SQLite doesn't support DEFAULT with emoji directly in ALTER TABLE
# Add column without default first
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(f'ALTER TABLE {table_name} ADD COLUMN emoji VARCHAR(10)')
)
# Set default emoji value for existing records
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(f"UPDATE {table_name} SET emoji = '{default_emoji}' WHERE emoji IS NULL")
)
async def downgrade(self):
"""Downgrade"""
pass

View File

@@ -0,0 +1,24 @@
import sqlalchemy
from .. import migration
@migration.migration_class(19)
class DBMigrateMonitoringMessageRole(migration.DBMigration):
"""Add role column to monitoring_messages table"""
async def upgrade(self):
"""Upgrade"""
try:
sql_text = sqlalchemy.text("ALTER TABLE monitoring_messages ADD COLUMN role VARCHAR(50) DEFAULT 'user'")
await self.ap.persistence_mgr.execute_async(sql_text)
except Exception:
# Column may already exist
pass
async def downgrade(self):
"""Downgrade"""
try:
sql_text = sqlalchemy.text('ALTER TABLE monitoring_messages DROP COLUMN role')
await self.ap.persistence_mgr.execute_async(sql_text)
except Exception:
pass

Some files were not shown because too many files have changed in this diff Show More