halo icon indicating copy to clipboard operation
halo copied to clipboard

期待插件支持 websocket 接口

Open Rainsheep opened this issue 1 year ago • 4 comments

你当前使用的版本

2.11.0

描述一下此特性

我想在插件中实现服务端向前端推送消息,目前想到比较好的方案为 websocket,但插件好像不支持开发 websocket 接口。 场景:在插件中想要执行一些 shell 脚本,并且将 shell 脚本的输出实时的推送给前端。 shell 脚本会在 halo 容器中下载一些东西,前端可以同步下载进度和 shell 脚本的输出。

感觉轮询比较耗费性能,websocket 可能是个更好的选择,希望 halo 支持插件 websocket。 或者你们是否有更好的方案?

附加信息

No response

Rainsheep avatar Jan 30 '24 17:01 Rainsheep

Hi @Rainsheep , thank you for reaching out here!

/kind feature /area core /area plugin

我们将研究一下插件中实现 WebSocket。

JohnNiang avatar Jan 31 '24 02:01 JohnNiang

允许让插件添加 SimpleUrlHandlerMapping 即可实现 websocket 功能

guqing avatar Jan 31 '24 03:01 guqing

我也需要websocket进行前后端通信

liuchangfitcloud avatar Feb 21 '24 02:02 liuchangfitcloud

I'm willing to contribute the feature according to https://github.com/halo-dev/halo/issues/5285#issuecomment-1918299978.

/assign

JohnNiang avatar Feb 21 '24 02:02 JohnNiang

经过一段时间研究,想要在插件中实现 WebSocket 功能似乎不是定义一个 SimpleUrlHandlerMapping 那么容易。尤其是需要动态注册 WebSocketHandler,还需要对 WebSocket 接口增加权限校验,需要完整考虑后才能够正式实现。

目前,我有一个方案暂时可以替代 WebSocket 方案,那就是 Server-Sent Event,具体样例可参考:

  • https://www.baeldung.com/spring-server-sent-events
  • https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events

JohnNiang avatar Mar 18 '24 17:03 JohnNiang

经过一段时间研究,想要在插件中实现 WebSocket 功能似乎不是定义一个 SimpleUrlHandlerMapping 那么容易。尤其是需要动态注册 WebSocketHandler,还需要对 WebSocket 接口增加权限校验,需要完整考虑后才能够正式实现。

请参考以下的示例: 在 halo 中简单写一个类用来代理插件中的 SimpleHandlerMapping 然后在插件启动时将其注册到代理的 HandlerMapping 中以便被 DispatcherHandler 使用到

@Component
public class PluginHandlerMappingIntegrator {

    @Autowired
    private PluginSimpleUrlHandlerMapping pluginSimpleUrlHandlerMapping;

    // simple example
    @EventListener
    public void onPluginStartUp(HaloPluginStartedEvent event) {
        var pluginWrapper = event.getPlugin();
        var p = pluginWrapper.getPlugin();
        if (!(p instanceof SpringPlugin springPlugin)) {
            return;
        }
        var simpleUrlHandlerMappings = springPlugin.getApplicationContext()
            .getBeansOfType(SimpleUrlHandlerMapping.class);
        simpleUrlHandlerMappings.values().forEach(handlerMapping -> {
            pluginSimpleUrlHandlerMapping.registerHandlerMapping(handlerMapping);
        });
    }

    @Component
    public static class PluginSimpleUrlHandlerMapping extends AbstractUrlHandlerMapping {
        public PluginSimpleUrlHandlerMapping() {
            // must be lower than SimpleUrlHandlerMapping
            setOrder(Ordered.LOWEST_PRECEDENCE - 100);
        }

        @Override
        public void initApplicationContext() throws BeansException {
            // do nothing
        }

        public void registerHandlerMapping(SimpleUrlHandlerMapping handlerMapping) {
			// simple example
            handlerMapping.getHandlerMap()
                .forEach((pattern, o) -> {
                    registerHandler(pattern.getPatternString(), o);
                });

        }
    }
}

