From 4fe92d8ece197d99f950e64f117f0ac3f300f953 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Fri, 7 Nov 2025 17:26:42 +0800 Subject: [PATCH] Feat/plugin on windows (#1760) * feat: communicate with runtime via ws * chore: bump langbot-plugin 0.1.9b2 * chore: comment on shutdown on windows --- pkg/persistence/mgr.py | 1 + pkg/plugin/connector.py | 42 +++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/pkg/persistence/mgr.py b/pkg/persistence/mgr.py index bd292ca7..8390a506 100644 --- a/pkg/persistence/mgr.py +++ b/pkg/persistence/mgr.py @@ -118,6 +118,7 @@ class PersistenceManager: 'name': 'ChatPipeline', 'description': 'Default pipeline, new bots will be bound to this pipeline | 默认提供的流水线,您配置的机器人将自动绑定到此流水线', 'config': pipeline_config, + 'extensions_preferences': {}, } await self.execute_async(sqlalchemy.insert(pipeline.LegacyPipeline).values(pipeline_data)) diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index 6191adeb..f3fa159f 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -43,6 +43,10 @@ class PluginRuntimeConnector: ctrl: stdio_client_controller.StdioClientController | ws_client_controller.WebSocketClientController + runtime_subprocess_on_windows: asyncio.subprocess.Process | None = None + + runtime_subprocess_on_windows_task: asyncio.Task | None = None + runtime_disconnect_callback: typing.Callable[ [PluginRuntimeConnector], typing.Coroutine[typing.Any, typing.Any, None] ] @@ -119,6 +123,41 @@ class PluginRuntimeConnector: make_connection_failed_callback=make_connection_failed_callback, ) task = self.ctrl.run(new_connection_callback) + elif platform.get_platform() == 'win32': + # Due to Windows's lack of supports for both stdio and subprocess: + # See also: https://docs.python.org/zh-cn/3.13/library/asyncio-platforms.html + # We have to launch runtime via cmd but communicate via ws. + self.ap.logger.info('(windows) use cmd to launch plugin runtime and communicate via ws') + + python_path = sys.executable + env = os.environ.copy() + self.runtime_subprocess_on_windows = await asyncio.create_subprocess_exec( + python_path, + '-m', 'langbot_plugin.cli.__init__', 'rt', + env=env, + ) + + # hold the process + self.runtime_subprocess_on_windows_task = asyncio.create_task(self.runtime_subprocess_on_windows.wait()) + + ws_url = 'ws://localhost:5400/control/ws' + + async def make_connection_failed_callback( + ctrl: ws_client_controller.WebSocketClientController, + exc: Exception = None, + ) -> None: + if exc is not None: + self.ap.logger.error(f'(windows) Failed to connect to plugin runtime({ws_url}): {exc}') + else: + self.ap.logger.error(f'(windows) Failed to connect to plugin runtime({ws_url}), trying to reconnect...') + await self.runtime_disconnect_callback(self) + + self.ctrl = ws_client_controller.WebSocketClientController( + ws_url=ws_url, + make_connection_failed_callback=make_connection_failed_callback, + ) + task = self.ctrl.run(new_connection_callback) + else: # stdio self.ap.logger.info('use stdio to connect to plugin runtime') # cmd: lbp rt -s @@ -312,6 +351,9 @@ class PluginRuntimeConnector: yield cmd_ret def dispose(self): + # No need to consider the shutdown on Windows + # for Windows can kill processes and subprocesses chainly + if self.is_enable_plugin and isinstance(self.ctrl, stdio_client_controller.StdioClientController): self.ap.logger.info('Terminating plugin runtime process...') self.ctrl.process.terminate() diff --git a/pyproject.toml b/pyproject.toml index 307d2821..22df7131 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.9b1", + "langbot-plugin==0.1.9b2", "asyncpg>=0.30.0", "line-bot-sdk>=3.19.0", "tboxsdk>=0.0.10",