From 3edae3e678e1d45b3e4c0cd145b3410d4b1368b1 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Nov 2025 13:45:09 +0800 Subject: [PATCH 01/26] feat: Support multiple knowledge base binding in pipelines (#1766) * Initial plan * Add multi-knowledge base support to pipelines - Created database migration dbm010 to convert knowledge-base field from string to array - Updated default pipeline config to use knowledge-bases array - Updated pipeline metadata to use knowledge-base-multi-selector type - Modified localagent.py to retrieve from multiple knowledge bases and concatenate results - Added KNOWLEDGE_BASE_MULTI_SELECTOR type to frontend form entities - Implemented multi-selector UI component with dialog for selecting multiple knowledge bases Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Add i18n translations for multi-knowledge base selector Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Fix prettier formatting errors in DynamicFormItemComponent Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Add accessibility attributes to knowledge base selector checkbox Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * fix: minor fix --------- 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 --- .../dbm010_pipeline_multi_knowledge_base.py | 88 +++++++++++ pkg/provider/runners/localagent.py | 35 +++-- pkg/utils/constants.py | 2 +- templates/default-pipeline-config.json | 2 +- templates/metadata/pipeline/ai.yaml | 10 +- .../dynamic-form/DynamicFormComponent.tsx | 3 + .../dynamic-form/DynamicFormItemComponent.tsx | 138 +++++++++++++++++- web/src/app/infra/entities/form/dynamic.ts | 1 + web/src/i18n/locales/en-US.ts | 3 + web/src/i18n/locales/ja-JP.ts | 3 + web/src/i18n/locales/zh-Hans.ts | 3 + web/src/i18n/locales/zh-Hant.ts | 3 + 12 files changed, 271 insertions(+), 20 deletions(-) create mode 100644 pkg/persistence/migrations/dbm010_pipeline_multi_knowledge_base.py diff --git a/pkg/persistence/migrations/dbm010_pipeline_multi_knowledge_base.py b/pkg/persistence/migrations/dbm010_pipeline_multi_knowledge_base.py new file mode 100644 index 00000000..c28b64ed --- /dev/null +++ b/pkg/persistence/migrations/dbm010_pipeline_multi_knowledge_base.py @@ -0,0 +1,88 @@ +from .. import migration + +import sqlalchemy + +from ...entity.persistence import pipeline as persistence_pipeline + + +@migration.migration_class(10) +class DBMigratePipelineMultiKnowledgeBase(migration.DBMigration): + """Pipeline support multiple knowledge base binding""" + + async def upgrade(self): + """Upgrade""" + # read all pipelines + pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline)) + + for pipeline in pipelines: + serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline) + + config = serialized_pipeline['config'] + + # Convert knowledge-base from string to array + if 'local-agent' in config['ai']: + current_kb = config['ai']['local-agent'].get('knowledge-base', '') + + # If it's already a list, skip + if isinstance(current_kb, list): + continue + + # Convert string to list + if current_kb and current_kb != '__none__': + config['ai']['local-agent']['knowledge-bases'] = [current_kb] + else: + config['ai']['local-agent']['knowledge-bases'] = [] + + # Remove old field + 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(), + } + ) + ) + + async def downgrade(self): + """Downgrade""" + # read all pipelines + pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline)) + + for pipeline in pipelines: + serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline) + + config = serialized_pipeline['config'] + + # Convert knowledge-bases from array back to string + if 'local-agent' in config['ai']: + current_kbs = config['ai']['local-agent'].get('knowledge-bases', []) + + # If it's already a string, skip + if isinstance(current_kbs, str): + continue + + # Convert list to string (take first one or empty) + if current_kbs and len(current_kbs) > 0: + config['ai']['local-agent']['knowledge-base'] = current_kbs[0] + else: + config['ai']['local-agent']['knowledge-base'] = '' + + # Remove new field + 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(), + } + ) + ) diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 7ab1e739..f89a4e1c 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -40,10 +40,14 @@ class LocalAgentRunner(runner.RequestRunner): """运行请求""" pending_tool_calls = [] - kb_uuid = query.pipeline_config['ai']['local-agent']['knowledge-base'] - - if kb_uuid == '__none__': - kb_uuid = None + # Get knowledge bases list (new field) + kb_uuids = query.pipeline_config['ai']['local-agent'].get('knowledge-bases', []) + + # Fallback to old field for backward compatibility + if not kb_uuids: + old_kb_uuid = query.pipeline_config['ai']['local-agent'].get('knowledge-base', '') + if old_kb_uuid and old_kb_uuid != '__none__': + kb_uuids = [old_kb_uuid] user_message = copy.deepcopy(query.user_message) @@ -57,21 +61,28 @@ class LocalAgentRunner(runner.RequestRunner): user_message_text += ce.text break - if kb_uuid and user_message_text: + if kb_uuids and user_message_text: # only support text for now - kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid) + all_results = [] + + # Retrieve from each knowledge base + for kb_uuid in kb_uuids: + kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid) - if not kb: - self.ap.logger.warning(f'Knowledge base {kb_uuid} not found') - raise ValueError(f'Knowledge base {kb_uuid} not found') + if not kb: + self.ap.logger.warning(f'Knowledge base {kb_uuid} not found, skipping') + continue - result = await kb.retrieve(user_message_text, kb.knowledge_base_entity.top_k) + result = await kb.retrieve(user_message_text, kb.knowledge_base_entity.top_k) + + if result: + all_results.extend(result) final_user_message_text = '' - if result: + if all_results: rag_context = '\n\n'.join( - f'[{i + 1}] {entry.metadata.get("text", "")}' for i, entry in enumerate(result) + f'[{i + 1}] {entry.metadata.get("text", "")}' for i, entry in enumerate(all_results) ) final_user_message_text = rag_combined_prompt_template.format( rag_context=rag_context, user_message=user_message_text diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index ee0fe9c9..5a7ab4fa 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,6 +1,6 @@ semantic_version = 'v4.4.1' -required_database_version = 9 +required_database_version = 10 """Tag the version of the database schema, used to check if the database needs to be migrated""" debug_mode = False diff --git a/templates/default-pipeline-config.json b/templates/default-pipeline-config.json index c5398e76..efbb9c3f 100644 --- a/templates/default-pipeline-config.json +++ b/templates/default-pipeline-config.json @@ -45,7 +45,7 @@ "content": "You are a helpful assistant." } ], - "knowledge-base": "" + "knowledge-bases": [] }, "dify-service-api": { "base-url": "https://api.dify.ai/v1", diff --git a/templates/metadata/pipeline/ai.yaml b/templates/metadata/pipeline/ai.yaml index e4d16a95..f6d54ee6 100644 --- a/templates/metadata/pipeline/ai.yaml +++ b/templates/metadata/pipeline/ai.yaml @@ -80,16 +80,16 @@ stages: zh_Hans: 除非您了解消息结构,否则请只使用 system 单提示词 type: prompt-editor required: true - - name: knowledge-base + - name: knowledge-bases label: - en_US: Knowledge Base + en_US: Knowledge Bases zh_Hans: 知识库 description: - en_US: Configure the knowledge base to use for the agent, if not selected, the agent will directly use the LLM to reply + en_US: Configure the knowledge bases to use for the agent, if not selected, the agent will directly use the LLM to reply zh_Hans: 配置用于提升回复质量的知识库,若不选择,则直接使用大模型回复 - type: knowledge-base-selector + type: knowledge-base-multi-selector required: false - default: '' + default: [] - name: tbox-app-api label: en_US: Tbox App API diff --git a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx index 5cdd2ff7..dd2178f2 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx @@ -58,6 +58,9 @@ export default function DynamicFormComponent({ case 'knowledge-base-selector': fieldSchema = z.string(); break; + case 'knowledge-base-multi-selector': + fieldSchema = z.array(z.string()); + break; case 'bot-selector': fieldSchema = z.string(); break; diff --git a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx index 03603c26..e078adb2 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx @@ -29,6 +29,15 @@ import { useTranslation } from 'react-i18next'; import { extractI18nObject } from '@/i18n/I18nProvider'; import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent } from '@/components/ui/card'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Plus, X } from 'lucide-react'; export default function DynamicFormItemComponent({ config, @@ -44,6 +53,8 @@ export default function DynamicFormItemComponent({ const [knowledgeBases, setKnowledgeBases] = useState([]); const [bots, setBots] = useState([]); const [uploading, setUploading] = useState(false); + const [kbDialogOpen, setKbDialogOpen] = useState(false); + const [tempSelectedKBIds, setTempSelectedKBIds] = useState([]); const { t } = useTranslation(); const handleFileUpload = async (file: File): Promise => { @@ -90,7 +101,10 @@ export default function DynamicFormItemComponent({ }, [config.type]); useEffect(() => { - if (config.type === DynamicFormItemType.KNOWLEDGE_BASE_SELECTOR) { + if ( + config.type === DynamicFormItemType.KNOWLEDGE_BASE_SELECTOR || + config.type === DynamicFormItemType.KNOWLEDGE_BASE_MULTI_SELECTOR + ) { httpClient .getKnowledgeBases() .then((resp) => { @@ -336,6 +350,128 @@ export default function DynamicFormItemComponent({ ); + case DynamicFormItemType.KNOWLEDGE_BASE_MULTI_SELECTOR: + return ( + <> +
+ {field.value && field.value.length > 0 ? ( +
+ {field.value.map((kbId: string) => { + const kb = knowledgeBases.find((base) => base.uuid === kbId); + if (!kb) return null; + return ( +
+
+
{kb.name}
+ {kb.description && ( +
+ {kb.description} +
+ )} +
+ +
+ ); + })} +
+ ) : ( +
+

+ {t('knowledge.noKnowledgeBaseSelected')} +

+
+ )} +
+ + + + {/* Knowledge Base Selection Dialog */} + + + + {t('knowledge.selectKnowledgeBases')} + +
+ {knowledgeBases.map((base) => { + const isSelected = tempSelectedKBIds.includes( + base.uuid ?? '', + ); + return ( +
{ + const kbId = base.uuid ?? ''; + setTempSelectedKBIds((prev) => + prev.includes(kbId) + ? prev.filter((id) => id !== kbId) + : [...prev, kbId], + ); + }} + > + +
+
{base.name}
+ {base.description && ( +
+ {base.description} +
+ )} +
+
+ ); + })} +
+ + + + +
+
+ + ); + case DynamicFormItemType.BOT_SELECTOR: return ( setNewKeyName(e.target.value)} + placeholder={t('common.name')} + className="mt-1" + /> + +
+ + setNewKeyDescription(e.target.value)} + placeholder={t('common.description')} + className="mt-1" + /> +
+ + + + + + + + + {/* Show Created Key Dialog */} + setCreatedKey(null)}> + + + {t('common.apiKeyCreated')} + + {t('common.apiKeyCreatedMessage')} + + +
+
+ +
+ + +
+
+
+ + + +
+
+ + {/* Create Webhook Dialog */} + + + + {t('common.createWebhook')} + +
+
+ + setNewWebhookName(e.target.value)} + placeholder={t('common.webhookName')} + className="mt-1" + /> +
+
+ + setNewWebhookUrl(e.target.value)} + placeholder="https://example.com/webhook" + className="mt-1" + /> +
+
+ + setNewWebhookDescription(e.target.value)} + placeholder={t('common.description')} + className="mt-1" + /> +
+
+ + +
+
+ + + + +
+
+ + {/* Delete API Key Confirmation Dialog */} + setCreatedKey(null)}> + + + {t('common.apiKeyCreated')} + + {t('common.apiKeyCreatedMessage')} + + +
+
+ +
+ + +
+
+
+ + + +
+
+ + {/* Delete Confirmation Dialog */} + + + setDeleteKeyId(null)} + /> + setDeleteKeyId(null)} + > + + {t('common.confirmDelete')} + + {t('common.apiKeyDeleteConfirm')} + + + + setDeleteKeyId(null)}> + {t('common.cancel')} + + deleteKeyId && handleDeleteApiKey(deleteKeyId)} + > + {t('common.delete')} + + + + + + + {/* Delete Webhook Confirmation Dialog */} + + + setDeleteWebhookId(null)} + /> + setDeleteWebhookId(null)} + > + + {t('common.confirmDelete')} + + {t('common.webhookDeleteConfirm')} + + + + setDeleteWebhookId(null)}> + {t('common.cancel')} + + + deleteWebhookId && handleDeleteWebhook(deleteWebhookId) + } + > + {t('common.delete')} + + + + + + + ); +} diff --git a/web/src/app/home/components/api-key-management-dialog/ApiKeyManagementDialog.tsx b/web/src/app/home/components/api-key-management-dialog/ApiKeyManagementDialog.tsx deleted file mode 100644 index 35a3781b..00000000 --- a/web/src/app/home/components/api-key-management-dialog/ApiKeyManagementDialog.tsx +++ /dev/null @@ -1,379 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { toast } from 'sonner'; -import { Copy, Trash2, Plus } from 'lucide-react'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, - DialogDescription, -} from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogPortal, - AlertDialogOverlay, -} from '@/components/ui/alert-dialog'; -import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; -import { backendClient } from '@/app/infra/http'; -import { extractI18nObject } from '@/i18n/I18nProvider'; - -interface ApiKey { - id: number; - name: string; - key: string; - description: string; - created_at: string; -} - -interface ApiKeyManagementDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export default function ApiKeyManagementDialog({ - open, - onOpenChange, -}: ApiKeyManagementDialogProps) { - const { t } = useTranslation(); - const [apiKeys, setApiKeys] = useState([]); - const [loading, setLoading] = useState(false); - const [showCreateDialog, setShowCreateDialog] = useState(false); - const [newKeyName, setNewKeyName] = useState(''); - const [newKeyDescription, setNewKeyDescription] = useState(''); - const [createdKey, setCreatedKey] = useState(null); - const [deleteKeyId, setDeleteKeyId] = useState(null); - - // 清理 body 样式,防止对话框关闭后页面无法交互 - useEffect(() => { - if (!deleteKeyId) { - const cleanup = () => { - document.body.style.removeProperty('pointer-events'); - }; - - cleanup(); - const timer = setTimeout(cleanup, 100); - return () => clearTimeout(timer); - } - }, [deleteKeyId]); - - useEffect(() => { - if (open) { - loadApiKeys(); - } - }, [open]); - - const loadApiKeys = async () => { - setLoading(true); - try { - const response = (await backendClient.get('/api/v1/apikeys')) as { - keys: ApiKey[]; - }; - setApiKeys(response.keys || []); - } catch (error) { - toast.error(`Failed to load API keys: ${error}`); - } finally { - setLoading(false); - } - }; - - const handleCreateApiKey = async () => { - if (!newKeyName.trim()) { - toast.error(t('common.apiKeyNameRequired')); - return; - } - - try { - const response = (await backendClient.post('/api/v1/apikeys', { - name: newKeyName, - description: newKeyDescription, - })) as { key: ApiKey }; - - setCreatedKey(response.key); - toast.success(t('common.apiKeyCreated')); - setNewKeyName(''); - setNewKeyDescription(''); - setShowCreateDialog(false); - loadApiKeys(); - } catch (error) { - toast.error(`Failed to create API key: ${error}`); - } - }; - - const handleDeleteApiKey = async (keyId: number) => { - try { - await backendClient.delete(`/api/v1/apikeys/${keyId}`); - toast.success(t('common.apiKeyDeleted')); - loadApiKeys(); - setDeleteKeyId(null); - } catch (error) { - toast.error(`Failed to delete API key: ${error}`); - } - }; - - const handleCopyKey = (key: string) => { - navigator.clipboard.writeText(key); - toast.success(t('common.apiKeyCopied')); - }; - - const maskApiKey = (key: string) => { - if (key.length <= 8) return key; - return `${key.substring(0, 8)}...${key.substring(key.length - 4)}`; - }; - - return ( - <> - { - // 如果删除确认框是打开的,不允许关闭主对话框 - if (!newOpen && deleteKeyId) { - return; - } - onOpenChange(newOpen); - }} - > - - - {t('common.manageApiKeys')} - - - {t('common.apiKeyHint')} -
{ - window.open( - extractI18nObject({ - zh_Hans: 'https://docs.langbot.app/zh/tags/readme', - en_US: 'https://docs.langbot.app/en/tags/readme', - }), - '_blank', - ); - }} - className="cursor-pointer" - > - - - -
-
-
-
- -
-
- -
- - {loading ? ( -
- {t('common.loading')} -
- ) : apiKeys.length === 0 ? ( -
- {t('common.noApiKeys')} -
- ) : ( -
- - - - {t('common.name')} - {t('common.apiKeyValue')} - - {t('common.actions')} - - - - - {apiKeys.map((key) => ( - - -
-
{key.name}
- {key.description && ( -
- {key.description} -
- )} -
-
- - - {maskApiKey(key.key)} - - - -
- - -
-
-
- ))} -
-
-
- )} -
- - - - -
-
- - {/* Create API Key Dialog */} - - - - {t('common.createApiKey')} - -
-
- - setNewKeyName(e.target.value)} - placeholder={t('common.name')} - className="mt-1" - /> -
-
- - setNewKeyDescription(e.target.value)} - placeholder={t('common.description')} - className="mt-1" - /> -
-
- - - - -
-
- - {/* Show Created Key Dialog */} - setCreatedKey(null)}> - - - {t('common.apiKeyCreated')} - - {t('common.apiKeyCreatedMessage')} - - -
-
- -
- - -
-
-
- - - -
-
- - {/* Delete Confirmation Dialog */} - - - setDeleteKeyId(null)} - /> - setDeleteKeyId(null)} - > - - {t('common.confirmDelete')} - - {t('common.apiKeyDeleteConfirm')} - - - - setDeleteKeyId(null)}> - {t('common.cancel')} - - deleteKeyId && handleDeleteApiKey(deleteKeyId)} - > - {t('common.delete')} - - - - - - - ); -} diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx index 444268b4..c3d80b22 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -25,7 +25,7 @@ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import { LanguageSelector } from '@/components/ui/language-selector'; import { Badge } from '@/components/ui/badge'; import PasswordChangeDialog from '@/app/home/components/password-change-dialog/PasswordChangeDialog'; -import ApiKeyManagementDialog from '@/app/home/components/api-key-management-dialog/ApiKeyManagementDialog'; +import ApiIntegrationDialog from '@/app/home/components/api-integration-dialog/ApiIntegrationDialog'; // TODO 侧边导航栏要加动画 export default function HomeSidebar({ @@ -236,7 +236,7 @@ export default function HomeSidebar({ } - name={t('common.apiKeys')} + name={t('common.apiIntegration')} /> - diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index f8935631..cf21c231 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -59,7 +59,9 @@ const enUS = { changePasswordSuccess: 'Password changed successfully', changePasswordFailed: 'Failed to change password, please check your current password', + apiIntegration: 'API Integration', apiKeys: 'API Keys', + manageApiIntegration: 'Manage API Integration', manageApiKeys: 'Manage API Keys', createApiKey: 'Create API Key', apiKeyName: 'API Key Name', @@ -74,6 +76,20 @@ const enUS = { noApiKeys: 'No API keys configured', apiKeyHint: 'API keys allow external systems to access LangBot Service APIs', + webhooks: 'Webhooks', + createWebhook: 'Create Webhook', + webhookName: 'Webhook Name', + webhookUrl: 'Webhook URL', + webhookDescription: 'Webhook Description', + webhookEnabled: 'Enabled', + webhookCreated: 'Webhook created successfully', + webhookDeleted: 'Webhook deleted successfully', + webhookDeleteConfirm: 'Are you sure you want to delete this webhook?', + webhookNameRequired: 'Webhook name is required', + webhookUrlRequired: 'Webhook URL is required', + noWebhooks: 'No webhooks configured', + webhookHint: + 'Webhooks allow LangBot to push person and group message events to external systems', actions: 'Actions', apiKeyCreatedMessage: 'Please copy this API key.', }, diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 197632d5..3be362c3 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -60,7 +60,9 @@ const jaJP = { changePasswordSuccess: 'パスワードの変更に成功しました', changePasswordFailed: 'パスワードの変更に失敗しました。現在のパスワードを確認してください', + apiIntegration: 'API統合', apiKeys: 'API キー', + manageApiIntegration: 'API統合の管理', manageApiKeys: 'API キーの管理', createApiKey: 'API キーを作成', apiKeyName: 'API キー名', @@ -75,6 +77,20 @@ const jaJP = { noApiKeys: 'API キーが設定されていません', apiKeyHint: 'API キーを使用すると、外部システムが LangBot Service API にアクセスできます', + webhooks: 'Webhooks', + createWebhook: 'Webhook を作成', + webhookName: 'Webhook 名', + webhookUrl: 'Webhook URL', + webhookDescription: 'Webhook の説明', + webhookEnabled: '有効', + webhookCreated: 'Webhook が正常に作成されました', + webhookDeleted: 'Webhook が正常に削除されました', + webhookDeleteConfirm: 'この Webhook を削除してもよろしいですか?', + webhookNameRequired: 'Webhook 名は必須です', + webhookUrlRequired: 'Webhook URL は必須です', + noWebhooks: 'Webhook が設定されていません', + webhookHint: + 'Webhook を使用すると、LangBot は個人メッセージとグループメッセージイベントを外部システムにプッシュできます', actions: 'アクション', apiKeyCreatedMessage: 'この API キーをコピーしてください。', }, diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index daa0cd0a..54cbb2eb 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -58,7 +58,9 @@ const zhHans = { passwordsDoNotMatch: '两次输入的密码不一致', changePasswordSuccess: '密码修改成功', changePasswordFailed: '密码修改失败,请检查当前密码是否正确', + apiIntegration: 'API 集成', apiKeys: 'API 密钥', + manageApiIntegration: '管理 API 集成', manageApiKeys: '管理 API 密钥', createApiKey: '创建 API 密钥', apiKeyName: 'API 密钥名称', @@ -72,6 +74,19 @@ const zhHans = { apiKeyCopied: 'API 密钥已复制到剪贴板', noApiKeys: '暂无 API 密钥', apiKeyHint: 'API 密钥允许外部系统访问 LangBot 的 Service API', + webhooks: 'Webhooks', + createWebhook: '创建 Webhook', + webhookName: 'Webhook 名称', + webhookUrl: 'Webhook URL', + webhookDescription: 'Webhook 描述', + webhookEnabled: '是否启用', + webhookCreated: 'Webhook 创建成功', + webhookDeleted: 'Webhook 删除成功', + webhookDeleteConfirm: '确定要删除此 Webhook 吗?', + webhookNameRequired: 'Webhook 名称不能为空', + webhookUrlRequired: 'Webhook URL 不能为空', + noWebhooks: '暂无 Webhook', + webhookHint: 'Webhook 允许 LangBot 将个人消息和群消息事件推送到外部系统', actions: '操作', apiKeyCreatedMessage: '请复制此 API 密钥。', }, diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 59b66a41..a0060e5f 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -58,7 +58,9 @@ const zhHant = { passwordsDoNotMatch: '兩次輸入的密碼不一致', changePasswordSuccess: '密碼修改成功', changePasswordFailed: '密碼修改失敗,請檢查當前密碼是否正確', + apiIntegration: 'API 整合', apiKeys: 'API 金鑰', + manageApiIntegration: '管理 API 整合', manageApiKeys: '管理 API 金鑰', createApiKey: '建立 API 金鑰', apiKeyName: 'API 金鑰名稱', @@ -72,6 +74,19 @@ const zhHant = { apiKeyCopied: 'API 金鑰已複製到剪貼簿', noApiKeys: '暫無 API 金鑰', apiKeyHint: 'API 金鑰允許外部系統訪問 LangBot 的 Service API', + webhooks: 'Webhooks', + createWebhook: '建立 Webhook', + webhookName: 'Webhook 名稱', + webhookUrl: 'Webhook URL', + webhookDescription: 'Webhook 描述', + webhookEnabled: '是否啟用', + webhookCreated: 'Webhook 建立成功', + webhookDeleted: 'Webhook 刪除成功', + webhookDeleteConfirm: '確定要刪除此 Webhook 嗎?', + webhookNameRequired: 'Webhook 名稱不能為空', + webhookUrlRequired: 'Webhook URL 不能為空', + noWebhooks: '暫無 Webhook', + webhookHint: 'Webhook 允許 LangBot 將個人訊息和群組訊息事件推送到外部系統', actions: '操作', apiKeyCreatedMessage: '請複製此 API 金鑰。', }, From 8cd50fbdb4ef063a5cca56133f37c1b377e50d84 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Mon, 10 Nov 2025 22:50:10 +0800 Subject: [PATCH 13/26] chore: bump version 4.5.0 --- docs/service-api-openapi.json | 2 +- pkg/utils/constants.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/service-api-openapi.json b/docs/service-api-openapi.json index 1e81adee..aab3758b 100644 --- a/docs/service-api-openapi.json +++ b/docs/service-api-openapi.json @@ -3,7 +3,7 @@ "info": { "title": "LangBot API with API Key Authentication", "description": "LangBot external service API documentation. These endpoints support API Key authentication \nfor external systems to programmatically access LangBot resources.\n\n**Authentication Methods:**\n- User Token (via `Authorization: Bearer `)\n- API Key (via `X-API-Key: ` or `Authorization: Bearer `)\n\nAll endpoints documented here accept BOTH authentication methods.\n", - "version": "4.4.1", + "version": "4.5.0", "contact": { "name": "LangBot", "url": "https://langbot.app" diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 56fa2e4d..c225ed82 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = 'v4.4.1' +semantic_version = 'v4.5.0' required_database_version = 11 """Tag the version of the database schema, used to check if the database needs to be migrated""" diff --git a/pyproject.toml b/pyproject.toml index cdb07dad..6073ed28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "langbot" -version = "4.4.1" +version = "4.5.0" description = "Easy-to-use global IM bot platform designed for LLM era" readme = "README.md" requires-python = ">=3.10.1,<4.0" From 1f877e2b8e7688318b3d5ee38675dd4f5eb7a7c8 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:49:43 +0800 Subject: [PATCH 14/26] Optimize model provider selection with category grouping (#1770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * Add provider category field to requesters and implement grouped dropdown Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Fix TypeScript type and prettier formatting issues Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Rename provider categories: aggregator→maas, self_deployed→self-hosted Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Move provider_category from metadata to spec section Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * perf: adjust category * perf: adjust data structure --------- 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 --- .../modelmgr/requesters/302aichatcmpl.yaml | 35 ++++++------- .../modelmgr/requesters/anthropicmsgs.yaml | 31 +++++------ .../modelmgr/requesters/bailianchatcmpl.yaml | 31 +++++------ .../modelmgr/requesters/chatcmpl.yaml | 35 ++++++------- .../requesters/compsharechatcmpl.yaml | 33 ++++++------ .../modelmgr/requesters/deepseekchatcmpl.yaml | 33 ++++++------ .../modelmgr/requesters/geminichatcmpl.yaml | 31 +++++------ .../modelmgr/requesters/giteeaichatcmpl.yaml | 35 ++++++------- .../modelmgr/requesters/jiekouaichatcmpl.yaml | 49 +++++++++--------- .../modelmgr/requesters/lmstudiochatcmpl.yaml | 33 ++++++------ .../requesters/modelscopechatcmpl.yaml | 45 ++++++++-------- .../modelmgr/requesters/moonshotchatcmpl.yaml | 31 +++++------ .../modelmgr/requesters/newapichatcmpl.yaml | 35 ++++++------- .../modelmgr/requesters/ollamachat.yaml | 33 ++++++------ .../requesters/openrouterchatcmpl.yaml | 33 ++++++------ .../modelmgr/requesters/ppiochatcmpl.yaml | 51 ++++++++++--------- .../modelmgr/requesters/qhaigcchatcmpl.yaml | 49 +++++++++--------- .../modelmgr/requesters/shengsuanyun.yaml | 49 +++++++++--------- .../requesters/siliconflowchatcmpl.yaml | 33 ++++++------ .../modelmgr/requesters/tokenpony.yaml | 35 ++++++------- .../modelmgr/requesters/volcarkchatcmpl.yaml | 31 +++++------ .../modelmgr/requesters/xaichatcmpl.yaml | 31 +++++------ .../modelmgr/requesters/zhipuaichatcmpl.yaml | 31 +++++------ .../models/component/ChooseRequesterEntity.ts | 1 + .../embedding-form/EmbeddingForm.tsx | 45 ++++++++++++++-- .../models/component/llm-form/LLMForm.tsx | 45 ++++++++++++++-- web/src/app/infra/entities/api/index.ts | 1 + web/src/i18n/locales/en-US.ts | 3 ++ web/src/i18n/locales/ja-JP.ts | 3 ++ web/src/i18n/locales/zh-Hans.ts | 3 ++ web/src/i18n/locales/zh-Hant.ts | 3 ++ 31 files changed, 522 insertions(+), 415 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/302aichatcmpl.yaml b/pkg/provider/modelmgr/requesters/302aichatcmpl.yaml index 5128f61d..4fc22be4 100644 --- a/pkg/provider/modelmgr/requesters/302aichatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/302aichatcmpl.yaml @@ -8,24 +8,25 @@ metadata: icon: 302ai.png spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://api.302.ai/v1" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://api.302.ai/v1 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm - - text-embedding + - llm + - text-embedding + provider_category: maas execution: python: path: ./302aichatcmpl.py - attr: AI302ChatCompletions \ No newline at end of file + attr: AI302ChatCompletions diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.yaml b/pkg/provider/modelmgr/requesters/anthropicmsgs.yaml index e3f745fb..0ef60d3e 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.yaml +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.yaml @@ -8,22 +8,23 @@ metadata: icon: anthropic.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://api.anthropic.com" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://api.anthropic.com + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm + - llm + provider_category: manufacturer execution: python: path: ./anthropicmsgs.py diff --git a/pkg/provider/modelmgr/requesters/bailianchatcmpl.yaml b/pkg/provider/modelmgr/requesters/bailianchatcmpl.yaml index 10aae30f..7c405232 100644 --- a/pkg/provider/modelmgr/requesters/bailianchatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/bailianchatcmpl.yaml @@ -8,22 +8,23 @@ metadata: icon: bailian.png spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://dashscope.aliyuncs.com/compatible-mode/v1" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://dashscope.aliyuncs.com/compatible-mode/v1 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm + - llm + provider_category: maas execution: python: path: ./bailianchatcmpl.py diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.yaml b/pkg/provider/modelmgr/requesters/chatcmpl.yaml index ff0de6f9..4f588fb2 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/chatcmpl.yaml @@ -8,24 +8,25 @@ metadata: icon: openai.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://api.openai.com/v1" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://api.openai.com/v1 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm - - text-embedding + - llm + - text-embedding + provider_category: manufacturer execution: python: path: ./chatcmpl.py - attr: OpenAIChatCompletions \ No newline at end of file + attr: OpenAIChatCompletions diff --git a/pkg/provider/modelmgr/requesters/compsharechatcmpl.yaml b/pkg/provider/modelmgr/requesters/compsharechatcmpl.yaml index 2b7f9a70..92fcafdc 100644 --- a/pkg/provider/modelmgr/requesters/compsharechatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/compsharechatcmpl.yaml @@ -8,23 +8,24 @@ metadata: icon: compshare.png spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://api.modelverse.cn/v1" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://api.modelverse.cn/v1 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm + - llm + provider_category: maas execution: python: path: ./compsharechatcmpl.py - attr: CompShareChatCompletions \ No newline at end of file + attr: CompShareChatCompletions diff --git a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.yaml b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.yaml index 9a22c5d9..8ef1fcf9 100644 --- a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.yaml @@ -8,23 +8,24 @@ metadata: icon: deepseek.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://api.deepseek.com" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://api.deepseek.com + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm + - llm + provider_category: manufacturer execution: python: path: ./deepseekchatcmpl.py - attr: DeepseekChatCompletions \ No newline at end of file + attr: DeepseekChatCompletions diff --git a/pkg/provider/modelmgr/requesters/geminichatcmpl.yaml b/pkg/provider/modelmgr/requesters/geminichatcmpl.yaml index 73fca19c..fdebe9b9 100644 --- a/pkg/provider/modelmgr/requesters/geminichatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/geminichatcmpl.yaml @@ -8,22 +8,23 @@ metadata: icon: gemini.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://generativelanguage.googleapis.com/v1beta/openai" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://generativelanguage.googleapis.com/v1beta/openai + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm + - llm + provider_category: manufacturer execution: python: path: ./geminichatcmpl.py diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.yaml b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.yaml index d1aec26b..e818bd7a 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.yaml @@ -8,24 +8,25 @@ metadata: icon: giteeai.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://ai.gitee.com/v1" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://ai.gitee.com/v1 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm - - text-embedding + - llm + - text-embedding + provider_category: maas execution: python: path: ./giteeaichatcmpl.py - attr: GiteeAIChatCompletions \ No newline at end of file + attr: GiteeAIChatCompletions diff --git a/pkg/provider/modelmgr/requesters/jiekouaichatcmpl.yaml b/pkg/provider/modelmgr/requesters/jiekouaichatcmpl.yaml index 44bfba38..3c791d73 100644 --- a/pkg/provider/modelmgr/requesters/jiekouaichatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/jiekouaichatcmpl.yaml @@ -8,31 +8,32 @@ metadata: icon: jiekouai.png spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://api.jiekou.ai/openai" - - name: args - label: - en_US: Args - zh_Hans: 附加参数 - type: object - required: true - default: {} - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: int - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://api.jiekou.ai/openai + - name: args + label: + en_US: Args + zh_Hans: 附加参数 + type: object + required: true + default: {} + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: int + required: true + default: 120 support_type: - - llm - - text-embedding + - llm + - text-embedding + provider_category: maas execution: python: path: ./jiekouaichatcmpl.py - attr: JieKouAIChatCompletions \ No newline at end of file + attr: JieKouAIChatCompletions diff --git a/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.yaml b/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.yaml index 8c44ab39..81dc82cf 100644 --- a/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/lmstudiochatcmpl.yaml @@ -8,23 +8,24 @@ metadata: icon: lmstudio.webp spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "http://127.0.0.1:1234/v1" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: http://127.0.0.1:1234/v1 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm - - text-embedding + - llm + - text-embedding + provider_category: self-hosted execution: python: path: ./lmstudiochatcmpl.py diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.yaml b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.yaml index a926d889..8d22002d 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.yaml @@ -8,29 +8,30 @@ metadata: icon: modelscope.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://api-inference.modelscope.cn/v1" - - name: args - label: - en_US: Args - zh_Hans: 附加参数 - type: object - required: true - default: {} - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: int - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://api-inference.modelscope.cn/v1 + - name: args + label: + en_US: Args + zh_Hans: 附加参数 + type: object + required: true + default: {} + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: int + required: true + default: 120 support_type: - - llm + - llm + provider_category: maas execution: python: path: ./modelscopechatcmpl.py diff --git a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.yaml b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.yaml index e51fdfa5..7a7e3060 100644 --- a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.yaml @@ -8,22 +8,23 @@ metadata: icon: moonshot.png spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://api.moonshot.ai/v1" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://api.moonshot.ai/v1 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm + - llm + provider_category: manufacturer execution: python: path: ./moonshotchatcmpl.py diff --git a/pkg/provider/modelmgr/requesters/newapichatcmpl.yaml b/pkg/provider/modelmgr/requesters/newapichatcmpl.yaml index 33573df5..e0f44e99 100644 --- a/pkg/provider/modelmgr/requesters/newapichatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/newapichatcmpl.yaml @@ -8,24 +8,25 @@ metadata: icon: newapi.png spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "http://localhost:3000/v1" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: http://localhost:3000/v1 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm - - text-embedding + - llm + - text-embedding + provider_category: maas execution: python: path: ./newapichatcmpl.py - attr: NewAPIChatCompletions \ No newline at end of file + attr: NewAPIChatCompletions diff --git a/pkg/provider/modelmgr/requesters/ollamachat.yaml b/pkg/provider/modelmgr/requesters/ollamachat.yaml index f7cdeeba..a724f8f8 100644 --- a/pkg/provider/modelmgr/requesters/ollamachat.yaml +++ b/pkg/provider/modelmgr/requesters/ollamachat.yaml @@ -8,23 +8,24 @@ metadata: icon: ollama.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "http://127.0.0.1:11434" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: http://127.0.0.1:11434 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm - - text-embedding + - llm + - text-embedding + provider_category: self-hosted execution: python: path: ./ollamachat.py diff --git a/pkg/provider/modelmgr/requesters/openrouterchatcmpl.yaml b/pkg/provider/modelmgr/requesters/openrouterchatcmpl.yaml index 8c957dba..f1603200 100644 --- a/pkg/provider/modelmgr/requesters/openrouterchatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/openrouterchatcmpl.yaml @@ -8,23 +8,24 @@ metadata: icon: openrouter.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://openrouter.ai/api/v1" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://openrouter.ai/api/v1 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm - - text-embedding + - llm + - text-embedding + provider_category: maas execution: python: path: ./openrouterchatcmpl.py diff --git a/pkg/provider/modelmgr/requesters/ppiochatcmpl.yaml b/pkg/provider/modelmgr/requesters/ppiochatcmpl.yaml index 90a81614..9e8eb1b0 100644 --- a/pkg/provider/modelmgr/requesters/ppiochatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/ppiochatcmpl.yaml @@ -3,36 +3,37 @@ kind: LLMAPIRequester metadata: name: ppio-chat-completions label: - en_US: ppio + en_US: ppio zh_Hans: 派欧云 icon: ppio.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://api.ppinfra.com/v3/openai" - - name: args - label: - en_US: Args - zh_Hans: 附加参数 - type: object - required: true - default: {} - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: int - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://api.ppinfra.com/v3/openai + - name: args + label: + en_US: Args + zh_Hans: 附加参数 + type: object + required: true + default: {} + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: int + required: true + default: 120 support_type: - - llm - - text-embedding + - llm + - text-embedding + provider_category: maas execution: python: path: ./ppiochatcmpl.py - attr: PPIOChatCompletions \ No newline at end of file + attr: PPIOChatCompletions diff --git a/pkg/provider/modelmgr/requesters/qhaigcchatcmpl.yaml b/pkg/provider/modelmgr/requesters/qhaigcchatcmpl.yaml index 2cd777d0..46ae1fad 100644 --- a/pkg/provider/modelmgr/requesters/qhaigcchatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/qhaigcchatcmpl.yaml @@ -8,31 +8,32 @@ metadata: icon: qhaigc.png spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://api.qhaigc.net/v1" - - name: args - label: - en_US: Args - zh_Hans: 附加参数 - type: object - required: true - default: {} - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: int - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://api.qhaigc.net/v1 + - name: args + label: + en_US: Args + zh_Hans: 附加参数 + type: object + required: true + default: {} + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: int + required: true + default: 120 support_type: - - llm - - text-embedding + - llm + - text-embedding + provider_category: maas execution: python: path: ./qhaigcchatcmpl.py - attr: QHAIGCChatCompletions \ No newline at end of file + attr: QHAIGCChatCompletions diff --git a/pkg/provider/modelmgr/requesters/shengsuanyun.yaml b/pkg/provider/modelmgr/requesters/shengsuanyun.yaml index 6668b677..77cf682c 100644 --- a/pkg/provider/modelmgr/requesters/shengsuanyun.yaml +++ b/pkg/provider/modelmgr/requesters/shengsuanyun.yaml @@ -8,31 +8,32 @@ metadata: icon: shengsuanyun.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://router.shengsuanyun.com/api/v1" - - name: args - label: - en_US: Args - zh_Hans: 附加参数 - type: object - required: true - default: {} - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: int - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://router.shengsuanyun.com/api/v1 + - name: args + label: + en_US: Args + zh_Hans: 附加参数 + type: object + required: true + default: {} + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: int + required: true + default: 120 support_type: - - llm - - text-embedding + - llm + - text-embedding + provider_category: maas execution: python: path: ./shengsuanyun.py - attr: ShengSuanYunChatCompletions \ No newline at end of file + attr: ShengSuanYunChatCompletions diff --git a/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.yaml b/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.yaml index 25a20653..28d3314a 100644 --- a/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/siliconflowchatcmpl.yaml @@ -8,23 +8,24 @@ metadata: icon: siliconflow.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://api.siliconflow.cn/v1" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://api.siliconflow.cn/v1 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm - - text-embedding + - llm + - text-embedding + provider_category: maas execution: python: path: ./siliconflowchatcmpl.py diff --git a/pkg/provider/modelmgr/requesters/tokenpony.yaml b/pkg/provider/modelmgr/requesters/tokenpony.yaml index 363583b0..f160bdea 100644 --- a/pkg/provider/modelmgr/requesters/tokenpony.yaml +++ b/pkg/provider/modelmgr/requesters/tokenpony.yaml @@ -8,24 +8,25 @@ metadata: icon: tokenpony.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://api.tokenpony.cn/v1" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://api.tokenpony.cn/v1 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm - - text-embedding + - llm + - text-embedding + provider_category: maas execution: python: path: ./tokenponychatcmpl.py - attr: TokenPonyChatCompletions \ No newline at end of file + attr: TokenPonyChatCompletions diff --git a/pkg/provider/modelmgr/requesters/volcarkchatcmpl.yaml b/pkg/provider/modelmgr/requesters/volcarkchatcmpl.yaml index c711ef2d..e5c82657 100644 --- a/pkg/provider/modelmgr/requesters/volcarkchatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/volcarkchatcmpl.yaml @@ -8,22 +8,23 @@ metadata: icon: volcark.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://ark.cn-beijing.volces.com/api/v3" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://ark.cn-beijing.volces.com/api/v3 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm + - llm + provider_category: maas execution: python: path: ./volcarkchatcmpl.py diff --git a/pkg/provider/modelmgr/requesters/xaichatcmpl.yaml b/pkg/provider/modelmgr/requesters/xaichatcmpl.yaml index 2769a402..2e721d70 100644 --- a/pkg/provider/modelmgr/requesters/xaichatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/xaichatcmpl.yaml @@ -8,22 +8,23 @@ metadata: icon: xai.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://api.x.ai/v1" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://api.x.ai/v1 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm + - llm + provider_category: manufacturer execution: python: path: ./xaichatcmpl.py diff --git a/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.yaml b/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.yaml index 34539d95..a4ebb2ec 100644 --- a/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.yaml +++ b/pkg/provider/modelmgr/requesters/zhipuaichatcmpl.yaml @@ -8,22 +8,23 @@ metadata: icon: zhipuai.svg spec: config: - - name: base_url - label: - en_US: Base URL - zh_Hans: 基础 URL - type: string - required: true - default: "https://open.bigmodel.cn/api/paas/v4" - - name: timeout - label: - en_US: Timeout - zh_Hans: 超时时间 - type: integer - required: true - default: 120 + - name: base_url + label: + en_US: Base URL + zh_Hans: 基础 URL + type: string + required: true + default: https://open.bigmodel.cn/api/paas/v4 + - name: timeout + label: + en_US: Timeout + zh_Hans: 超时时间 + type: integer + required: true + default: 120 support_type: - - llm + - llm + provider_category: manufacturer execution: python: path: ./zhipuaichatcmpl.py diff --git a/web/src/app/home/models/component/ChooseRequesterEntity.ts b/web/src/app/home/models/component/ChooseRequesterEntity.ts index 5728c1ce..e2113af4 100644 --- a/web/src/app/home/models/component/ChooseRequesterEntity.ts +++ b/web/src/app/home/models/component/ChooseRequesterEntity.ts @@ -1,4 +1,5 @@ export interface IChooseRequesterEntity { label: string; value: string; + provider_category?: string; } diff --git a/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx b/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx index 7d355c0c..ec9cac6c 100644 --- a/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx +++ b/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx @@ -34,6 +34,7 @@ import { SelectContent, SelectGroup, SelectItem, + SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select'; @@ -186,6 +187,7 @@ export default function EmbeddingForm({ return { label: extractI18nObject(item.label), value: item.name, + provider_category: item.spec.provider_category || 'manufacturer', }; }), ); @@ -425,11 +427,44 @@ export default function EmbeddingForm({ - {requesterNameList.map((item) => ( - - {item.label} - - ))} + + {t('models.modelManufacturer')} + + {requesterNameList + .filter( + (item) => + item.provider_category === 'manufacturer', + ) + .map((item) => ( + + {item.label} + + ))} + + + + {t('models.aggregationPlatform')} + + {requesterNameList + .filter((item) => item.provider_category === 'maas') + .map((item) => ( + + {item.label} + + ))} + + + {t('models.selfDeployed')} + {requesterNameList + .filter( + (item) => + item.provider_category === 'self-hosted', + ) + .map((item) => ( + + {item.label} + + ))} diff --git a/web/src/app/home/models/component/llm-form/LLMForm.tsx b/web/src/app/home/models/component/llm-form/LLMForm.tsx index c10f1e94..a20d6745 100644 --- a/web/src/app/home/models/component/llm-form/LLMForm.tsx +++ b/web/src/app/home/models/component/llm-form/LLMForm.tsx @@ -34,6 +34,7 @@ import { SelectContent, SelectGroup, SelectItem, + SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select'; @@ -203,6 +204,7 @@ export default function LLMForm({ return { label: extractI18nObject(item.label), value: item.name, + provider_category: item.spec.provider_category || 'manufacturer', }; }), ); @@ -440,11 +442,44 @@ export default function LLMForm({ - {requesterNameList.map((item) => ( - - {item.label} - - ))} + + {t('models.modelManufacturer')} + + {requesterNameList + .filter( + (item) => + item.provider_category === 'manufacturer', + ) + .map((item) => ( + + {item.label} + + ))} + + + + {t('models.aggregationPlatform')} + + {requesterNameList + .filter((item) => item.provider_category === 'maas') + .map((item) => ( + + {item.label} + + ))} + + + {t('models.selfDeployed')} + {requesterNameList + .filter( + (item) => + item.provider_category === 'self-hosted', + ) + .map((item) => ( + + {item.label} + + ))} diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index 152828fd..dbe7b145 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -29,6 +29,7 @@ export interface Requester { icon?: string; spec: { config: IDynamicFormItemSchema[]; + provider_category: string; }; } diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index cf21c231..9a88837d 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -141,6 +141,9 @@ const enUS = { selectModelProvider: 'Select Model Provider', modelProviderDescription: 'Please fill in the model name provided by the supplier', + modelManufacturer: 'Model Manufacturer', + aggregationPlatform: 'Aggregation Platform', + selfDeployed: 'Self-deployed', selectModel: 'Select Model', testSuccess: 'Test successful', testError: 'Test failed, please check your model configuration', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 3be362c3..0c02b7c3 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -144,6 +144,9 @@ const jaJP = { 'リクエストボディに追加されるパラメータ(max_tokens、temperature、top_p など)', selectModelProvider: 'モデルプロバイダーを選択', modelProviderDescription: 'プロバイダーが提供するモデル名をご入力ください', + modelManufacturer: 'モデルメーカー', + aggregationPlatform: 'アグリゲーションプラットフォーム', + selfDeployed: 'セルフデプロイ', selectModel: 'モデルを選択してください', testSuccess: 'テストに成功しました', testError: 'テストに失敗しました。モデル設定を確認してください', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 54cbb2eb..f41db5d2 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -138,6 +138,9 @@ const zhHans = { boolean: '布尔值', selectModelProvider: '选择模型供应商', modelProviderDescription: '请填写供应商向您提供的模型名称', + modelManufacturer: '模型厂商', + aggregationPlatform: '中转平台', + selfDeployed: '自部署', selectModel: '请选择模型', testSuccess: '测试成功', testError: '测试失败,请检查模型配置', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index a0060e5f..e7203803 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -138,6 +138,9 @@ const zhHant = { boolean: '布林值', selectModelProvider: '選擇模型供應商', modelProviderDescription: '請填寫供應商向您提供的模型名稱', + modelManufacturer: '模型廠商', + aggregationPlatform: '中轉平台', + selfDeployed: '自部署', selectModel: '請選擇模型', testSuccess: '測試成功', testError: '測試失敗,請檢查模型設定', From 0e0d7cc7b813e2291d914d31e711a612bfb22f96 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 11 Nov 2025 12:53:20 +0800 Subject: [PATCH 15/26] chore: add commit message format in AGENTS.md --- AGENTS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index c854059d..09bf5926 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,11 @@ Plugin Runtime automatically starts each installed plugin and interacts through - LangBot is a global project, any comments in code should be in English, and user experience should be considered in all aspects. - Thus you should consider the i18n support in all aspects. - LangBot is widely adopted in both toC and toB scenarios, so you should consider the compatibility and security in all aspects. +- If you were asked to make a commit, please follow the commit message format: + - format: (): + - 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. ## Some Principles From 524c56a12b4d5f89a20de377ea299bf35b2217d8 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:52:30 +0800 Subject: [PATCH 16/26] feat(web): add hover card to embedding model selector in knowledge base form (#1772) * Initial plan * feat: Add hover card with model details to embedding model selector in KB form - Updated KBForm.tsx to fetch full EmbeddingModel objects instead of simplified entities - Added HoverCard component to show model details (icon, description, base URL, extra args) when hovering over embedding model options - Removed unused IEmbeddingModelEntity import and embeddingModelNameList state - Made the embedding model selector consistent with LLM model selector behavior 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> --- .../knowledge/components/kb-form/KBForm.tsx | 106 +++++++++++++++--- 1 file changed, 89 insertions(+), 17 deletions(-) diff --git a/web/src/app/home/knowledge/components/kb-form/KBForm.tsx b/web/src/app/home/knowledge/components/kb-form/KBForm.tsx index 0804ee12..6c9bdd8c 100644 --- a/web/src/app/home/knowledge/components/kb-form/KBForm.tsx +++ b/web/src/app/home/knowledge/components/kb-form/KBForm.tsx @@ -13,7 +13,6 @@ import { FormMessage, FormDescription, } from '@/components/ui/form'; -import { IEmbeddingModelEntity } from './ChooseEntity'; import { httpClient } from '@/app/infra/http/HttpClient'; import { Select, @@ -23,8 +22,13 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { KnowledgeBase } from '@/app/infra/entities/api'; +import { KnowledgeBase, EmbeddingModel } from '@/app/infra/entities/api'; import { toast } from 'sonner'; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from '@/components/ui/hover-card'; const getFormSchema = (t: (key: string) => string) => z.object({ @@ -63,9 +67,7 @@ export default function KBForm({ }, }); - const [embeddingModelNameList, setEmbeddingModelNameList] = useState< - IEmbeddingModelEntity[] - >([]); + const [embeddingModels, setEmbeddingModels] = useState([]); useEffect(() => { getEmbeddingModelNameList().then(() => { @@ -97,14 +99,7 @@ export default function KBForm({ const getEmbeddingModelNameList = async () => { const resp = await httpClient.getProviderEmbeddingModels(); - setEmbeddingModelNameList( - resp.models.map((item) => { - return { - label: item.name, - value: item.uuid, - }; - }), - ); + setEmbeddingModels(resp.models); }; const onSubmit = (data: z.infer) => { @@ -216,10 +211,87 @@ export default function KBForm({ - {embeddingModelNameList.map((item) => ( - - {item.label} - + {embeddingModels.map((model) => ( + + + + {model.name} + + + +
+
+ icon +