然后在随便在哪个插件中写上

public class MyWebSocketHandler implements WebSocketHandler {

    @Override
    @NonNull
    public Mono<Void> handle(WebSocketSession session) {
        // 这里编写处理WebSocket会话的逻辑
        System.out.println("\nWebSocket session established---------->\n\n");
        return session.send(session.receive()
            .map(msg -> session.textMessage("Echo: " + msg.getPayloadAsText())));
    }
}

@Configuration(proxyBeanMethods = false)
public class WebSocketConfig {
    @Bean
    public SimpleUrlHandlerMapping handlerMapping() {
        return new SimpleUrlHandlerMapping() {{
            setUrlMap(Collections.singletonMap("/ws", new MyWebSocketHandler()));
            setOrder(10);
        }};
    }
}

然后测试 websocket 的链接并发送一个 ping 字符串作为示例的结果如下: image

如此, API是插件定义的,halo支持 watch的verb,可以在角色模板中声明

如果只是 SSE 现在 halo 不需要做任何事情插件本身就是可以用的, AI 插件就用了SSE 分段发送文本

guqing avatar Mar 19 '24 05:03 guqing

时间原因,不再追踪此问题,close

Rainsheep avatar Mar 20 '24 09:03 Rainsheep

Hi @Rainsheep ,期望能够保留当前 Issue,直到 Halo 在插件中彻底支持 WebSocket。

JohnNiang avatar Mar 20 '24 09:03 JohnNiang

open,后续我可能不再继续追踪此 issue,希望 @liuchangfitcloud 或其它开发者可以关注下。 也希望官方尽快彻底支持

Rainsheep avatar Mar 20 '24 09:03 Rainsheep

感谢 @guqing 在 https://github.com/halo-dev/halo/issues/5285#issuecomment-2005782748 提供的思路,我将尝试验证并实现。

JohnNiang avatar Mar 20 '24 10:03 JohnNiang

为了规范 API 设计,我计划在插件中进行如下实现即可拥有 WebSocket 的处理能力:

@Component
public class MyWebSocketConfigurer implements WebSocketConfigurer {
  
  @Override
  public WebSocketBuilder builder() {
    var handler = session -> {
      var messages = session.receive()
              .map(message -> {
                var payload = message.getPayloadAsText();
                return session.textMessage(payload.toUpperCase());
              });
      return session.send(messages);
    };
    var firstHandler = ...;
    var secondHandler = ...;
    
    return new WebSocketBuilder()
            .groupVersion(GroupVersion.parseApiVersion("my-plugin.halowrite.com/v1alpha1"))
            .endpoint("/first-resources", firstHandler)
            .endpoint("/second-resources", secondHandler);
  }
  
}

插件启动时,Halo core 中将匹配 /apis/my-plugin.halowrite.com/v1alpha1/first-resources 路由,并交给 firstHandler 进行处理。

权限配置样例如下:

- apiGroups: ["my-plugin.halowrite.com"]
  resources: ["first-resources"]
  verbs: ["watch"]

如果有任何意见或者建议,非常欢迎提出来讨论。

JohnNiang avatar Mar 27 '24 06:03 JohnNiang

我觉得可以

guqing avatar Mar 27 '24 06:03 guqing

为了和 CustomEndpoint 风格保持一致,这里更正一下在插件中的实现示例:

@Component
public class MyWebSocketEndpoint implements WebSocketEndpoint {
  
  @Override
  public GroupVersion groupVersion() {
    return GroupVersion.parseApiVersion("my-plugin.halowrite.com/v1alpha1");
  }
  
  @Override
  public String urlPath() {
    return "/resources";
  }
  
  @Override
  public WebSocketHandler handler() {
    return session -> {
      var messages = session.receive()
              .map(message -> {
                var payload = message.getPayloadAsText();
                return session.textMessage(payload.toUpperCase());
              });
      return session.send(messages);
    };
  }
  
}

JohnNiang avatar Mar 28 '24 06:03 JohnNiang