diff --git a/pkg/api/http/controller/groups/pipelines/__init__.py b/pkg/api/http/controller/groups/pipelines/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/api/http/controller/groups/pipelines.py b/pkg/api/http/controller/groups/pipelines/pipelines.py similarity index 98% rename from pkg/api/http/controller/groups/pipelines.py rename to pkg/api/http/controller/groups/pipelines/pipelines.py index 1a8036cc6..96ca239a1 100644 --- a/pkg/api/http/controller/groups/pipelines.py +++ b/pkg/api/http/controller/groups/pipelines/pipelines.py @@ -2,7 +2,7 @@ import quart -from .. import group +from ... import group @group.group_class('pipelines', '/api/v1/pipelines') diff --git a/pkg/api/http/controller/groups/pipelines/webchat.py b/pkg/api/http/controller/groups/pipelines/webchat.py new file mode 100644 index 000000000..005738db5 --- /dev/null +++ b/pkg/api/http/controller/groups/pipelines/webchat.py @@ -0,0 +1,79 @@ +import quart + +from ... import group + + +@group.group_class('webchat', '/api/v1/pipelines//chat') +class WebChatDebugRouterGroup(group.RouterGroup): + async def initialize(self) -> None: + @self.route('/send', methods=['POST']) + async def send_message(pipeline_uuid: str) -> str: + """发送调试消息到流水线""" + try: + data = await quart.request.get_json() + session_type = data.get('session_type', 'person') + message_chain_obj = data.get('message', []) + + if not message_chain_obj: + return self.http_status(400, -1, 'message is required') + + if session_type not in ['person', 'group']: + return self.http_status(400, -1, 'session_type must be person or group') + + webchat_adapter = self.ap.platform_mgr.webchat_proxy_bot.adapter + + if not webchat_adapter: + return self.http_status(404, -1, 'WebChat adapter not found') + + result = await webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj) + + return self.success( + data={ + 'message': result, + } + ) + + except Exception as e: + return self.http_status(500, -1, f'Internal server error: {str(e)}') + + @self.route('/messages/', methods=['GET']) + async def get_messages(pipeline_uuid: str, session_type: str) -> str: + """获取调试消息历史""" + try: + if session_type not in ['person', 'group']: + return self.http_status(400, -1, 'session_type must be person or group') + + webchat_adapter = self.ap.platform_mgr.webchat_proxy_bot.adapter + + if not webchat_adapter: + return self.http_status(404, -1, 'WebChat adapter not found') + + messages = webchat_adapter.get_webchat_messages(pipeline_uuid, session_type) + + return self.success(data={'messages': messages}) + + except Exception as e: + return self.http_status(500, -1, f'Internal server error: {str(e)}') + + @self.route('/reset/', methods=['POST']) + async def reset_session(session_type: str) -> str: + """重置调试会话""" + try: + if session_type not in ['person', 'group']: + return self.http_status(400, -1, 'session_type must be person or group') + + webchat_adapter = None + for bot in self.ap.platform_mgr.bots: + if hasattr(bot.adapter, '__class__') and bot.adapter.__class__.__name__ == 'WebChatAdapter': + webchat_adapter = bot.adapter + break + + if not webchat_adapter: + return self.http_status(404, -1, 'WebChat adapter not found') + + webchat_adapter.reset_debug_session(session_type) + + return self.success(data={'message': 'Session reset successfully'}) + + except Exception as e: + return self.http_status(500, -1, f'Internal server error: {str(e)}') diff --git a/pkg/api/http/controller/main.py b/pkg/api/http/controller/main.py index 60882359f..eb434d883 100644 --- a/pkg/api/http/controller/main.py +++ b/pkg/api/http/controller/main.py @@ -13,10 +13,12 @@ from . import group from .groups import provider as groups_provider from .groups import platform as groups_platform +from .groups import pipelines as groups_pipelines importutil.import_modules_in_pkg(groups) importutil.import_modules_in_pkg(groups_provider) importutil.import_modules_in_pkg(groups_platform) +importutil.import_modules_in_pkg(groups_pipelines) class HTTPController: diff --git a/pkg/persistence/mgr.py b/pkg/persistence/mgr.py index f0d7459b1..606aa9fd6 100644 --- a/pkg/persistence/mgr.py +++ b/pkg/persistence/mgr.py @@ -66,13 +66,15 @@ async def create_tables(self): # write default pipeline result = await self.execute_async(sqlalchemy.select(pipeline.LegacyPipeline)) + default_pipeline_uuid = None if result.first() is None: self.ap.logger.info('Creating default pipeline...') pipeline_config = json.load(open('templates/default-pipeline-config.json', 'r', encoding='utf-8')) + default_pipeline_uuid = str(uuid.uuid4()) pipeline_data = { - 'uuid': str(uuid.uuid4()), + 'uuid': default_pipeline_uuid, 'for_version': self.ap.ver_mgr.get_current_version(), 'stages': pipeline_service.default_stage_order, 'is_default': True, @@ -82,6 +84,7 @@ async def create_tables(self): } await self.execute_async(sqlalchemy.insert(pipeline.LegacyPipeline).values(pipeline_data)) + # ================================= # run migrations diff --git a/pkg/pipeline/controller.py b/pkg/pipeline/controller.py index 052187a22..6679bd88a 100644 --- a/pkg/pipeline/controller.py +++ b/pkg/pipeline/controller.py @@ -51,11 +51,10 @@ async def _process_query(selected_query: entities.Query): # find pipeline # Here firstly find the bot, then find the pipeline, in case the bot adapter's config is not the latest one. # Like aiocqhttp, once a client is connected, even the adapter was updated and restarted, the existing client connection will not be affected. - bot = await self.ap.platform_mgr.get_bot_by_uuid(selected_query.bot_uuid) - if bot: - pipeline = await self.ap.pipeline_mgr.get_pipeline_by_uuid( - bot.bot_entity.use_pipeline_uuid - ) + pipeline_uuid = selected_query.pipeline_uuid + + if pipeline_uuid: + pipeline = await self.ap.pipeline_mgr.get_pipeline_by_uuid(pipeline_uuid) if pipeline: await pipeline.run(selected_query) diff --git a/pkg/pipeline/pool.py b/pkg/pipeline/pool.py index 3da4e19bb..6975e53c2 100644 --- a/pkg/pipeline/pool.py +++ b/pkg/pipeline/pool.py @@ -35,6 +35,7 @@ async def add_query( message_event: platform_events.MessageEvent, message_chain: platform_message.MessageChain, adapter: msadapter.MessagePlatformAdapter, + pipeline_uuid: typing.Optional[str] = None, ) -> entities.Query: async with self.condition: query = entities.Query( @@ -48,6 +49,7 @@ async def add_query( resp_messages=[], resp_message_chain=[], adapter=adapter, + pipeline_uuid=pipeline_uuid, ) self.queries.append(query) self.query_id_counter += 1 diff --git a/pkg/platform/botmgr.py b/pkg/platform/botmgr.py index b24e14aa3..128039167 100644 --- a/pkg/platform/botmgr.py +++ b/pkg/platform/botmgr.py @@ -5,7 +5,6 @@ import traceback import sqlalchemy - # FriendMessage, Image, MessageChain, Plain from . import adapter as msadapter @@ -78,6 +77,7 @@ async def on_friend_message( message_event=event, message_chain=event.message_chain, adapter=adapter, + pipeline_uuid=self.bot_entity.use_pipeline_uuid, ) async def on_group_message( @@ -102,6 +102,7 @@ async def on_group_message( message_event=event, message_chain=event.message_chain, adapter=adapter, + pipeline_uuid=self.bot_entity.use_pipeline_uuid, ) self.adapter.register_listener(platform_events.FriendMessage, on_friend_message) @@ -144,6 +145,8 @@ class PlatformManager: bots: list[RuntimeBot] + webchat_proxy_bot: RuntimeBot + adapter_components: list[engine.Component] adapter_dict: dict[str, type[msadapter.MessagePlatformAdapter]] @@ -161,6 +164,31 @@ async def initialize(self): adapter_dict[component.metadata.name] = component.get_python_component_class() self.adapter_dict = adapter_dict + webchat_adapter_class = self.adapter_dict['webchat'] + + # initialize webchat adapter + webchat_logger = EventLogger(name='webchat-adapter', ap=self.ap) + webchat_adapter_inst = webchat_adapter_class( + {}, + self.ap, + webchat_logger, + ) + + self.webchat_proxy_bot = RuntimeBot( + ap=self.ap, + bot_entity=persistence_bot.Bot( + uuid='webchat-proxy-bot', + name='WebChat', + description='', + adapter='webchat', + adapter_config={}, + enable=True, + ), + adapter=webchat_adapter_inst, + logger=webchat_logger, + ) + await self.webchat_proxy_bot.initialize() + await self.load_bots_from_db() def get_running_adapters(self) -> list[msadapter.MessagePlatformAdapter]: @@ -220,7 +248,9 @@ async def remove_bot(self, bot_uuid: str): return def get_available_adapters_info(self) -> list[dict]: - return [component.to_plain_dict() for component in self.adapter_components] + return [ + component.to_plain_dict() for component in self.adapter_components if component.metadata.name != 'webchat' + ] def get_available_adapter_info_by_name(self, name: str) -> dict | None: for component in self.adapter_components: @@ -273,6 +303,8 @@ async def write_back_config( async def run(self): # This method will only be called when the application launching + await self.webchat_proxy_bot.run() + for bot in self.bots: if bot.enable: await bot.run() diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py new file mode 100644 index 000000000..51b0479f2 --- /dev/null +++ b/pkg/platform/sources/webchat.py @@ -0,0 +1,209 @@ +import asyncio +import logging +import typing +from datetime import datetime + +from pydantic import BaseModel + +from .. import adapter as msadapter +from ..types import events as platform_events, message as platform_message, entities as platform_entities +from ...core import app +from ..logger import EventLogger + +logger = logging.getLogger(__name__) + + +class WebChatMessage(BaseModel): + id: int + role: str + content: str + message_chain: list[dict] + timestamp: str + + +class WebChatSession: + id: str + message_lists: dict[str, list[WebChatMessage]] = {} + resp_waiters: dict[int, asyncio.Future[WebChatMessage]] + + def __init__(self, id: str): + self.id = id + self.message_lists = {} + self.resp_waiters = {} + + def get_message_list(self, pipeline_uuid: str) -> list[WebChatMessage]: + if pipeline_uuid not in self.message_lists: + self.message_lists[pipeline_uuid] = [] + + return self.message_lists[pipeline_uuid] + + +class WebChatAdapter(msadapter.MessagePlatformAdapter): + """WebChat调试适配器,用于流水线调试""" + + webchat_person_session: WebChatSession + webchat_group_session: WebChatSession + + listeners: typing.Dict[ + typing.Type[platform_events.Event], + typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], None], + ] = {} + + def __init__(self, config: dict, ap: app.Application, logger: EventLogger): + self.ap = ap + self.logger = logger + self.config = config + + self.webchat_person_session = WebChatSession(id='webchatperson') + self.webchat_group_session = WebChatSession(id='webchatgroup') + + self.bot_account_id = 'webchatbot' + + async def send_message( + self, + target_type: str, + target_id: str, + message: platform_message.MessageChain, + ) -> dict: + """发送消息到调试会话""" + session_key = target_id + + if session_key not in self.debug_messages: + self.debug_messages[session_key] = [] + + message_data = { + 'id': len(self.debug_messages[session_key]) + 1, + 'type': 'bot', + 'content': str(message), + 'timestamp': datetime.now().isoformat(), + 'message_chain': [component.__dict__ for component in message], + } + + self.debug_messages[session_key].append(message_data) + + await self.logger.info(f'Send message to {session_key}: {message}') + + return message_data + + async def reply_message( + self, + message_source: platform_events.MessageEvent, + message: platform_message.MessageChain, + quote_origin: bool = False, + ) -> dict: + """回复消息""" + message_data = WebChatMessage( + id=-1, + role='assistant', + content=str(message), + message_chain=[component.__dict__ for component in message], + timestamp=datetime.now().isoformat(), + ) + + # notify waiter + if isinstance(message_source, platform_events.FriendMessage): + self.webchat_person_session.resp_waiters[message_source.message_chain.message_id].set_result(message_data) + elif isinstance(message_source, platform_events.GroupMessage): + self.webchat_group_session.resp_waiters[message_source.message_chain.message_id].set_result(message_data) + + return message_data.model_dump() + + def register_listener( + self, + event_type: typing.Type[platform_events.Event], + func: typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], typing.Awaitable[None]], + ): + """注册事件监听器""" + self.listeners[event_type] = func + + def unregister_listener( + self, + event_type: typing.Type[platform_events.Event], + func: typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], typing.Awaitable[None]], + ): + """取消注册事件监听器""" + del self.listeners[event_type] + + async def run_async(self): + """运行适配器""" + await self.logger.info('WebChat调试适配器已启动') + + try: + while True: + await asyncio.sleep(1) + except asyncio.CancelledError: + await self.logger.info('WebChat调试适配器已停止') + raise + + async def kill(self): + """停止适配器""" + await self.logger.info('WebChat调试适配器正在停止') + + async def send_webchat_message( + self, pipeline_uuid: str, session_type: str, message_chain_obj: typing.List[dict] + ) -> dict: + """发送调试消息到流水线""" + if session_type == 'person': + use_session = self.webchat_person_session + else: + use_session = self.webchat_group_session + + message_chain = platform_message.MessageChain.parse_obj(message_chain_obj) + + message_id = len(use_session.get_message_list(pipeline_uuid)) + 1 + + use_session.get_message_list(pipeline_uuid).append( + WebChatMessage( + id=message_id, + role='user', + content=str(message_chain), + message_chain=message_chain_obj, + timestamp=datetime.now().isoformat(), + ) + ) + + message_chain.insert(0, platform_message.Source(id=message_id, time=datetime.now().timestamp())) + + if session_type == 'person': + sender = platform_entities.Friend(id='webchatperson', nickname='User') + event = platform_events.FriendMessage( + sender=sender, message_chain=message_chain, time=datetime.now().timestamp() + ) + else: + group = platform_entities.Group( + id='webchatgroup', name='Group', permission=platform_entities.Permission.Member + ) + sender = platform_entities.GroupMember( + id='webchatperson', + member_name='User', + group=group, + permission=platform_entities.Permission.Member, + ) + event = platform_events.GroupMessage( + sender=sender, message_chain=message_chain, time=datetime.now().timestamp() + ) + + self.ap.platform_mgr.webchat_proxy_bot.bot_entity.use_pipeline_uuid = pipeline_uuid + + if event.__class__ in self.listeners: + await self.listeners[event.__class__](event, self) + + # set waiter + waiter = asyncio.Future[WebChatMessage]() + use_session.resp_waiters[message_id] = waiter + waiter.add_done_callback(lambda future: use_session.resp_waiters.pop(message_id)) + + resp_message = await waiter + + resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1 + + use_session.get_message_list(pipeline_uuid).append(resp_message) + + return resp_message.model_dump() + + def get_webchat_messages(self, pipeline_uuid: str, session_type: str) -> list[dict]: + """获取调试消息历史""" + if session_type == 'person': + return [message.model_dump() for message in self.webchat_person_session.get_message_list(pipeline_uuid)] + else: + return [message.model_dump() for message in self.webchat_group_session.get_message_list(pipeline_uuid)] diff --git a/pkg/platform/sources/webchat.yaml b/pkg/platform/sources/webchat.yaml new file mode 100644 index 000000000..4e8cc38e2 --- /dev/null +++ b/pkg/platform/sources/webchat.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: webchat + label: + en_US: "WebChat Debug" + zh_Hans: "网页聊天调试" + description: + en_US: "WebChat adapter for pipeline debugging" + zh_Hans: "用于流水线调试的网页聊天适配器" + icon: "" +spec: {} +execution: + python: + path: "webchat.py" + attr: "WebChatAdapter" diff --git a/web/package-lock.json b/web/package-lock.json index 7a622b41b..ee9b57678 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -15,6 +15,8 @@ "@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-hover-card": "^1.1.13", "@radix-ui/react-label": "^2.1.6", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.4", "@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-switch": "^1.2.4", @@ -1255,6 +1257,215 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", + "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.6.tgz", @@ -1389,6 +1600,78 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz", + "integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.4.tgz", diff --git a/web/package.json b/web/package.json index 70e7edb84..9c278e0d1 100644 --- a/web/package.json +++ b/web/package.json @@ -23,6 +23,8 @@ "@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-hover-card": "^1.1.13", "@radix-ui/react-label": "^2.1.6", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.4", "@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-switch": "^1.2.4", diff --git a/web/src/app/home/bots/components/bot-card/BotCard.tsx b/web/src/app/home/bots/components/bot-card/BotCard.tsx index 848eb077d..67f470ded 100644 --- a/web/src/app/home/bots/components/bot-card/BotCard.tsx +++ b/web/src/app/home/bots/components/bot-card/BotCard.tsx @@ -4,6 +4,7 @@ import { httpClient } from '@/app/infra/http/HttpClient'; import { Switch } from '@/components/ui/switch'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; +import { Button } from '@/components/ui/button'; export default function BotCard({ botCardVO, @@ -92,25 +93,25 @@ export default function BotCard({ e.stopPropagation(); }} /> -
{ onClickLogIcon(); e.stopPropagation(); }} > - + -
+ diff --git a/web/src/app/home/pipelines/components/pipeline-card/PipelineCard.tsx b/web/src/app/home/pipelines/components/pipeline-card/PipelineCard.tsx index 244380fbe..05bf24704 100644 --- a/web/src/app/home/pipelines/components/pipeline-card/PipelineCard.tsx +++ b/web/src/app/home/pipelines/components/pipeline-card/PipelineCard.tsx @@ -1,9 +1,22 @@ import styles from './pipelineCard.module.css'; import { PipelineCardVO } from '@/app/home/pipelines/components/pipeline-card/PipelineCardVO'; import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; -export default function PipelineCard({ cardVO }: { cardVO: PipelineCardVO }) { +export default function PipelineCard({ + cardVO, + onDebug, +}: { + cardVO: PipelineCardVO; + onDebug: (pipelineId: string) => void; +}) { const { t } = useTranslation(); + + const handleDebugClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onDebug(cardVO.id); + }; + return (
@@ -48,6 +61,22 @@ export default function PipelineCard({ cardVO }: { cardVO: PipelineCardVO }) {
)} + ); diff --git a/web/src/app/home/pipelines/components/pipeline-card/pipelineCard.module.css b/web/src/app/home/pipelines/components/pipeline-card/pipelineCard.module.css index 344954418..2ecbd44a7 100644 --- a/web/src/app/home/pipelines/components/pipeline-card/pipelineCard.module.css +++ b/web/src/app/home/pipelines/components/pipeline-card/pipelineCard.module.css @@ -67,9 +67,11 @@ .operationContainer { display: flex; - flex-direction: row; + flex-direction: column; + align-items: flex-end; + justify-content: space-between; gap: 0.5rem; - width: 5rem; + width: 8rem; } .operationDefaultBadge { @@ -98,3 +100,8 @@ font-weight: bold; max-width: 100%; } + +.debugButtonIcon { + width: 1.2rem; + height: 1.2rem; +} diff --git a/web/src/app/home/pipelines/debug-dialog/AtBadge.tsx b/web/src/app/home/pipelines/debug-dialog/AtBadge.tsx new file mode 100644 index 000000000..38a83d390 --- /dev/null +++ b/web/src/app/home/pipelines/debug-dialog/AtBadge.tsx @@ -0,0 +1,31 @@ +import { Badge } from '@/components/ui/badge'; +import { X } from 'lucide-react'; + +interface AtBadgeProps { + targetName: string; + readonly?: boolean; + onRemove?: () => void; +} + +export default function AtBadge({ + targetName, + readonly = false, + onRemove, +}: AtBadgeProps) { + return ( + + @{targetName} + {!readonly && onRemove && ( + + )} + + ); +} diff --git a/web/src/app/home/pipelines/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/debug-dialog/DebugDialog.tsx new file mode 100644 index 000000000..2a4505dcb --- /dev/null +++ b/web/src/app/home/pipelines/debug-dialog/DebugDialog.tsx @@ -0,0 +1,422 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { cn } from '@/lib/utils'; +import { Pipeline } from '@/app/infra/entities/api'; +import { Message } from '@/app/infra/entities/message'; +import { toast } from 'sonner'; +import AtBadge from './AtBadge'; + +interface MessageComponent { + type: 'At' | 'Plain'; + target?: string; + text?: string; +} + +interface DebugDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + pipelineId: string; +} + +export default function DebugDialog({ + open, + onOpenChange, + pipelineId, +}: DebugDialogProps) { + const { t } = useTranslation(); + const [selectedPipelineId, setSelectedPipelineId] = useState(pipelineId); + const [sessionType, setSessionType] = useState<'person' | 'group'>('person'); + const [messages, setMessages] = useState([]); + const [inputValue, setInputValue] = useState(''); + const [pipelines, setPipelines] = useState([]); + const [showAtPopover, setShowAtPopover] = useState(false); + const [hasAt, setHasAt] = useState(false); + const [isHovering, setIsHovering] = useState(false); + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + const popoverRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + useEffect(() => { + if (open) { + setSelectedPipelineId(pipelineId); + loadPipelines(); + loadMessages(pipelineId); + } + }, [open, pipelineId]); + + useEffect(() => { + if (open) { + loadMessages(selectedPipelineId); + } + }, [sessionType, selectedPipelineId]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + popoverRef.current && + !popoverRef.current.contains(event.target as Node) && + !inputRef.current?.contains(event.target as Node) + ) { + setShowAtPopover(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + useEffect(() => { + if (showAtPopover) { + setIsHovering(true); + } + }, [showAtPopover]); + + const loadPipelines = async () => { + try { + const response = await httpClient.getPipelines(); + setPipelines(response.pipelines); + } catch (error) { + console.error('Failed to load pipelines:', error); + } + }; + + const loadMessages = async (pipelineId: string) => { + try { + const response = await httpClient.getWebChatHistoryMessages( + pipelineId, + sessionType, + ); + setMessages(response.messages); + } catch (error) { + console.error('Failed to load messages:', error); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (sessionType === 'group') { + if (value.endsWith('@')) { + setShowAtPopover(true); + } else if (showAtPopover && (!value.includes('@') || value.length > 1)) { + setShowAtPopover(false); + } + } + setInputValue(value); + }; + + const handleAtSelect = () => { + setHasAt(true); + setShowAtPopover(false); + setInputValue(inputValue.slice(0, -1)); + }; + + const handleAtRemove = () => { + setHasAt(false); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + if (showAtPopover) { + handleAtSelect(); + } else { + sendMessage(); + } + } else if (e.key === 'Backspace' && hasAt && inputValue === '') { + handleAtRemove(); + } + }; + + const sendMessage = async () => { + if (!inputValue.trim() && !hasAt) return; + + try { + const messageChain = []; + + let text_content = inputValue.trim(); + if (hasAt) { + text_content = ' ' + text_content; + } + + if (hasAt) { + messageChain.push({ + type: 'At', + target: 'webchatbot', + }); + } + messageChain.push({ + type: 'Plain', + text: text_content, + }); + + if (hasAt) { + // for showing + text_content = '@webchatbot' + text_content; + } + + const userMessage: Message = { + id: -1, + role: 'user', + content: text_content, + timestamp: new Date().toISOString(), + message_chain: messageChain, + }; + + setMessages((prevMessages) => [...prevMessages, userMessage]); + setInputValue(''); + setHasAt(false); + + const response = await httpClient.sendWebChatMessage( + sessionType, + messageChain, + selectedPipelineId, + 120000, + ); + + setMessages((prevMessages) => [...prevMessages, response.message]); + } catch ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error: any + ) { + console.log(error, 'type of error', typeof error); + console.error('Failed to send message:', error); + + if (!error.message.includes('timeout') && sessionType === 'person') { + toast.error(t('pipelines.debugDialog.sendFailed')); + } + } finally { + inputRef.current?.focus(); + } + }; + + // const resetSession = async () => { + // try { + // await httpClient.resetWebChatSession(selectedPipelineId, sessionType); + // setMessages([]); + // } catch (error) { + // console.error('Failed to reset session:', error); + // } + // }; + + const renderMessageContent = (message: Message) => { + return ( + + {(message.message_chain as MessageComponent[]).map( + (component, index) => { + if (component.type === 'At') { + return ( + + ); + } else if (component.type === 'Plain') { + return {component.text}; + } + return null; + }, + )} + + ); + }; + + return ( + + + +
+ + {t('pipelines.debugDialog.title')} + + +
+
+
+
+
+ + +
+
+
+ +
+ +
+ {messages.length === 0 ? ( +
+ {t('pipelines.debugDialog.noMessages')} +
+ ) : ( + messages.map((message) => ( +
+
+ {renderMessageContent(message)} +
+ {message.role === 'user' + ? t('pipelines.debugDialog.userMessage') + : t('pipelines.debugDialog.botMessage')} +
+
+
+ )) + )} +
+
+ + +
+
+ {hasAt && ( + + )} +
+ + {showAtPopover && ( +
+
setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + > + + @webchatbot - {t('pipelines.debugDialog.atTips')} + +
+
+ )} +
+
+ +
+
+
+ +
+ ); +} diff --git a/web/src/app/home/pipelines/page.tsx b/web/src/app/home/pipelines/page.tsx index f12fad491..fb17e6e63 100644 --- a/web/src/app/home/pipelines/page.tsx +++ b/web/src/app/home/pipelines/page.tsx @@ -15,6 +15,8 @@ import { } from '@/components/ui/dialog'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; +import DebugDialog from './debug-dialog/DebugDialog'; + export default function PluginConfigPage() { const { t } = useTranslation(); const [modalOpen, setModalOpen] = useState(false); @@ -32,6 +34,8 @@ export default function PluginConfigPage() { const [disableForm, setDisableForm] = useState(false); const [selectedPipelineIsDefault, setSelectedPipelineIsDefault] = useState(false); + const [debugDialogOpen, setDebugDialogOpen] = useState(false); + const [debugPipelineId, setDebugPipelineId] = useState(''); useEffect(() => { getPipelines(); @@ -92,6 +96,11 @@ export default function PluginConfigPage() { }); } + const handleDebug = (pipelineId: string) => { + setDebugPipelineId(pipelineId); + setDebugDialogOpen(true); + }; + return (
@@ -149,11 +158,17 @@ export default function PluginConfigPage() { getSelectedPipelineForm(pipeline.id); }} > - +
); })} + + ); } diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index fdd361d39..d86a8be0d 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -1,6 +1,7 @@ import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; import { PipelineConfigTab } from '@/app/infra/entities/pipeline'; import { I18nLabel } from '@/app/infra/entities/common'; +import { Message } from '@/app/infra/entities/message'; export interface ApiResponse { code: number; @@ -228,3 +229,11 @@ export interface GetPipelineResponseData { export interface GetPipelineMetadataResponseData { configs: PipelineConfigTab[]; } + +export interface ApiRespWebChatMessage { + message: Message; +} + +export interface ApiRespWebChatMessages { + messages: Message[]; +} diff --git a/web/src/app/infra/entities/message/index.ts b/web/src/app/infra/entities/message/index.ts new file mode 100644 index 000000000..6578fff88 --- /dev/null +++ b/web/src/app/infra/entities/message/index.ts @@ -0,0 +1,7 @@ +export interface Message { + id: number; + role: 'user' | 'assistant'; + content: string; + message_chain: object[]; + timestamp: string; +} diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index 3ef824b77..4fb21042e 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -29,6 +29,8 @@ import { GetPipelineResponseData, GetPipelineMetadataResponseData, AsyncTask, + ApiRespWebChatMessage, + ApiRespWebChatMessages, } from '@/app/infra/entities/api'; import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest'; import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse'; @@ -301,6 +303,43 @@ class HttpClient { return this.delete(`/api/v1/pipelines/${uuid}`); } + // ============ Debug WebChat API ============ + public sendWebChatMessage( + sessionType: string, + messageChain: object[], + pipelineId: string, + timeout: number = 15000, + ): Promise { + return this.post( + `/api/v1/pipelines/${pipelineId}/chat/send`, + { + session_type: sessionType, + message: messageChain, + }, + { + timeout, + }, + ); + } + + public getWebChatHistoryMessages( + pipelineId: string, + sessionType: string, + ): Promise { + return this.get( + `/api/v1/pipelines/${pipelineId}/chat/messages/${sessionType}`, + ); + } + + public resetWebChatSession( + pipelineId: string, + sessionType: string, + ): Promise<{ message: string }> { + return this.post( + `/api/v1/pipelines/${pipelineId}/chat/reset/${sessionType}`, + ); + } + // ============ Platform API ============ public getAdapters(): Promise { return this.get('/api/v1/platform/adapters'); @@ -456,8 +495,8 @@ class HttpClient { } // export const httpClient = new HttpClient('https://event-log.langbot.dev'); -// export const httpClient = new HttpClient('http://localhost:5300'); -export const httpClient = new HttpClient('/'); +export const httpClient = new HttpClient('http://localhost:5300'); +// export const httpClient = new HttpClient('/'); // 临时写法,未来两种Client都继承自HttpClient父类,不允许共享方法 export const spaceClient = new HttpClient('https://space.langbot.app'); diff --git a/web/src/components/ui/popover.tsx b/web/src/components/ui/popover.tsx new file mode 100644 index 000000000..0e285c1de --- /dev/null +++ b/web/src/components/ui/popover.tsx @@ -0,0 +1,48 @@ +'use client'; + +import * as React from 'react'; +import * as PopoverPrimitive from '@radix-ui/react-popover'; + +import { cn } from '@/lib/utils'; + +function Popover({ + ...props +}: React.ComponentProps) { + return ; +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function PopoverContent({ + className, + align = 'center', + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return ; +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; diff --git a/web/src/components/ui/scroll-area.tsx b/web/src/components/ui/scroll-area.tsx new file mode 100644 index 000000000..cafa294f5 --- /dev/null +++ b/web/src/components/ui/scroll-area.tsx @@ -0,0 +1,58 @@ +'use client'; + +import * as React from 'react'; +import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; + +import { cn } from '@/lib/utils'; + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ); +} + +function ScrollBar({ + className, + orientation = 'vertical', + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { ScrollArea, ScrollBar }; diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 3d025e4ed..3972377e6 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -183,6 +183,7 @@ const enUS = { 'Pipelines define the processing flow for message events, used to bind to bots', createPipeline: 'Create Pipeline', editPipeline: 'Edit Pipeline', + chat: 'Chat', getPipelineListError: 'Failed to get pipeline list: ', daysAgo: 'days ago', today: 'Today', @@ -202,6 +203,25 @@ const enUS = { deleteConfirmation: 'Are you sure you want to delete this pipeline? Bots bound to this pipeline will not work.', defaultPipelineCannotDelete: 'Default pipeline cannot be deleted', + debugDialog: { + title: 'Pipeline Chat', + selectPipeline: 'Select Pipeline', + sessionType: 'Session Type', + privateChat: 'Private Chat', + groupChat: 'Group Chat', + send: 'Send', + reset: 'Reset Conversation', + inputPlaceholder: 'Enter message...', + noMessages: 'No messages', + userMessage: 'User', + botMessage: 'Bot', + sendFailed: 'Send failed', + resetSuccess: 'Conversation reset successfully', + resetFailed: 'Reset failed', + loadMessagesFailed: 'Failed to load messages', + loadPipelinesFailed: 'Failed to load pipelines', + atTips: 'Mention the bot', + }, }, register: { title: 'Initialize LangBot 👋', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index b8fb06ceb..bd232d7ea 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -178,6 +178,7 @@ const zhHans = { description: '流水线定义了对消息事件的处理流程,用于绑定到机器人', createPipeline: '创建流水线', editPipeline: '编辑流水线', + chat: '对话', getPipelineListError: '获取流水线列表失败:', daysAgo: '天前', today: '今天', @@ -197,6 +198,25 @@ const zhHans = { deleteConfirmation: '你确定要删除这个流水线吗?已绑定此流水线的机器人将无法使用。', defaultPipelineCannotDelete: '默认流水线不可删除', + debugDialog: { + title: '流水线对话', + selectPipeline: '选择流水线', + sessionType: '会话类型', + privateChat: '私聊', + groupChat: '群聊', + send: '发送', + reset: '重置对话', + inputPlaceholder: '输入消息...', + noMessages: '暂无消息', + userMessage: '用户', + botMessage: '机器人', + sendFailed: '发送失败', + resetSuccess: '对话已重置', + resetFailed: '重置失败', + loadMessagesFailed: '加载消息失败', + loadPipelinesFailed: '加载流水线失败', + atTips: '提及机器人', + }, }, register: { title: '初始化 LangBot 👋',