+ {model.name} +

+
+

+ {model.description} +

+ {model.requester_config && ( +
+ + + + + Base URL: + + {model.requester_config.base_url} +
+ )} + {model.extra_args && + Object.keys(model.extra_args).length > + 0 && ( +
+
+ {t('models.extraParameters')} +
+
+ {Object.entries( + model.extra_args as Record< + string, + unknown + >, + ).map(([key, value]) => ( +
+ + {key}: + + + {JSON.stringify(value)} + +
+ ))} +
+
+ )} +
+
+
))}
From 02892e57bbbca4eefd78815f962210f28d054c2c Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 11 Nov 2025 18:10:31 +0800 Subject: [PATCH 17/26] fix: default is able to be deleted --- web/src/app/home/pipelines/PipelineDetailDialog.tsx | 3 --- .../components/pipeline-form/PipelineFormComponent.tsx | 8 +++++--- web/src/app/home/pipelines/page.tsx | 1 - 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/web/src/app/home/pipelines/PipelineDetailDialog.tsx b/web/src/app/home/pipelines/PipelineDetailDialog.tsx index c12bf768..1a849c91 100644 --- a/web/src/app/home/pipelines/PipelineDetailDialog.tsx +++ b/web/src/app/home/pipelines/PipelineDetailDialog.tsx @@ -39,7 +39,6 @@ export default function PipelineDialog({ onOpenChange, pipelineId: propPipelineId, isEditMode = false, - isDefaultPipeline = false, onFinish, onNewPipelineCreated, onDeletePipeline, @@ -133,7 +132,6 @@ export default function PipelineDialog({
{currentMode === 'config' && ( (false); const formSchema = isEditMode ? z.object({ @@ -133,6 +132,7 @@ export default function PipelineFormComponent({ httpClient .getPipeline(pipelineId || '') .then((resp: GetPipelineResponseData) => { + setIsDefaultPipeline(resp.pipeline.is_default ?? false); form.reset({ basic: { name: resp.pipeline.name, @@ -353,7 +353,9 @@ export default function PipelineFormComponent({ .getPipeline(pipelineId) .then((resp) => { const originalPipeline = resp.pipeline; - newPipelineName = `${originalPipeline.name}${t('pipelines.copySuffix')}`; + newPipelineName = `${originalPipeline.name}${t( + 'pipelines.copySuffix', + )}`; const newPipeline: Pipeline = { name: newPipelineName, description: originalPipeline.description, diff --git a/web/src/app/home/pipelines/page.tsx b/web/src/app/home/pipelines/page.tsx index c0b3930a..e84ae5b7 100644 --- a/web/src/app/home/pipelines/page.tsx +++ b/web/src/app/home/pipelines/page.tsx @@ -116,7 +116,6 @@ export default function PluginConfigPage() { onOpenChange={setDialogOpen} pipelineId={selectedPipelineId || undefined} isEditMode={isEditForm} - isDefaultPipeline={selectedPipelineIsDefault} onFinish={() => { getPipelines(); }} From 7a10dfdac1b534bf784329692db0a3538966f389 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 18:19:35 +0800 Subject: [PATCH 18/26] refactor: parallelize Docker multi-arch builds (arm64/amd64) (#1774) * Initial plan * refactor: parallelize Docker image builds for arm64 and amd64 Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * security: add explicit GITHUB_TOKEN permissions to workflow jobs Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * refactor: use build cache instead of intermediate tags Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * ci: perf trigger --------- 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 --- .github/workflows/build-docker-image.yml | 96 ++++++++++++++++++++---- 1 file changed, 83 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 7df1aeae..986a06ee 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -1,15 +1,17 @@ name: Build Docker Image on: - #防止fork乱用action设置只能手动触发构建 - workflow_dispatch: ## 发布release的时候会自动构建 release: types: [published] jobs: - publish-docker-image: + prepare: runs-on: ubuntu-latest - name: Build image - + name: Prepare build metadata + permissions: + contents: read + outputs: + version: ${{ steps.check_version.outputs.version }} + is_prerelease: ${{ github.event.release.prerelease }} steps: - name: Checkout uses: actions/checkout@v2 @@ -37,13 +39,81 @@ jobs: echo $GITHUB_REF echo ::set-output name=version::${GITHUB_REF} fi + + build-images: + runs-on: ubuntu-latest + needs: prepare + name: Build ${{ matrix.platform }} image + permissions: + contents: read + strategy: + matrix: + platform: [linux/amd64, linux/arm64] + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + persist-credentials: false + + - name: Set platform tag + id: platform_tag + run: | + # Convert platform to tag suffix (e.g., linux/amd64 -> amd64) + PLATFORM_TAG=$(echo ${{ matrix.platform }} | sed 's/linux\///g') + echo ::set-output name=tag::${PLATFORM_TAG} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to Registry run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }} - - name: Create Buildx - 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/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/arm64,linux/amd64 -t rockchin/langbot:${{ steps.check_version.outputs.version }} . --push \ No newline at end of file + + - name: Build and cache + run: | + docker buildx build \ + --platform ${{ matrix.platform }} \ + --cache-to type=registry,ref=rockchin/langbot:cache-${{ steps.platform_tag.outputs.tag }},mode=max \ + --cache-from type=registry,ref=rockchin/langbot:cache-${{ steps.platform_tag.outputs.tag }} \ + -t rockchin/langbot:${{ needs.prepare.outputs.version }} \ + . + + push-multiarch: + runs-on: ubuntu-latest + needs: [prepare, build-images] + name: Build and push multi-arch images + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Registry + run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push for Release + if: ${{ needs.prepare.outputs.is_prerelease == 'false' }} + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --cache-from type=registry,ref=rockchin/langbot:cache-amd64 \ + --cache-from type=registry,ref=rockchin/langbot:cache-arm64 \ + -t rockchin/langbot:${{ needs.prepare.outputs.version }} \ + -t rockchin/langbot:latest \ + --push \ + . + + - name: Build and push for Pre-release + if: ${{ needs.prepare.outputs.is_prerelease == 'true' }} + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --cache-from type=registry,ref=rockchin/langbot:cache-amd64 \ + --cache-from type=registry,ref=rockchin/langbot:cache-arm64 \ + -t rockchin/langbot:${{ needs.prepare.outputs.version }} \ + --push \ + . From cef24d8c4b7f08b7688e8ca030214f5876f3d73c Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 11 Nov 2025 18:24:06 +0800 Subject: [PATCH 19/26] fix: linter errors --- web/src/app/home/pipelines/page.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/web/src/app/home/pipelines/page.tsx b/web/src/app/home/pipelines/page.tsx index e84ae5b7..bb20abfa 100644 --- a/web/src/app/home/pipelines/page.tsx +++ b/web/src/app/home/pipelines/page.tsx @@ -22,9 +22,6 @@ export default function PluginConfigPage() { const [isEditForm, setIsEditForm] = useState(false); const [pipelineList, setPipelineList] = useState([]); const [selectedPipelineId, setSelectedPipelineId] = useState(''); - - const [selectedPipelineIsDefault, setSelectedPipelineIsDefault] = - useState(false); const [sortByValue, setSortByValue] = useState('created_at'); const [sortOrderValue, setSortOrderValue] = useState('DESC'); @@ -92,8 +89,6 @@ export default function PluginConfigPage() { const handleCreateNew = () => { setIsEditForm(false); setSelectedPipelineId(''); - - setSelectedPipelineIsDefault(false); setDialogOpen(true); }; From f25ac7853816227c68d23e4816c1af21331784cf Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 11 Nov 2025 19:03:29 +0800 Subject: [PATCH 20/26] ci: no longer build for linux/arm64 --- .github/workflows/build-docker-image.yml | 95 +++--------------------- 1 file changed, 12 insertions(+), 83 deletions(-) diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 986a06ee..17be2ca3 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -3,15 +3,12 @@ on: ## 发布release的时候会自动构建 release: types: [published] + workflow_dispatch: jobs: - prepare: + publish-docker-image: runs-on: ubuntu-latest - name: Prepare build metadata - permissions: - contents: read - outputs: - version: ${{ steps.check_version.outputs.version }} - is_prerelease: ${{ github.event.release.prerelease }} + name: Build image + steps: - name: Checkout uses: actions/checkout@v2 @@ -39,81 +36,13 @@ jobs: echo $GITHUB_REF echo ::set-output name=version::${GITHUB_REF} fi - - build-images: - runs-on: ubuntu-latest - needs: prepare - name: Build ${{ matrix.platform }} image - permissions: - contents: read - strategy: - matrix: - platform: [linux/amd64, linux/arm64] - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - persist-credentials: false - - - name: Set platform tag - id: platform_tag - run: | - # Convert platform to tag suffix (e.g., linux/amd64 -> amd64) - PLATFORM_TAG=$(echo ${{ matrix.platform }} | sed 's/linux\///g') - echo ::set-output name=tag::${PLATFORM_TAG} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Login to Registry run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and cache - run: | - docker buildx build \ - --platform ${{ matrix.platform }} \ - --cache-to type=registry,ref=rockchin/langbot:cache-${{ steps.platform_tag.outputs.tag }},mode=max \ - --cache-from type=registry,ref=rockchin/langbot:cache-${{ steps.platform_tag.outputs.tag }} \ - -t rockchin/langbot:${{ needs.prepare.outputs.version }} \ - . - - push-multiarch: - runs-on: ubuntu-latest - needs: [prepare, build-images] - name: Build and push multi-arch images - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - persist-credentials: false - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to Registry - run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push for Release - if: ${{ needs.prepare.outputs.is_prerelease == 'false' }} - run: | - docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --cache-from type=registry,ref=rockchin/langbot:cache-amd64 \ - --cache-from type=registry,ref=rockchin/langbot:cache-arm64 \ - -t rockchin/langbot:${{ needs.prepare.outputs.version }} \ - -t rockchin/langbot:latest \ - --push \ - . - - - name: Build and push for Pre-release - if: ${{ needs.prepare.outputs.is_prerelease == 'true' }} - run: | - docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --cache-from type=registry,ref=rockchin/langbot:cache-amd64 \ - --cache-from type=registry,ref=rockchin/langbot:cache-arm64 \ - -t rockchin/langbot:${{ needs.prepare.outputs.version }} \ - --push \ - . + - name: Create Buildx + 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 + - 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 \ No newline at end of file From 99f649c6b71c3b208277db1ca0a97e7d97c5467b Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Wed, 12 Nov 2025 11:15:27 +0800 Subject: [PATCH 21/26] docs: update README add jiekou.ai --- README.md | 3 ++- README_EN.md | 1 + README_JP.md | 1 + README_TW.md | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1c3c56bd..f11f521f 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ docker compose up -d | [胜算云](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 平台 | @@ -124,7 +125,7 @@ docker compose up -d | [火山方舟](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 | +| [百宝箱Tbox](https://www.tbox.cn/open) | ✅ | 蚂蚁百宝箱智能体平台,每月免费10亿大模型Token | ### TTS diff --git a/README_EN.md b/README_EN.md index 86a021fd..64c63876 100644 --- a/README_EN.md +++ b/README_EN.md @@ -105,6 +105,7 @@ Or visit the demo environment: https://demo.langbot.dev/ | [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | LLM and GPU resource platform | | [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) | ✅ | | diff --git a/README_JP.md b/README_JP.md index d4ac47cd..0afb811a 100644 --- a/README_JP.md +++ b/README_JP.md @@ -104,6 +104,7 @@ LangBotはBTPanelにリストされています。BTPanelをインストール | [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) | ✅ | | diff --git a/README_TW.md b/README_TW.md index 2c178a63..075b218e 100644 --- a/README_TW.md +++ b/README_TW.md @@ -107,6 +107,7 @@ docker compose up -d | [勝算雲](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 平台 | From 0f10cc62ece55a879db1294ec26562f28fa0f5e5 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:09:26 +0800 Subject: [PATCH 22/26] Add S3 object storage protocol support (#1780) * Initial plan * Add S3 object storage support with provider selection Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Fix lint issue: remove unused MagicMock import 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> --- pkg/storage/mgr.py | 15 +- pkg/storage/providers/s3storage.py | 145 ++++++++++++++++++ pyproject.toml | 1 + templates/config.yaml | 8 + tests/unit_tests/storage/__init__.py | 0 .../test_storage_provider_selection.py | 100 ++++++++++++ 6 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 pkg/storage/providers/s3storage.py create mode 100644 tests/unit_tests/storage/__init__.py create mode 100644 tests/unit_tests/storage/test_storage_provider_selection.py diff --git a/pkg/storage/mgr.py b/pkg/storage/mgr.py index 8d52e465..2f263f15 100644 --- a/pkg/storage/mgr.py +++ b/pkg/storage/mgr.py @@ -3,11 +3,11 @@ from __future__ import annotations from ..core import app from . import provider -from .providers import localstorage +from .providers import localstorage, s3storage class StorageMgr: - """存储管理器""" + """Storage manager""" ap: app.Application @@ -15,7 +15,16 @@ class StorageMgr: def __init__(self, ap: app.Application): self.ap = ap - self.storage_provider = localstorage.LocalStorageProvider(ap) async def initialize(self): + storage_config = self.ap.instance_config.data.get('storage', {}) + storage_type = storage_config.get('use', 'local') + + if storage_type == 's3': + self.storage_provider = s3storage.S3StorageProvider(self.ap) + self.ap.logger.info('Initialized S3 storage backend.') + else: + self.storage_provider = localstorage.LocalStorageProvider(self.ap) + self.ap.logger.info('Initialized local storage backend.') + await self.storage_provider.initialize() diff --git a/pkg/storage/providers/s3storage.py b/pkg/storage/providers/s3storage.py new file mode 100644 index 00000000..ed4fc443 --- /dev/null +++ b/pkg/storage/providers/s3storage.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +import boto3 +from botocore.exceptions import ClientError + +from ...core import app +from .. import provider + + +class S3StorageProvider(provider.StorageProvider): + """S3 object storage provider""" + + def __init__(self, ap: app.Application): + super().__init__(ap) + self.s3_client = None + self.bucket_name = None + + async def initialize(self): + """Initialize S3 client with configuration from config.yaml""" + storage_config = self.ap.instance_config.data.get('storage', {}) + s3_config = storage_config.get('s3', {}) + + # Get S3 configuration + endpoint_url = s3_config.get('endpoint_url', '') + access_key_id = s3_config.get('access_key_id', '') + secret_access_key = s3_config.get('secret_access_key', '') + region_name = s3_config.get('region', 'us-east-1') + self.bucket_name = s3_config.get('bucket', 'langbot-storage') + + # Initialize S3 client + session = boto3.session.Session() + self.s3_client = session.client( + service_name='s3', + region_name=region_name, + endpoint_url=endpoint_url if endpoint_url else None, + aws_access_key_id=access_key_id, + aws_secret_access_key=secret_access_key, + ) + + # Ensure bucket exists + try: + self.s3_client.head_bucket(Bucket=self.bucket_name) + except ClientError as e: + error_code = e.response['Error']['Code'] + if error_code == '404': + # Bucket doesn't exist, create it + try: + self.s3_client.create_bucket(Bucket=self.bucket_name) + self.ap.logger.info(f'Created S3 bucket: {self.bucket_name}') + except Exception as create_error: + self.ap.logger.error(f'Failed to create S3 bucket: {create_error}') + raise + else: + self.ap.logger.error(f'Failed to access S3 bucket: {e}') + raise + + async def save( + self, + key: str, + value: bytes, + ): + """Save bytes to S3""" + try: + self.s3_client.put_object( + Bucket=self.bucket_name, + Key=key, + Body=value, + ) + except Exception as e: + self.ap.logger.error(f'Failed to save to S3: {e}') + raise + + async def load( + self, + key: str, + ) -> bytes: + """Load bytes from S3""" + try: + response = self.s3_client.get_object( + Bucket=self.bucket_name, + Key=key, + ) + return response['Body'].read() + except Exception as e: + self.ap.logger.error(f'Failed to load from S3: {e}') + raise + + async def exists( + self, + key: str, + ) -> bool: + """Check if object exists in S3""" + try: + self.s3_client.head_object( + Bucket=self.bucket_name, + Key=key, + ) + return True + except ClientError as e: + if e.response['Error']['Code'] == '404': + return False + else: + self.ap.logger.error(f'Failed to check existence in S3: {e}') + raise + + async def delete( + self, + key: str, + ): + """Delete object from S3""" + try: + self.s3_client.delete_object( + Bucket=self.bucket_name, + Key=key, + ) + except Exception as e: + self.ap.logger.error(f'Failed to delete from S3: {e}') + raise + + async def delete_dir_recursive( + self, + dir_path: str, + ): + """Delete all objects with the given prefix (directory)""" + try: + # Ensure dir_path ends with / + if not dir_path.endswith('/'): + dir_path = dir_path + '/' + + # List all objects with the prefix + paginator = self.s3_client.get_paginator('list_objects_v2') + pages = paginator.paginate(Bucket=self.bucket_name, Prefix=dir_path) + + # Delete all objects + for page in pages: + if 'Contents' in page: + objects_to_delete = [{'Key': obj['Key']} for obj in page['Contents']] + if objects_to_delete: + self.s3_client.delete_objects( + Bucket=self.bucket_name, + Delete={'Objects': objects_to_delete}, + ) + except Exception as e: + self.ap.logger.error(f'Failed to delete directory from S3: {e}') + raise diff --git a/pyproject.toml b/pyproject.toml index 6073ed28..6a8d79f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ dependencies = [ "asyncpg>=0.30.0", "line-bot-sdk>=3.19.0", "tboxsdk>=0.0.10", + "boto3>=1.35.0", ] keywords = [ "bot", diff --git a/templates/config.yaml b/templates/config.yaml index 366ee782..28c4d57b 100644 --- a/templates/config.yaml +++ b/templates/config.yaml @@ -35,6 +35,14 @@ vdb: host: localhost port: 6333 api_key: '' +storage: + use: local + s3: + endpoint_url: '' + access_key_id: '' + secret_access_key: '' + region: 'us-east-1' + bucket: 'langbot-storage' plugin: enable: true runtime_ws_url: 'ws://langbot_plugin_runtime:5400/control/ws' diff --git a/tests/unit_tests/storage/__init__.py b/tests/unit_tests/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/storage/test_storage_provider_selection.py b/tests/unit_tests/storage/test_storage_provider_selection.py new file mode 100644 index 00000000..9f87f10a --- /dev/null +++ b/tests/unit_tests/storage/test_storage_provider_selection.py @@ -0,0 +1,100 @@ +""" +Tests for storage manager and provider selection +""" + +import pytest +from unittest.mock import Mock, AsyncMock, patch +from pkg.storage.mgr import StorageMgr +from pkg.storage.providers.localstorage import LocalStorageProvider +from pkg.storage.providers.s3storage import S3StorageProvider + + +class TestStorageProviderSelection: + """Test storage provider selection based on configuration""" + + @pytest.mark.asyncio + async def test_default_to_local_storage(self): + """Test that local storage is used by default when no config is provided""" + # Mock application + mock_app = Mock() + mock_app.instance_config = Mock() + mock_app.instance_config.data = {} + mock_app.logger = Mock() + + storage_mgr = StorageMgr(mock_app) + + with patch.object(LocalStorageProvider, 'initialize', new_callable=AsyncMock) as mock_init: + await storage_mgr.initialize() + assert isinstance(storage_mgr.storage_provider, LocalStorageProvider) + mock_init.assert_called_once() + + @pytest.mark.asyncio + async def test_explicit_local_storage(self): + """Test that local storage is used when explicitly configured""" + # Mock application + mock_app = Mock() + mock_app.instance_config = Mock() + mock_app.instance_config.data = { + 'storage': { + 'use': 'local' + } + } + mock_app.logger = Mock() + + storage_mgr = StorageMgr(mock_app) + + with patch.object(LocalStorageProvider, 'initialize', new_callable=AsyncMock) as mock_init: + await storage_mgr.initialize() + assert isinstance(storage_mgr.storage_provider, LocalStorageProvider) + mock_init.assert_called_once() + + @pytest.mark.asyncio + async def test_s3_storage_provider_selection(self): + """Test that S3 storage is used when configured""" + # Mock application + mock_app = Mock() + mock_app.instance_config = Mock() + mock_app.instance_config.data = { + 'storage': { + 'use': 's3', + 's3': { + 'endpoint_url': 'https://s3.amazonaws.com', + 'access_key_id': 'test_key', + 'secret_access_key': 'test_secret', + 'region': 'us-east-1', + 'bucket': 'test-bucket' + } + } + } + mock_app.logger = Mock() + + storage_mgr = StorageMgr(mock_app) + + with patch.object(S3StorageProvider, 'initialize', new_callable=AsyncMock) as mock_init: + await storage_mgr.initialize() + assert isinstance(storage_mgr.storage_provider, S3StorageProvider) + mock_init.assert_called_once() + + @pytest.mark.asyncio + async def test_invalid_storage_type_defaults_to_local(self): + """Test that invalid storage type defaults to local storage""" + # Mock application + mock_app = Mock() + mock_app.instance_config = Mock() + mock_app.instance_config.data = { + 'storage': { + 'use': 'invalid_type' + } + } + mock_app.logger = Mock() + + storage_mgr = StorageMgr(mock_app) + + with patch.object(LocalStorageProvider, 'initialize', new_callable=AsyncMock) as mock_init: + await storage_mgr.initialize() + assert isinstance(storage_mgr.storage_provider, LocalStorageProvider) + mock_init.assert_called_once() + + +if __name__ == '__main__': + pytest.main([__file__, '-v']) From 268ac8855a2bde4656699dbc469d04af95dd8f53 Mon Sep 17 00:00:00 2001 From: fdc310 <82008029+fdc310@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:28:38 +0800 Subject: [PATCH 23/26] fix: because launcher_id and sender_id This caused the user_id parameter of Coze to be too long. (#1778) --- pkg/provider/runners/cozeapi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/provider/runners/cozeapi.py b/pkg/provider/runners/cozeapi.py index 2a639cc3..0fdb6f9b 100644 --- a/pkg/provider/runners/cozeapi.py +++ b/pkg/provider/runners/cozeapi.py @@ -125,8 +125,8 @@ class CozeAPIRunner(runner.RequestRunner): 注意:由于cozepy没有提供非流式API,这里使用流式API并在结束后一次性返回完整内容 """ - user_id = f'{query.launcher_id}_{query.sender_id}' - + user_id = f'{query.launcher_type.value}_{query.launcher_id}' + # 预处理用户消息 additional_messages = await self._preprocess_user_message(query) @@ -206,7 +206,7 @@ class CozeAPIRunner(runner.RequestRunner): self, query: pipeline_query.Query ) -> typing.AsyncGenerator[provider_message.MessageChunk, None]: """调用聊天助手(流式)""" - user_id = f'{query.launcher_id}_{query.sender_id}' + user_id = f'{query.launcher_type.value}_{query.launcher_id}' # 预处理用户消息 additional_messages = await self._preprocess_user_message(query) From 43553e2c7d56fb5c9f7a806ae15a5b5081c531f1 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:25:11 +0800 Subject: [PATCH 24/26] feat: Add Kubernetes deployment configuration for cluster deployments (#1779) * Initial plan * feat: Add Kubernetes deployment configuration and guide Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * feat: Add test script and update docker-compose with k8s reference Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * doc: add k8s deployment doc in README --------- 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 --- README.md | 4 + README_EN.md | 4 + README_JP.md | 4 + README_TW.md | 4 + docker/README_K8S.md | 629 +++++++++++++++++++++++++++++++++++++ docker/deploy-k8s-test.sh | 74 +++++ docker/docker-compose.yaml | 2 + docker/kubernetes.yaml | 400 +++++++++++++++++++++++ 8 files changed, 1121 insertions(+) create mode 100644 docker/README_K8S.md create mode 100755 docker/deploy-k8s-test.sh create mode 100644 docker/kubernetes.yaml diff --git a/README.md b/README.md index f11f521f..754f6258 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,10 @@ docker compose up -d 直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。 +#### Kubernetes 部署 + +参考 [Kubernetes 部署](./docker/README_K8S.md) 文档。 + ## 😎 保持更新 点击仓库右上角 Star 和 Watch 按钮,获取最新动态。 diff --git a/README_EN.md b/README_EN.md index 64c63876..e020beba 100644 --- a/README_EN.md +++ b/README_EN.md @@ -55,6 +55,10 @@ Community contributed Zeabur template. 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. diff --git a/README_JP.md b/README_JP.md index 0afb811a..4679e37d 100644 --- a/README_JP.md +++ b/README_JP.md @@ -55,6 +55,10 @@ LangBotはBTPanelにリストされています。BTPanelをインストール リリースバージョンを直接使用して実行します。[手動デプロイ](https://docs.langbot.app/en/deploy/langbot/manual.html)のドキュメントを参照してください。 +#### Kubernetes デプロイ + +[Kubernetes デプロイ](./docker/README_K8S.md) ドキュメントを参照してください。 + ## 😎 最新情報を入手 リポジトリの右上にある Star と Watch ボタンをクリックして、最新の更新を取得してください。 diff --git a/README_TW.md b/README_TW.md index 075b218e..3250568a 100644 --- a/README_TW.md +++ b/README_TW.md @@ -57,6 +57,10 @@ docker compose up -d 直接使用發行版運行,查看文件[手動部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。 +#### Kubernetes 部署 + +參考 [Kubernetes 部署](./docker/README_K8S.md) 文件。 + ## 😎 保持更新 點擊倉庫右上角 Star 和 Watch 按鈕,獲取最新動態。 diff --git a/docker/README_K8S.md b/docker/README_K8S.md new file mode 100644 index 00000000..6a4889f0 --- /dev/null +++ b/docker/README_K8S.md @@ -0,0 +1,629 @@ +# LangBot Kubernetes 部署指南 / Kubernetes Deployment Guide + +[简体中文](#简体中文) | [English](#english) + +--- + +## 简体中文 + +### 概述 + +本指南提供了在 Kubernetes 集群中部署 LangBot 的完整步骤。Kubernetes 部署配置基于 `docker-compose.yaml`,适用于生产环境的容器化部署。 + +### 前置要求 + +- Kubernetes 集群(版本 1.19+) +- `kubectl` 命令行工具已配置并可访问集群 +- 集群中有可用的存储类(StorageClass)用于持久化存储(可选但推荐) +- 至少 2 vCPU 和 4GB RAM 的可用资源 + +### 架构说明 + +Kubernetes 部署包含以下组件: + +1. **langbot**: 主应用服务 + - 提供 Web UI(端口 5300) + - 处理平台 webhook(端口 2280-2290) + - 数据持久化卷 + +2. **langbot-plugin-runtime**: 插件运行时服务 + - WebSocket 通信(端口 5400) + - 插件数据持久化卷 + +3. **持久化存储**: + - `langbot-data`: LangBot 主数据 + - `langbot-plugins`: 插件文件 + - `langbot-plugin-runtime-data`: 插件运行时数据 + +### 快速开始 + +#### 1. 下载部署文件 + +```bash +# 克隆仓库 +git clone https://github.com/langbot-app/LangBot +cd LangBot/docker + +# 或直接下载 kubernetes.yaml +wget https://raw.githubusercontent.com/langbot-app/LangBot/main/docker/kubernetes.yaml +``` + +#### 2. 部署到 Kubernetes + +```bash +# 应用所有配置 +kubectl apply -f kubernetes.yaml + +# 检查部署状态 +kubectl get all -n langbot + +# 查看 Pod 日志 +kubectl logs -n langbot -l app=langbot -f +``` + +#### 3. 访问 LangBot + +默认情况下,LangBot 服务使用 ClusterIP 类型,只能在集群内部访问。您可以选择以下方式之一来访问: + +**选项 A: 端口转发(推荐用于测试)** + +```bash +kubectl port-forward -n langbot svc/langbot 5300:5300 +``` + +然后访问 http://localhost:5300 + +**选项 B: NodePort(适用于开发环境)** + +编辑 `kubernetes.yaml`,取消注释 NodePort Service 部分,然后: + +```bash +kubectl apply -f kubernetes.yaml +# 获取节点 IP +kubectl get nodes -o wide +# 访问 http://:30300 +``` + +**选项 C: LoadBalancer(适用于云环境)** + +编辑 `kubernetes.yaml`,取消注释 LoadBalancer Service 部分,然后: + +```bash +kubectl apply -f kubernetes.yaml +# 获取外部 IP +kubectl get svc -n langbot langbot-loadbalancer +# 访问 http:// +``` + +**选项 D: Ingress(推荐用于生产环境)** + +确保集群中已安装 Ingress Controller(如 nginx-ingress),然后: + +1. 编辑 `kubernetes.yaml` 中的 Ingress 配置 +2. 修改域名为您的实际域名 +3. 应用配置: + +```bash +kubectl apply -f kubernetes.yaml +# 访问 http://langbot.yourdomain.com +``` + +### 配置说明 + +#### 环境变量 + +在 `ConfigMap` 中配置环境变量: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: langbot-config + namespace: langbot +data: + TZ: "Asia/Shanghai" # 修改为您的时区 +``` + +#### 存储配置 + +默认使用动态存储分配。如果您有特定的 StorageClass,请在 PVC 中指定: + +```yaml +spec: + storageClassName: your-storage-class-name + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi +``` + +#### 资源限制 + +根据您的需求调整资源限制: + +```yaml +resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "4Gi" + cpu: "2000m" +``` + +### 常用操作 + +#### 查看日志 + +```bash +# 查看 LangBot 主服务日志 +kubectl logs -n langbot -l app=langbot -f + +# 查看插件运行时日志 +kubectl logs -n langbot -l app=langbot-plugin-runtime -f +``` + +#### 重启服务 + +```bash +# 重启 LangBot +kubectl rollout restart deployment/langbot -n langbot + +# 重启插件运行时 +kubectl rollout restart deployment/langbot-plugin-runtime -n langbot +``` + +#### 更新镜像 + +```bash +# 更新到最新版本 +kubectl set image deployment/langbot -n langbot langbot=rockchin/langbot:latest +kubectl set image deployment/langbot-plugin-runtime -n langbot langbot-plugin-runtime=rockchin/langbot:latest + +# 检查更新状态 +kubectl rollout status deployment/langbot -n langbot +``` + +#### 扩容(不推荐) + +注意:由于 LangBot 使用 ReadWriteOnce 的持久化存储,不支持多副本扩容。如需高可用,请考虑使用 ReadWriteMany 存储或其他架构方案。 + +#### 备份数据 + +```bash +# 备份 PVC 数据 +kubectl exec -n langbot -it -- tar czf /tmp/backup.tar.gz /app/data +kubectl cp langbot/:/tmp/backup.tar.gz ./backup.tar.gz +``` + +### 卸载 + +```bash +# 删除所有资源(保留 PVC) +kubectl delete deployment,service,configmap -n langbot --all + +# 删除 PVC(会删除数据) +kubectl delete pvc -n langbot --all + +# 删除命名空间 +kubectl delete namespace langbot +``` + +### 故障排查 + +#### Pod 无法启动 + +```bash +# 查看 Pod 状态 +kubectl get pods -n langbot + +# 查看详细信息 +kubectl describe pod -n langbot + +# 查看事件 +kubectl get events -n langbot --sort-by='.lastTimestamp' +``` + +#### 存储问题 + +```bash +# 检查 PVC 状态 +kubectl get pvc -n langbot + +# 检查 PV +kubectl get pv +``` + +#### 网络访问问题 + +```bash +# 检查 Service +kubectl get svc -n langbot + +# 检查端口转发 +kubectl port-forward -n langbot svc/langbot 5300:5300 +``` + +### 生产环境建议 + +1. **使用特定版本标签**:避免使用 `latest` 标签,使用具体版本号如 `rockchin/langbot:v1.0.0` +2. **配置资源限制**:根据实际负载调整 CPU 和内存限制 +3. **使用 Ingress + TLS**:配置 HTTPS 访问和证书管理 +4. **配置监控和告警**:集成 Prometheus、Grafana 等监控工具 +5. **定期备份**:配置自动备份策略保护数据 +6. **使用专用 StorageClass**:为生产环境配置高性能存储 +7. **配置亲和性规则**:确保 Pod 调度到合适的节点 + +### 高级配置 + +#### 使用 Secrets 管理敏感信息 + +如果需要配置 API 密钥等敏感信息: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: langbot-secrets + namespace: langbot +type: Opaque +data: + api_key: +``` + +然后在 Deployment 中引用: + +```yaml +env: +- name: API_KEY + valueFrom: + secretKeyRef: + name: langbot-secrets + key: api_key +``` + +#### 配置水平自动扩缩容(HPA) + +注意:需要确保使用 ReadWriteMany 存储类型 + +```yaml +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: langbot-hpa + namespace: langbot +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: langbot + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 +``` + +### 参考资源 + +- [LangBot 官方文档](https://docs.langbot.app) +- [Docker 部署文档](https://docs.langbot.app/zh/deploy/langbot/docker.html) +- [Kubernetes 官方文档](https://kubernetes.io/docs/) + +--- + +## English + +### Overview + +This guide provides complete steps for deploying LangBot in a Kubernetes cluster. The Kubernetes deployment configuration is based on `docker-compose.yaml` and is suitable for production containerized deployments. + +### Prerequisites + +- Kubernetes cluster (version 1.19+) +- `kubectl` command-line tool configured with cluster access +- Available StorageClass in the cluster for persistent storage (optional but recommended) +- At least 2 vCPU and 4GB RAM of available resources + +### Architecture + +The Kubernetes deployment includes the following components: + +1. **langbot**: Main application service + - Provides Web UI (port 5300) + - Handles platform webhooks (ports 2280-2290) + - Data persistence volume + +2. **langbot-plugin-runtime**: Plugin runtime service + - WebSocket communication (port 5400) + - Plugin data persistence volume + +3. **Persistent Storage**: + - `langbot-data`: LangBot main data + - `langbot-plugins`: Plugin files + - `langbot-plugin-runtime-data`: Plugin runtime data + +### Quick Start + +#### 1. Download Deployment Files + +```bash +# Clone repository +git clone https://github.com/langbot-app/LangBot +cd LangBot/docker + +# Or download kubernetes.yaml directly +wget https://raw.githubusercontent.com/langbot-app/LangBot/main/docker/kubernetes.yaml +``` + +#### 2. Deploy to Kubernetes + +```bash +# Apply all configurations +kubectl apply -f kubernetes.yaml + +# Check deployment status +kubectl get all -n langbot + +# View Pod logs +kubectl logs -n langbot -l app=langbot -f +``` + +#### 3. Access LangBot + +By default, LangBot service uses ClusterIP type, accessible only within the cluster. Choose one of the following methods to access: + +**Option A: Port Forwarding (Recommended for testing)** + +```bash +kubectl port-forward -n langbot svc/langbot 5300:5300 +``` + +Then visit http://localhost:5300 + +**Option B: NodePort (Suitable for development)** + +Edit `kubernetes.yaml`, uncomment the NodePort Service section, then: + +```bash +kubectl apply -f kubernetes.yaml +# Get node IP +kubectl get nodes -o wide +# Visit http://:30300 +``` + +**Option C: LoadBalancer (Suitable for cloud environments)** + +Edit `kubernetes.yaml`, uncomment the LoadBalancer Service section, then: + +```bash +kubectl apply -f kubernetes.yaml +# Get external IP +kubectl get svc -n langbot langbot-loadbalancer +# Visit http:// +``` + +**Option D: Ingress (Recommended for production)** + +Ensure an Ingress Controller (e.g., nginx-ingress) is installed in the cluster, then: + +1. Edit the Ingress configuration in `kubernetes.yaml` +2. Change the domain to your actual domain +3. Apply configuration: + +```bash +kubectl apply -f kubernetes.yaml +# Visit http://langbot.yourdomain.com +``` + +### Configuration + +#### Environment Variables + +Configure environment variables in ConfigMap: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: langbot-config + namespace: langbot +data: + TZ: "Asia/Shanghai" # Change to your timezone +``` + +#### Storage Configuration + +Uses dynamic storage provisioning by default. If you have a specific StorageClass, specify it in PVC: + +```yaml +spec: + storageClassName: your-storage-class-name + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi +``` + +#### Resource Limits + +Adjust resource limits based on your needs: + +```yaml +resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "4Gi" + cpu: "2000m" +``` + +### Common Operations + +#### View Logs + +```bash +# View LangBot main service logs +kubectl logs -n langbot -l app=langbot -f + +# View plugin runtime logs +kubectl logs -n langbot -l app=langbot-plugin-runtime -f +``` + +#### Restart Services + +```bash +# Restart LangBot +kubectl rollout restart deployment/langbot -n langbot + +# Restart plugin runtime +kubectl rollout restart deployment/langbot-plugin-runtime -n langbot +``` + +#### Update Images + +```bash +# Update to latest version +kubectl set image deployment/langbot -n langbot langbot=rockchin/langbot:latest +kubectl set image deployment/langbot-plugin-runtime -n langbot langbot-plugin-runtime=rockchin/langbot:latest + +# Check update status +kubectl rollout status deployment/langbot -n langbot +``` + +#### Scaling (Not Recommended) + +Note: Due to LangBot using ReadWriteOnce persistent storage, multi-replica scaling is not supported. For high availability, consider using ReadWriteMany storage or alternative architectures. + +#### Backup Data + +```bash +# Backup PVC data +kubectl exec -n langbot -it -- tar czf /tmp/backup.tar.gz /app/data +kubectl cp langbot/:/tmp/backup.tar.gz ./backup.tar.gz +``` + +### Uninstall + +```bash +# Delete all resources (keep PVCs) +kubectl delete deployment,service,configmap -n langbot --all + +# Delete PVCs (will delete data) +kubectl delete pvc -n langbot --all + +# Delete namespace +kubectl delete namespace langbot +``` + +### Troubleshooting + +#### Pods Not Starting + +```bash +# Check Pod status +kubectl get pods -n langbot + +# View detailed information +kubectl describe pod -n langbot + +# View events +kubectl get events -n langbot --sort-by='.lastTimestamp' +``` + +#### Storage Issues + +```bash +# Check PVC status +kubectl get pvc -n langbot + +# Check PV +kubectl get pv +``` + +#### Network Access Issues + +```bash +# Check Service +kubectl get svc -n langbot + +# Test port forwarding +kubectl port-forward -n langbot svc/langbot 5300:5300 +``` + +### Production Recommendations + +1. **Use specific version tags**: Avoid using `latest` tag, use specific version like `rockchin/langbot:v1.0.0` +2. **Configure resource limits**: Adjust CPU and memory limits based on actual load +3. **Use Ingress + TLS**: Configure HTTPS access and certificate management +4. **Configure monitoring and alerts**: Integrate monitoring tools like Prometheus, Grafana +5. **Regular backups**: Configure automated backup strategy to protect data +6. **Use dedicated StorageClass**: Configure high-performance storage for production +7. **Configure affinity rules**: Ensure Pods are scheduled to appropriate nodes + +### Advanced Configuration + +#### Using Secrets for Sensitive Information + +If you need to configure sensitive information like API keys: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: langbot-secrets + namespace: langbot +type: Opaque +data: + api_key: +``` + +Then reference in Deployment: + +```yaml +env: +- name: API_KEY + valueFrom: + secretKeyRef: + name: langbot-secrets + key: api_key +``` + +#### Configure Horizontal Pod Autoscaling (HPA) + +Note: Requires ReadWriteMany storage type + +```yaml +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: langbot-hpa + namespace: langbot +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: langbot + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 +``` + +### References + +- [LangBot Official Documentation](https://docs.langbot.app) +- [Docker Deployment Guide](https://docs.langbot.app/zh/deploy/langbot/docker.html) +- [Kubernetes Official Documentation](https://kubernetes.io/docs/) diff --git a/docker/deploy-k8s-test.sh b/docker/deploy-k8s-test.sh new file mode 100755 index 00000000..ff8e56e7 --- /dev/null +++ b/docker/deploy-k8s-test.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Quick test script for LangBot Kubernetes deployment +# This script helps you test the Kubernetes deployment locally + +set -e + +echo "🚀 LangBot Kubernetes Deployment Test Script" +echo "==============================================" +echo "" + +# Check for kubectl +if ! command -v kubectl &> /dev/null; then + echo "❌ kubectl is not installed. Please install kubectl first." + echo "Visit: https://kubernetes.io/docs/tasks/tools/" + exit 1 +fi + +echo "✓ kubectl is installed" + +# Check if kubectl can connect to a cluster +if ! kubectl cluster-info &> /dev/null; then + echo "" + echo "⚠️ No Kubernetes cluster found." + echo "" + echo "To test locally, you can use:" + echo " - kind: https://kind.sigs.k8s.io/" + echo " - minikube: https://minikube.sigs.k8s.io/" + echo " - k3s: https://k3s.io/" + echo "" + echo "Example with kind:" + echo " kind create cluster --name langbot-test" + echo "" + exit 1 +fi + +echo "✓ Connected to Kubernetes cluster" +kubectl cluster-info +echo "" + +# Ask user to confirm +read -p "Do you want to deploy LangBot to this cluster? (y/N) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Deployment cancelled." + exit 0 +fi + +echo "" +echo "📦 Deploying LangBot..." +kubectl apply -f kubernetes.yaml + +echo "" +echo "⏳ Waiting for pods to be ready..." +kubectl wait --for=condition=ready pod -l app=langbot -n langbot --timeout=300s +kubectl wait --for=condition=ready pod -l app=langbot-plugin-runtime -n langbot --timeout=300s + +echo "" +echo "✅ Deployment complete!" +echo "" +echo "📊 Deployment status:" +kubectl get all -n langbot + +echo "" +echo "🌐 To access LangBot Web UI, run:" +echo " kubectl port-forward -n langbot svc/langbot 5300:5300" +echo "" +echo "Then visit: http://localhost:5300" +echo "" +echo "📝 To view logs:" +echo " kubectl logs -n langbot -l app=langbot -f" +echo "" +echo "🗑️ To uninstall:" +echo " kubectl delete namespace langbot" +echo "" diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 107a9e26..f9bb6ffa 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -1,3 +1,5 @@ +# Docker Compose configuration for LangBot +# For Kubernetes deployment, see kubernetes.yaml and README_K8S.md version: "3" services: diff --git a/docker/kubernetes.yaml b/docker/kubernetes.yaml new file mode 100644 index 00000000..424c18eb --- /dev/null +++ b/docker/kubernetes.yaml @@ -0,0 +1,400 @@ +# Kubernetes Deployment for LangBot +# This file provides Kubernetes deployment manifests for LangBot based on docker-compose.yaml +# +# Usage: +# kubectl apply -f kubernetes.yaml +# +# Prerequisites: +# - A Kubernetes cluster (1.19+) +# - kubectl configured to communicate with your cluster +# - (Optional) A StorageClass for dynamic volume provisioning +# +# Components: +# - Namespace: langbot +# - PersistentVolumeClaims for data persistence +# - Deployments for langbot and langbot_plugin_runtime +# - Services for network access +# - ConfigMap for timezone configuration + +--- +# Namespace +apiVersion: v1 +kind: Namespace +metadata: + name: langbot + labels: + app: langbot + +--- +# PersistentVolumeClaim for LangBot data +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: langbot-data + namespace: langbot +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + # Uncomment and modify if you have a specific StorageClass + # storageClassName: your-storage-class + +--- +# PersistentVolumeClaim for LangBot plugins +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: langbot-plugins + namespace: langbot +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + # Uncomment and modify if you have a specific StorageClass + # storageClassName: your-storage-class + +--- +# PersistentVolumeClaim for Plugin Runtime data +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: langbot-plugin-runtime-data + namespace: langbot +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + # Uncomment and modify if you have a specific StorageClass + # storageClassName: your-storage-class + +--- +# ConfigMap for environment configuration +apiVersion: v1 +kind: ConfigMap +metadata: + name: langbot-config + namespace: langbot +data: + TZ: "Asia/Shanghai" + PLUGIN__RUNTIME_WS_URL: "ws://langbot-plugin-runtime:5400/control/ws" + +--- +# Deployment for LangBot Plugin Runtime +apiVersion: apps/v1 +kind: Deployment +metadata: + name: langbot-plugin-runtime + namespace: langbot + labels: + app: langbot-plugin-runtime +spec: + replicas: 1 + selector: + matchLabels: + app: langbot-plugin-runtime + template: + metadata: + labels: + app: langbot-plugin-runtime + spec: + containers: + - name: langbot-plugin-runtime + image: rockchin/langbot:latest + imagePullPolicy: Always + command: ["uv", "run", "-m", "langbot_plugin.cli.__init__", "rt"] + ports: + - containerPort: 5400 + name: runtime + protocol: TCP + env: + - name: TZ + valueFrom: + configMapKeyRef: + name: langbot-config + key: TZ + volumeMounts: + - name: plugin-data + mountPath: /app/data/plugins + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "2Gi" + cpu: "1000m" + # Liveness probe to restart container if it becomes unresponsive + livenessProbe: + tcpSocket: + port: 5400 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + # Readiness probe to know when container is ready to accept traffic + readinessProbe: + tcpSocket: + port: 5400 + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + volumes: + - name: plugin-data + persistentVolumeClaim: + claimName: langbot-plugin-runtime-data + restartPolicy: Always + +--- +# Service for LangBot Plugin Runtime +apiVersion: v1 +kind: Service +metadata: + name: langbot-plugin-runtime + namespace: langbot + labels: + app: langbot-plugin-runtime +spec: + type: ClusterIP + selector: + app: langbot-plugin-runtime + ports: + - port: 5400 + targetPort: 5400 + protocol: TCP + name: runtime + +--- +# Deployment for LangBot +apiVersion: apps/v1 +kind: Deployment +metadata: + name: langbot + namespace: langbot + labels: + app: langbot +spec: + replicas: 1 + selector: + matchLabels: + app: langbot + template: + metadata: + labels: + app: langbot + spec: + containers: + - name: langbot + image: rockchin/langbot:latest + imagePullPolicy: Always + ports: + - containerPort: 5300 + name: web + protocol: TCP + - containerPort: 2280 + name: webhook-start + protocol: TCP + # Note: Kubernetes doesn't support port ranges directly in container ports + # The webhook ports 2280-2290 are available, but we only expose the start of the range + # If you need all ports exposed, consider using a Service with multiple port definitions + env: + - name: TZ + valueFrom: + configMapKeyRef: + name: langbot-config + key: TZ + - name: PLUGIN__RUNTIME_WS_URL + valueFrom: + configMapKeyRef: + name: langbot-config + key: PLUGIN__RUNTIME_WS_URL + volumeMounts: + - name: data + mountPath: /app/data + - name: plugins + mountPath: /app/plugins + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "4Gi" + cpu: "2000m" + # Liveness probe to restart container if it becomes unresponsive + livenessProbe: + httpGet: + path: / + port: 5300 + initialDelaySeconds: 60 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + # Readiness probe to know when container is ready to accept traffic + readinessProbe: + httpGet: + path: / + port: 5300 + initialDelaySeconds: 30 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + volumes: + - name: data + persistentVolumeClaim: + claimName: langbot-data + - name: plugins + persistentVolumeClaim: + claimName: langbot-plugins + restartPolicy: Always + +--- +# Service for LangBot (ClusterIP for internal access) +apiVersion: v1 +kind: Service +metadata: + name: langbot + namespace: langbot + labels: + app: langbot +spec: + type: ClusterIP + selector: + app: langbot + ports: + - port: 5300 + targetPort: 5300 + protocol: TCP + name: web + - port: 2280 + targetPort: 2280 + protocol: TCP + name: webhook-2280 + - port: 2281 + targetPort: 2281 + protocol: TCP + name: webhook-2281 + - port: 2282 + targetPort: 2282 + protocol: TCP + name: webhook-2282 + - port: 2283 + targetPort: 2283 + protocol: TCP + name: webhook-2283 + - port: 2284 + targetPort: 2284 + protocol: TCP + name: webhook-2284 + - port: 2285 + targetPort: 2285 + protocol: TCP + name: webhook-2285 + - port: 2286 + targetPort: 2286 + protocol: TCP + name: webhook-2286 + - port: 2287 + targetPort: 2287 + protocol: TCP + name: webhook-2287 + - port: 2288 + targetPort: 2288 + protocol: TCP + name: webhook-2288 + - port: 2289 + targetPort: 2289 + protocol: TCP + name: webhook-2289 + - port: 2290 + targetPort: 2290 + protocol: TCP + name: webhook-2290 + +--- +# Ingress for external access (Optional - requires Ingress Controller) +# Uncomment and modify the following section if you want to expose LangBot via Ingress +# apiVersion: networking.k8s.io/v1 +# kind: Ingress +# metadata: +# name: langbot-ingress +# namespace: langbot +# annotations: +# # Uncomment and modify based on your ingress controller +# # nginx.ingress.kubernetes.io/rewrite-target: / +# # cert-manager.io/cluster-issuer: letsencrypt-prod +# spec: +# ingressClassName: nginx # Change based on your ingress controller +# rules: +# - host: langbot.yourdomain.com # Change to your domain +# http: +# paths: +# - path: / +# pathType: Prefix +# backend: +# service: +# name: langbot +# port: +# number: 5300 +# # Uncomment for TLS/HTTPS +# # tls: +# # - hosts: +# # - langbot.yourdomain.com +# # secretName: langbot-tls + +--- +# Service for LangBot with LoadBalancer (Alternative to Ingress) +# Uncomment the following if you want to expose LangBot directly via LoadBalancer +# This is useful in cloud environments (AWS, GCP, Azure, etc.) +# apiVersion: v1 +# kind: Service +# metadata: +# name: langbot-loadbalancer +# namespace: langbot +# labels: +# app: langbot +# spec: +# type: LoadBalancer +# selector: +# app: langbot +# ports: +# - port: 80 +# targetPort: 5300 +# protocol: TCP +# name: web +# - port: 2280 +# targetPort: 2280 +# protocol: TCP +# name: webhook-start +# # Add more webhook ports as needed + +--- +# Service for LangBot with NodePort (Alternative for exposing service) +# Uncomment if you want to expose LangBot via NodePort +# This is useful for testing or when LoadBalancer is not available +# apiVersion: v1 +# kind: Service +# metadata: +# name: langbot-nodeport +# namespace: langbot +# labels: +# app: langbot +# spec: +# type: NodePort +# selector: +# app: langbot +# ports: +# - port: 5300 +# targetPort: 5300 +# nodePort: 30300 # Must be in range 30000-32767 +# protocol: TCP +# name: web +# - port: 2280 +# targetPort: 2280 +# nodePort: 30280 # Must be in range 30000-32767 +# protocol: TCP +# name: webhook From 58369480e227c9068984418ec7f16f78705d1f7a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 12:38:45 +0800 Subject: [PATCH 25/26] fix: add scrollbar to pipeline extensions tab when content overflows (#1781) * Initial plan * feat: add scrollbar to pipeline extensions tab 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> --- web/src/app/home/pipelines/PipelineDetailDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/app/home/pipelines/PipelineDetailDialog.tsx b/web/src/app/home/pipelines/PipelineDetailDialog.tsx index 1a849c91..35a780fc 100644 --- a/web/src/app/home/pipelines/PipelineDetailDialog.tsx +++ b/web/src/app/home/pipelines/PipelineDetailDialog.tsx @@ -190,7 +190,7 @@ export default function PipelineDialog({ {getDialogTitle()}
{currentMode === 'config' && ( From 6a24c951e02fd404a081bd6f8bf2f4f138586ecc Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 16 Nov 2025 14:58:54 +0800 Subject: [PATCH 26/26] chore: bump langbot-plugin to 0.1.11b1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6a8d79f2..0dbe4fef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ dependencies = [ "langchain-text-splitters>=0.0.1", "chromadb>=0.4.24", "qdrant-client (>=1.15.1,<2.0.0)", - "langbot-plugin==0.1.10", + "langbot-plugin==0.1.11b1", "asyncpg>=0.30.0", "line-bot-sdk>=3.19.0", "tboxsdk>=0.0.10",