dynapm 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -2542,20 +2542,26 @@ var __webpack_modules__ = {
2542
2542
  net: function(module) {
2543
2543
  module.exports = require("net");
2544
2544
  },
2545
+ "node:net": function(module) {
2546
+ module.exports = require("node:net");
2547
+ },
2548
+ "node:url": function(module) {
2549
+ module.exports = require("node:url");
2550
+ },
2545
2551
  stream: function(module) {
2546
2552
  module.exports = require("stream");
2547
2553
  },
2548
2554
  tls: function(module) {
2549
2555
  module.exports = require("tls");
2550
2556
  },
2557
+ "uWebSockets.js": function(module) {
2558
+ module.exports = require("uWebSockets.js");
2559
+ },
2551
2560
  url: function(module) {
2552
2561
  module.exports = require("url");
2553
2562
  },
2554
2563
  zlib: function(module) {
2555
2564
  module.exports = require("zlib");
2556
- },
2557
- "uWebSockets.js": function(module) {
2558
- module.exports = import("uWebSockets.js");
2559
2565
  }
2560
2566
  };
2561
2567
  var __webpack_module_cache__ = {};
@@ -2581,6 +2587,41 @@ function __webpack_require__(moduleId) {
2581
2587
  return getter;
2582
2588
  };
2583
2589
  })();
2590
+ (()=>{
2591
+ var getProto = Object.getPrototypeOf ? function(obj) {
2592
+ return Object.getPrototypeOf(obj);
2593
+ } : function(obj) {
2594
+ return obj.__proto__;
2595
+ };
2596
+ var leafPrototypes;
2597
+ __webpack_require__.t = function(value, mode) {
2598
+ if (1 & mode) value = this(value);
2599
+ if (8 & mode) return value;
2600
+ if ('object' == typeof value && value) {
2601
+ if (4 & mode && value.__esModule) return value;
2602
+ if (16 & mode && 'function' == typeof value.then) return value;
2603
+ }
2604
+ var ns = Object.create(null);
2605
+ __webpack_require__.r(ns);
2606
+ var def = {};
2607
+ leafPrototypes = leafPrototypes || [
2608
+ null,
2609
+ getProto({}),
2610
+ getProto([]),
2611
+ getProto(getProto)
2612
+ ];
2613
+ for(var current = 2 & mode && value; 'object' == typeof current && !~leafPrototypes.indexOf(current); current = getProto(current))Object.getOwnPropertyNames(current).forEach(function(key) {
2614
+ def[key] = function() {
2615
+ return value[key];
2616
+ };
2617
+ });
2618
+ def['default'] = function() {
2619
+ return value;
2620
+ };
2621
+ __webpack_require__.d(ns, def);
2622
+ return ns;
2623
+ };
2624
+ })();
2584
2625
  (()=>{
2585
2626
  __webpack_require__.d = function(exports1, definition) {
2586
2627
  for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
@@ -2594,6 +2635,16 @@ function __webpack_require__(moduleId) {
2594
2635
  return Object.prototype.hasOwnProperty.call(obj, prop);
2595
2636
  };
2596
2637
  })();
2638
+ (()=>{
2639
+ __webpack_require__.r = function(exports1) {
2640
+ if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
2641
+ value: 'Module'
2642
+ });
2643
+ Object.defineProperty(exports1, '__esModule', {
2644
+ value: true
2645
+ });
2646
+ };
2647
+ })();
2597
2648
  var __webpack_exports__ = {};
2598
2649
  (()=>{
2599
2650
  const external_node_child_process_namespaceObject = require("node:child_process");
@@ -2653,6 +2704,11 @@ var __webpack_exports__ = {};
2653
2704
  }
2654
2705
  lock = (async ()=>{
2655
2706
  try {
2707
+ const alreadyRunning = await this.isRunning(service);
2708
+ if (alreadyRunning) {
2709
+ console.log(`[${service.name}] 服务已在运行,跳过启动`);
2710
+ return;
2711
+ }
2656
2712
  console.log(`[${service.name}] 正在启动...`);
2657
2713
  const result = await this.executor.execute(service.commands.start, {
2658
2714
  cwd: service.commands.cwd,
@@ -2683,30 +2739,309 @@ var __webpack_exports__ = {};
2683
2739
  service._state.status = 'offline';
2684
2740
  }
2685
2741
  }
2686
- const external_node_net_namespaceObject = require("node:net");
2687
- var external_node_net_default = /*#__PURE__*/ __webpack_require__.n(external_node_net_namespaceObject);
2688
- const external_node_http_namespaceObject = require("node:http");
2689
- var external_node_http_default = /*#__PURE__*/ __webpack_require__.n(external_node_http_namespaceObject);
2690
- const external_node_https_namespaceObject = require("node:https");
2691
- var external_node_https_default = /*#__PURE__*/ __webpack_require__.n(external_node_https_namespaceObject);
2692
- const external_node_url_namespaceObject = require("node:url");
2742
+ var external_uWebSockets_js_ = __webpack_require__("uWebSockets.js");
2743
+ var external_uWebSockets_js_default = /*#__PURE__*/ __webpack_require__.n(external_uWebSockets_js_);
2744
+ var external_node_net_ = __webpack_require__("node:net");
2745
+ var external_node_net_default = /*#__PURE__*/ __webpack_require__.n(external_node_net_);
2746
+ var external_node_url_ = __webpack_require__("node:url");
2693
2747
  __webpack_require__("./node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/stream.js");
2694
2748
  __webpack_require__("./node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/receiver.js");
2695
2749
  __webpack_require__("./node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/sender.js");
2696
2750
  var websocket = __webpack_require__("./node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/websocket.js");
2697
2751
  __webpack_require__("./node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/websocket-server.js");
2698
2752
  const wrapper = websocket;
2753
+ class AdminApiHandler {
2754
+ config;
2755
+ logger;
2756
+ hostnameRoutes;
2757
+ portRoutes;
2758
+ serviceManager;
2759
+ constructor(config, logger, hostnameRoutes, portRoutes, serviceManager){
2760
+ this.config = config;
2761
+ this.logger = logger;
2762
+ this.hostnameRoutes = hostnameRoutes;
2763
+ this.portRoutes = portRoutes;
2764
+ this.serviceManager = serviceManager;
2765
+ }
2766
+ isIpAllowed(ip) {
2767
+ if (!this.config.adminApi?.allowedIps || 0 === this.config.adminApi.allowedIps.length) return true;
2768
+ return this.config.adminApi.allowedIps.includes(ip);
2769
+ }
2770
+ isAuthenticated(authHeader) {
2771
+ if (!this.config.adminApi?.authToken) return true;
2772
+ if (!authHeader) return false;
2773
+ const token = authHeader.replace('Bearer ', '');
2774
+ return token === this.config.adminApi.authToken;
2775
+ }
2776
+ getServiceUptime(service) {
2777
+ if ('online' === service._state.status && service._state.startTime) return service._state.totalUptime + (Date.now() - service._state.startTime);
2778
+ return service._state.totalUptime;
2779
+ }
2780
+ handleAdminApi(res, req) {
2781
+ const ip = req.getHeader('x-forwarded-for')?.split(',')[0]?.trim() || req.getHeader('cf-connecting-ip') || '127.0.0.1';
2782
+ if (!this.isIpAllowed(ip)) {
2783
+ res.cork(()=>{
2784
+ res.writeStatus('403 Forbidden');
2785
+ res.end('Forbidden');
2786
+ });
2787
+ this.logger.warn({
2788
+ msg: `🚫 [Admin API] 拒绝访问: ${ip}`
2789
+ });
2790
+ return;
2791
+ }
2792
+ const authHeader = req.getHeader('authorization');
2793
+ if (!this.isAuthenticated(authHeader)) {
2794
+ res.cork(()=>{
2795
+ res.writeStatus('401 Unauthorized');
2796
+ res.writeHeader('WWW-Authenticate', 'Bearer');
2797
+ res.end('Unauthorized');
2798
+ });
2799
+ return;
2800
+ }
2801
+ const url = req.getUrl();
2802
+ const method = req.getMethod();
2803
+ if ('/_dynapm/api/services' === url && 'get' === method.toLowerCase()) this.getServicesList(res);
2804
+ else if (url.startsWith('/_dynapm/api/services/') && 'get' === method.toLowerCase()) {
2805
+ const serviceName = url.split('/')[4];
2806
+ this.getServiceDetail(res, serviceName);
2807
+ } else if (url.endsWith('/stop') && 'post' === method.toLowerCase()) {
2808
+ const parts = url.split('/');
2809
+ const serviceName = parts[4];
2810
+ this.stopService(res, serviceName);
2811
+ } else if (url.endsWith('/start') && 'post' === method.toLowerCase()) {
2812
+ const parts = url.split('/');
2813
+ const serviceName = parts[4];
2814
+ this.startService(res, serviceName);
2815
+ } else if ('/_dynapm/api/events' === url && 'get' === method.toLowerCase()) this.handleEventStream(res);
2816
+ else res.cork(()=>{
2817
+ res.writeStatus('404 Not Found');
2818
+ res.end('Not Found');
2819
+ });
2820
+ }
2821
+ getServicesList(res) {
2822
+ const serviceMap = new Map();
2823
+ for (const mapping of this.hostnameRoutes.values())serviceMap.set(mapping.service.name, mapping.service);
2824
+ for (const mapping of this.portRoutes.values())serviceMap.set(mapping.service.name, mapping.service);
2825
+ const services = Array.from(serviceMap.values()).map((service)=>({
2826
+ name: service.name,
2827
+ base: service.base,
2828
+ status: service._state.status,
2829
+ uptime: this.getServiceUptime(service),
2830
+ lastAccessTime: service._state.lastAccessTime,
2831
+ activeConnections: service._state.activeConnections,
2832
+ idleTimeout: service.idleTimeout,
2833
+ proxyOnly: service.proxyOnly || false,
2834
+ pid: service._state.pid
2835
+ }));
2836
+ res.cork(()=>{
2837
+ res.writeHeader('Content-Type', 'application/json');
2838
+ res.end(JSON.stringify({
2839
+ services
2840
+ }));
2841
+ });
2842
+ }
2843
+ getServiceDetail(res, serviceName) {
2844
+ const mapping = Array.from(this.hostnameRoutes.values()).find((m)=>m.service.name === serviceName);
2845
+ this.logger.info({
2846
+ msg: `🔍 [Admin API] 查找服务: ${serviceName}, 找到: ${mapping?.service.name || 'null'}`
2847
+ });
2848
+ if (!mapping) {
2849
+ res.cork(()=>{
2850
+ res.writeStatus('404 Not Found');
2851
+ res.end(JSON.stringify({
2852
+ error: 'Service not found'
2853
+ }));
2854
+ });
2855
+ return;
2856
+ }
2857
+ const service = mapping.service;
2858
+ const detail = {
2859
+ name: service.name,
2860
+ base: service.base,
2861
+ status: service._state.status,
2862
+ uptime: this.getServiceUptime(service),
2863
+ lastAccessTime: service._state.lastAccessTime,
2864
+ activeConnections: service._state.activeConnections,
2865
+ idleTimeout: service.idleTimeout,
2866
+ startTimeout: service.startTimeout,
2867
+ proxyOnly: service.proxyOnly || false,
2868
+ pid: service._state.pid,
2869
+ healthCheck: service.healthCheck || {
2870
+ type: 'tcp'
2871
+ },
2872
+ startCount: service._state.startCount,
2873
+ totalUptime: service._state.totalUptime
2874
+ };
2875
+ res.cork(()=>{
2876
+ res.writeHeader('Content-Type', 'application/json');
2877
+ res.end(JSON.stringify(detail));
2878
+ });
2879
+ }
2880
+ async stopService(res, serviceName) {
2881
+ const mapping = Array.from(this.hostnameRoutes.values()).find((m)=>m.service.name === serviceName);
2882
+ if (!mapping) {
2883
+ res.cork(()=>{
2884
+ res.writeStatus('404 Not Found');
2885
+ res.end(JSON.stringify({
2886
+ error: 'Service not found'
2887
+ }));
2888
+ });
2889
+ return;
2890
+ }
2891
+ const service = mapping.service;
2892
+ if ('online' !== service._state.status) {
2893
+ res.cork(()=>{
2894
+ res.writeStatus('400 Bad Request');
2895
+ res.end(JSON.stringify({
2896
+ error: 'Service is not online'
2897
+ }));
2898
+ });
2899
+ return;
2900
+ }
2901
+ try {
2902
+ service._state.status = 'stopping';
2903
+ if (service._state.startTime) {
2904
+ service._state.totalUptime += Date.now() - service._state.startTime;
2905
+ service._state.startTime = void 0;
2906
+ }
2907
+ await this.serviceManager.stop(service);
2908
+ service._state.status = 'offline';
2909
+ res.cork(()=>{
2910
+ res.writeHeader('Content-Type', 'application/json');
2911
+ res.end(JSON.stringify({
2912
+ success: true,
2913
+ message: `服务 ${service.name} 已停止`
2914
+ }));
2915
+ });
2916
+ } catch (error) {
2917
+ const message = error instanceof Error ? error.message : String(error);
2918
+ service._state.status = 'online';
2919
+ res.cork(()=>{
2920
+ res.writeStatus('500 Internal Server Error');
2921
+ res.end(JSON.stringify({
2922
+ error: message
2923
+ }));
2924
+ });
2925
+ }
2926
+ }
2927
+ async startService(res, serviceName) {
2928
+ const mapping = Array.from(this.hostnameRoutes.values()).find((m)=>m.service.name === serviceName);
2929
+ if (!mapping) {
2930
+ res.cork(()=>{
2931
+ res.writeStatus('404 Not Found');
2932
+ res.end(JSON.stringify({
2933
+ error: 'Service not found'
2934
+ }));
2935
+ });
2936
+ return;
2937
+ }
2938
+ const service = mapping.service;
2939
+ if ('online' === service._state.status || 'starting' === service._state.status) {
2940
+ res.cork(()=>{
2941
+ res.writeStatus('400 Bad Request');
2942
+ res.end(JSON.stringify({
2943
+ error: 'Service is already running or starting'
2944
+ }));
2945
+ });
2946
+ return;
2947
+ }
2948
+ try {
2949
+ service._state.status = 'starting';
2950
+ this.serviceManager.start(service).catch((err)=>{
2951
+ this.logger.error({
2952
+ msg: `❌ [${service.name}] 启动失败`,
2953
+ error: err.message
2954
+ });
2955
+ service._state.status = 'offline';
2956
+ });
2957
+ const waitStartTime = Date.now();
2958
+ let isReady = false;
2959
+ while(Date.now() - waitStartTime < service.startTimeout){
2960
+ isReady = await checkTcpPort(service.base);
2961
+ if (isReady) break;
2962
+ await new Promise((resolve)=>setTimeout(resolve, 100));
2963
+ }
2964
+ if (!isReady) {
2965
+ service._state.status = 'offline';
2966
+ res.cork(()=>{
2967
+ res.writeStatus('503 Service Unavailable');
2968
+ res.end(JSON.stringify({
2969
+ error: 'Service start timeout'
2970
+ }));
2971
+ });
2972
+ return;
2973
+ }
2974
+ service._state.status = 'online';
2975
+ service._state.startTime = Date.now();
2976
+ service._state.startCount++;
2977
+ res.cork(()=>{
2978
+ res.writeHeader('Content-Type', 'application/json');
2979
+ res.end(JSON.stringify({
2980
+ success: true,
2981
+ message: `服务 ${service.name} 已启动`
2982
+ }));
2983
+ });
2984
+ } catch (error) {
2985
+ const message = error instanceof Error ? error.message : String(error);
2986
+ service._state.status = 'offline';
2987
+ res.cork(()=>{
2988
+ res.writeStatus('500 Internal Server Error');
2989
+ res.end(JSON.stringify({
2990
+ error: message
2991
+ }));
2992
+ });
2993
+ }
2994
+ }
2995
+ handleEventStream(res) {
2996
+ res.cork(()=>{
2997
+ res.writeStatus('200 OK');
2998
+ res.writeHeader('Content-Type', 'text/event-stream');
2999
+ res.writeHeader('Cache-Control', 'no-cache');
3000
+ res.writeHeader('Connection', 'keep-alive');
3001
+ res.writeHeader('X-Accel-Buffering', 'no');
3002
+ });
3003
+ res.cork(()=>{
3004
+ res.end(`event: connected\ndata: {"timestamp":${Date.now()}}\n\n`);
3005
+ });
3006
+ }
3007
+ }
3008
+ function checkTcpPort(url) {
3009
+ const { URL } = __webpack_require__("node:url");
3010
+ const net = __webpack_require__("node:net");
3011
+ const parsed = new URL(url);
3012
+ const host = parsed.hostname;
3013
+ const port = parseInt(parsed.port || ('https:' === parsed.protocol ? '443' : '80'));
3014
+ return new Promise((resolve)=>{
3015
+ const socket = net.createConnection({
3016
+ host,
3017
+ port,
3018
+ timeout: 100
3019
+ }, ()=>{
3020
+ socket.destroy();
3021
+ resolve(true);
3022
+ });
3023
+ socket.on('error', ()=>{
3024
+ socket.destroy();
3025
+ resolve(false);
3026
+ });
3027
+ socket.on('timeout', ()=>{
3028
+ socket.destroy();
3029
+ resolve(false);
3030
+ });
3031
+ });
3032
+ }
2699
3033
  function formatTime(ms) {
2700
3034
  if (ms < 1000) return `${ms}ms`;
2701
3035
  return `${(ms / 1000).toFixed(2)}s`;
2702
3036
  }
3037
+ const external_undici_namespaceObject = require("undici");
2703
3038
  const GatewayConstants = {
2704
3039
  IDLE_CHECK_INTERVAL: 3000,
2705
3040
  TCP_CHECK_TIMEOUT: 100,
2706
3041
  BACKEND_READY_CHECK_DELAY: 50
2707
3042
  };
2708
- function checkTcpPort(url) {
2709
- const parsed = new external_node_url_namespaceObject.URL(url);
3043
+ function gateway_checkTcpPort(url) {
3044
+ const parsed = new external_node_url_.URL(url);
2710
3045
  const host = parsed.hostname;
2711
3046
  const port = parseInt(parsed.port || ('https:' === parsed.protocol ? '443' : '80'));
2712
3047
  return new Promise((resolve)=>{
@@ -2731,46 +3066,112 @@ var __webpack_exports__ = {};
2731
3066
  class Gateway {
2732
3067
  config;
2733
3068
  serviceManager;
2734
- services;
3069
+ hostnameRoutes;
3070
+ portRoutes;
2735
3071
  logger;
3072
+ logging;
3073
+ adminApi;
2736
3074
  constructor(config, logger){
2737
3075
  this.config = config;
2738
3076
  this.serviceManager = new ServiceManager();
2739
- this.services = new Map();
3077
+ this.hostnameRoutes = new Map();
3078
+ this.portRoutes = new Map();
2740
3079
  this.logger = logger;
3080
+ this.logging = {
3081
+ enableRequestLog: config.logging?.enableRequestLog ?? false,
3082
+ enableWebSocketLog: config.logging?.enableWebSocketLog ?? false,
3083
+ enablePerformanceLog: config.logging?.enablePerformanceLog ?? false
3084
+ };
3085
+ this.adminApi = new AdminApiHandler(config, logger, this.hostnameRoutes, this.portRoutes, this.serviceManager);
2741
3086
  this.initServices();
2742
3087
  this.initIdleChecker();
2743
3088
  }
2744
3089
  initServices() {
2745
- for (const [hostname, service] of Object.entries(this.config.services)){
3090
+ if (!this.config.services) return;
3091
+ console.log('[DynaPM] 初始化服务...');
3092
+ console.log('[DynaPM] 服务数量:', Object.keys(this.config.services).length);
3093
+ for (const service of Object.values(this.config.services)){
2746
3094
  service._state = {
2747
- status: 'offline',
3095
+ status: service.proxyOnly ? 'online' : 'offline',
2748
3096
  lastAccessTime: Date.now(),
2749
- activeConnections: 0
3097
+ activeConnections: 0,
3098
+ startCount: 0,
3099
+ totalUptime: 0
2750
3100
  };
2751
- this.services.set(hostname, service);
3101
+ const routes = service.routes || [];
3102
+ if (0 === routes.length) {
3103
+ console.warn(`[DynaPM] ⚠️ [${service.name}] 没有配置路由`);
3104
+ continue;
3105
+ }
3106
+ console.log(`[DynaPM] ✅ [${service.name}] 配置了 ${routes.length} 个路由:`);
3107
+ for (const route of routes){
3108
+ const targetUrl = new external_node_url_.URL(route.target);
3109
+ const undiciClient = new external_undici_namespaceObject.Client(route.target, {
3110
+ keepAliveTimeout: 60000,
3111
+ pipelining: 1
3112
+ });
3113
+ const mapping = {
3114
+ service,
3115
+ target: route.target,
3116
+ targetUrl,
3117
+ undiciClient
3118
+ };
3119
+ if ('host' === route.type) {
3120
+ const hostname = route.value;
3121
+ this.hostnameRoutes.set(hostname, mapping);
3122
+ console.log(`[DynaPM] └─ hostname: ${hostname} -> ${route.target}`);
3123
+ } else if ('port' === route.type) {
3124
+ const port = route.value;
3125
+ this.portRoutes.set(port, mapping);
3126
+ console.log(`[DynaPM] └─ port: ${port} -> ${route.target}`);
3127
+ }
3128
+ }
2752
3129
  }
3130
+ const hostnameCount = this.hostnameRoutes.size;
3131
+ const portCount = this.portRoutes.size;
3132
+ console.log(`[DynaPM] 📊 共配置 ${hostnameCount} 个 hostname 映射, ${portCount} 个端口绑定`);
3133
+ this.logger.info({
3134
+ msg: `📊 共配置 ${hostnameCount} 个 hostname 映射, ${portCount} 个端口绑定`
3135
+ });
2753
3136
  }
2754
3137
  initIdleChecker() {
2755
3138
  setInterval(()=>{
2756
3139
  const now = Date.now();
2757
- for (const service of this.services.values())if ('online' === service._state.status && 0 === service._state.activeConnections && now - service._state.lastAccessTime > service.idleTimeout) {
2758
- this.logger.info({
2759
- msg: `🛌 [${service.name}] 闲置超时,正在停止...`
2760
- });
2761
- this.serviceManager.stop(service).catch((err)=>{
2762
- this.logger.error({
2763
- msg: `❌ [${service.name}] 停止失败`,
2764
- error: err.message
2765
- });
2766
- });
2767
- service._state.status = 'offline';
3140
+ const checkedServices = new Set();
3141
+ for (const mapping of this.hostnameRoutes.values())if (!checkedServices.has(mapping.service)) {
3142
+ checkedServices.add(mapping.service);
3143
+ this.checkIdleService(mapping.service, now);
3144
+ }
3145
+ for (const mapping of this.portRoutes.values())if (!checkedServices.has(mapping.service)) {
3146
+ checkedServices.add(mapping.service);
3147
+ this.checkIdleService(mapping.service, now);
2768
3148
  }
2769
3149
  }, GatewayConstants.IDLE_CHECK_INTERVAL);
2770
3150
  }
2771
- handleRequest(res, req) {
3151
+ checkIdleService(service, now) {
3152
+ if (service.proxyOnly) return;
3153
+ if ('online' === service._state.status && 0 === service._state.activeConnections && now - service._state.lastAccessTime > service.idleTimeout) {
3154
+ this.logger.info({
3155
+ msg: `🛌 [${service.name}] 闲置超时,正在停止...`
3156
+ });
3157
+ service._state.status = 'stopping';
3158
+ if (service._state.startTime) {
3159
+ service._state.totalUptime += now - service._state.startTime;
3160
+ service._state.startTime = void 0;
3161
+ }
3162
+ this.serviceManager.stop(service).catch((err)=>{
3163
+ this.logger.error({
3164
+ msg: `❌ [${service.name}] 停止失败`,
3165
+ error: err.message
3166
+ });
3167
+ }).finally(()=>{
3168
+ service._state.status = 'offline';
3169
+ });
3170
+ }
3171
+ }
3172
+ handlePortBindingRequest(res, req, mapping) {
3173
+ const service = mapping.service;
2772
3174
  const startTime = Date.now();
2773
- const hostname = req.getHeader('host')?.split(':')[0] || '';
2774
3175
  const method = req.getMethod();
2775
3176
  const url = req.getUrl();
2776
3177
  const queryString = req.getQuery();
@@ -2780,8 +3181,33 @@ var __webpack_exports__ = {};
2780
3181
  const safeValue = value.replace(/[\r\n]/g, '');
2781
3182
  headers[key] = safeValue;
2782
3183
  });
2783
- const service = this.services.get(hostname);
2784
- if (!service) {
3184
+ service._state.lastAccessTime = Date.now();
3185
+ const needsStart = 'offline' === service._state.status;
3186
+ if (needsStart) this.handleServiceStart(res, mapping, fullUrl, startTime, method, headers);
3187
+ else this.handleDirectProxy(res, mapping, fullUrl, startTime, method, headers);
3188
+ }
3189
+ handleRequest(res, req) {
3190
+ const startTime = Date.now();
3191
+ const hostHeader = req.getHeader('host');
3192
+ let hostname = '';
3193
+ if (hostHeader) {
3194
+ const colonIndex = hostHeader.indexOf(':');
3195
+ hostname = -1 !== colonIndex ? hostHeader.substring(0, colonIndex) : hostHeader;
3196
+ }
3197
+ const method = req.getMethod();
3198
+ const url = req.getUrl();
3199
+ const queryString = req.getQuery();
3200
+ const fullUrl = queryString ? url + '?' + queryString : url;
3201
+ const headers = {};
3202
+ req.forEach((key, value)=>{
3203
+ let safeValue = value;
3204
+ const crIndex = value.indexOf('\r');
3205
+ const lfIndex = value.indexOf('\n');
3206
+ if (-1 !== crIndex || -1 !== lfIndex) safeValue = value.replace(/[\r\n]/g, '');
3207
+ headers[key] = safeValue;
3208
+ });
3209
+ const mapping = this.hostnameRoutes.get(hostname);
3210
+ if (!mapping) {
2785
3211
  this.logger.info({
2786
3212
  msg: `❌ [${hostname}] ${method} ${fullUrl} - 404`
2787
3213
  });
@@ -2791,17 +3217,70 @@ var __webpack_exports__ = {};
2791
3217
  });
2792
3218
  return;
2793
3219
  }
3220
+ const service = mapping.service;
2794
3221
  service._state.lastAccessTime = Date.now();
2795
- const needsStart = 'offline' === service._state.status;
2796
- if (needsStart) this.handleServiceStart(res, service, fullUrl, startTime, method, headers);
2797
- else this.handleDirectProxy(res, service, fullUrl, startTime, method, headers);
3222
+ const status = service._state.status;
3223
+ const needsStart = 'offline' === status || 'stopping' === status;
3224
+ if (needsStart) {
3225
+ if ('stopping' === status) this.handleServiceWithWait(res, mapping, fullUrl, startTime, method, headers);
3226
+ else this.handleServiceStart(res, mapping, fullUrl, startTime, method, headers);
3227
+ } else this.handleDirectProxy(res, mapping, fullUrl, startTime, method, headers);
2798
3228
  }
2799
- handleServiceStart(res, service, fullUrl, startTime, method, headers) {
2800
- const startStartTime = Date.now();
3229
+ async startServiceAndProxy(res, mapping, fullUrl, startTime, method, headers, body) {
3230
+ const service = mapping.service;
3231
+ const target = mapping.target;
2801
3232
  this.logger.info({
2802
3233
  msg: `🚀 [${service.name}] ${method} ${fullUrl} - 启动服务...`
2803
3234
  });
2804
3235
  service._state.status = 'starting';
3236
+ try {
3237
+ await this.serviceManager.start(service);
3238
+ const waitStartTime = Date.now();
3239
+ let isReady = false;
3240
+ while(Date.now() - waitStartTime < service.startTimeout){
3241
+ isReady = await gateway_checkTcpPort(target);
3242
+ if (isReady) {
3243
+ const waitDuration = Date.now() - waitStartTime;
3244
+ this.logger.info({
3245
+ msg: `✅ [${service.name}] 服务就绪 (等待${formatTime(waitDuration)})`
3246
+ });
3247
+ break;
3248
+ }
3249
+ }
3250
+ if (!isReady) {
3251
+ service._state.status = 'offline';
3252
+ throw new Error(`服务启动超时: 端口 ${target} 不可用`);
3253
+ }
3254
+ service._state.status = 'online';
3255
+ service._state.startTime = Date.now();
3256
+ service._state.startCount++;
3257
+ await this.forwardProxyRequest(res, mapping, fullUrl, startTime, method, headers, body);
3258
+ } catch (error) {
3259
+ const message = error instanceof Error ? error.message : String(error);
3260
+ if ('Client aborted' === message) return;
3261
+ this.logger.error({
3262
+ msg: `❌ [${service.name}] 启动失败`,
3263
+ error: message
3264
+ });
3265
+ try {
3266
+ res.cork(()=>{
3267
+ res.writeStatus('503 Service Unavailable');
3268
+ res.end('Service Unavailable');
3269
+ });
3270
+ } catch (sendErr) {
3271
+ const sendErrMsg = sendErr instanceof Error ? sendErr.message : String(sendErr);
3272
+ this.logger.error({
3273
+ msg: `❌ [${service.name}] 发送错误响应失败`,
3274
+ error: sendErrMsg
3275
+ });
3276
+ }
3277
+ }
3278
+ }
3279
+ handleServiceWithWait(res, mapping, fullUrl, startTime, method, headers) {
3280
+ const service = mapping.service;
3281
+ this.logger.info({
3282
+ msg: `⏳ [${service.name}] ${method} ${fullUrl} - 等待服务停止完成...`
3283
+ });
2805
3284
  const chunks = [];
2806
3285
  let aborted = false;
2807
3286
  res.onAborted(()=>{
@@ -2813,46 +3292,35 @@ var __webpack_exports__ = {};
2813
3292
  chunks.push(chunk);
2814
3293
  if (isLast) {
2815
3294
  const fullBody = Buffer.concat(chunks);
3295
+ if (aborted) return;
2816
3296
  (async ()=>{
2817
- try {
2818
- await this.serviceManager.start(service);
2819
- const waitStartTime = Date.now();
2820
- let isReady = false;
2821
- while(Date.now() - waitStartTime < service.startTimeout){
2822
- isReady = await checkTcpPort(service.base);
2823
- if (isReady) {
2824
- const waitDuration = Date.now() - waitStartTime;
2825
- const totalDuration = Date.now() - startStartTime;
2826
- this.logger.info({
2827
- msg: `✅ [${service.name}] 服务就绪 (启动${formatTime(totalDuration - waitDuration)}, 等待${formatTime(waitDuration)})`
2828
- });
2829
- break;
2830
- }
2831
- }
2832
- if (!isReady) {
2833
- service._state.status = 'offline';
2834
- throw new Error(`服务启动超时: 端口 ${service.base} 不可用`);
3297
+ const maxWaitTime = 30000;
3298
+ const checkInterval = 100;
3299
+ const waitStartTime = Date.now();
3300
+ while('stopping' === service._state.status){
3301
+ if (Date.now() - waitStartTime > maxWaitTime) {
3302
+ this.logger.error({
3303
+ msg: `❌ [${service.name}] 等待服务停止超时`
3304
+ });
3305
+ res.cork(()=>{
3306
+ res.writeStatus('503 Service Unavailable');
3307
+ res.end('Service stopping timeout');
3308
+ });
3309
+ return;
2835
3310
  }
2836
- service._state.status = 'online';
2837
- if (aborted) return;
2838
- await this.forwardProxyRequest(res, service, fullUrl, startTime, method, headers, fullBody);
2839
- } catch (error) {
2840
- const message = error instanceof Error ? error.message : String(error);
2841
- if ('Client aborted' === message) return;
2842
- this.logger.error({
2843
- msg: `❌ [${service.name}] 启动失败`,
2844
- error: message
2845
- });
2846
- if (!aborted) res.cork(()=>{
2847
- res.writeStatus('503 Service Unavailable');
2848
- res.end('Service Unavailable');
2849
- });
3311
+ await new Promise((resolve)=>setTimeout(resolve, checkInterval));
2850
3312
  }
3313
+ if (aborted) return;
3314
+ this.logger.info({
3315
+ msg: `✅ [${service.name}] 服务已停止,开始启动...`
3316
+ });
3317
+ await this.startServiceAndProxy(res, mapping, fullUrl, startTime, method, headers, fullBody);
2851
3318
  })();
2852
3319
  }
2853
3320
  });
2854
3321
  }
2855
- handleDirectProxy(res, service, fullUrl, startTime, method, headers) {
3322
+ handleServiceStart(res, mapping, fullUrl, startTime, method, headers) {
3323
+ mapping.service;
2856
3324
  const chunks = [];
2857
3325
  let aborted = false;
2858
3326
  res.onAborted(()=>{
@@ -2865,74 +3333,108 @@ var __webpack_exports__ = {};
2865
3333
  if (isLast) {
2866
3334
  const fullBody = Buffer.concat(chunks);
2867
3335
  if (aborted) return;
2868
- this.forwardProxyRequest(res, service, fullUrl, startTime, method, headers, fullBody).catch((err)=>{
3336
+ this.startServiceAndProxy(res, mapping, fullUrl, startTime, method, headers, fullBody);
3337
+ }
3338
+ });
3339
+ }
3340
+ handleDirectProxy(res, mapping, fullUrl, startTime, method, headers) {
3341
+ const service = mapping.service;
3342
+ const chunks = [];
3343
+ let aborted = false;
3344
+ res.onAborted(()=>{
3345
+ aborted = true;
3346
+ });
3347
+ res.onData((ab, isLast)=>{
3348
+ if (aborted) return;
3349
+ const chunk = Buffer.from(ab);
3350
+ chunks.push(chunk);
3351
+ if (isLast) {
3352
+ const fullBody = 1 === chunks.length ? chunks[0] : Buffer.concat(chunks);
3353
+ if (aborted) return;
3354
+ this.forwardProxyRequest(res, mapping, fullUrl, startTime, method, headers, fullBody).catch((err)=>{
2869
3355
  if ('Client aborted' === err.message) return;
2870
3356
  this.logger.error({
2871
3357
  msg: `❌ [${service.name}] 代理失败`,
2872
3358
  error: err.message
2873
3359
  });
2874
- if (!aborted) res.cork(()=>{
2875
- res.writeStatus('500 Internal Server Error');
2876
- res.end('Proxy Error');
2877
- });
3360
+ if (!aborted) try {
3361
+ res.cork(()=>{
3362
+ res.writeStatus('500 Internal Server Error');
3363
+ res.end('Proxy Error');
3364
+ });
3365
+ } catch (sendErr) {
3366
+ const sendErrMsg = sendErr instanceof Error ? sendErr.message : String(sendErr);
3367
+ this.logger.error({
3368
+ msg: `❌ [${service.name}] 发送错误响应失败`,
3369
+ error: sendErrMsg
3370
+ });
3371
+ }
2878
3372
  });
2879
3373
  }
2880
3374
  });
2881
3375
  }
2882
- async forwardProxyRequest(res, service, path, startTime, method, headers, body) {
2883
- const targetUrl = new external_node_url_namespaceObject.URL(service.base + path);
2884
- const isHttps = 'https:' === targetUrl.protocol;
2885
- const httpModule = isHttps ? external_node_https_default() : external_node_http_default();
2886
- const proxyHeaders = {
2887
- ...headers
2888
- };
2889
- delete proxyHeaders['connection'];
2890
- delete proxyHeaders['keep-alive'];
3376
+ async forwardProxyRequest(res, mapping, path, startTime, method, headers, body) {
3377
+ const service = mapping.service;
3378
+ const perfLog = this.logging.enablePerformanceLog;
3379
+ const perfPrepStart = perfLog ? performance.now() : 0;
3380
+ const targetUrl = mapping.targetUrl;
3381
+ const perfUrlTime = perfLog ? performance.now() - perfPrepStart : 0;
3382
+ const proxyHeaders = {};
3383
+ for(const key in headers)if ('connection' !== key && 'keep-alive' !== key) proxyHeaders[key] = headers[key];
2891
3384
  proxyHeaders['host'] = targetUrl.host;
3385
+ const perfHeadersTime = perfLog ? performance.now() - perfPrepStart - perfUrlTime : 0;
2892
3386
  const state = {
2893
3387
  aborted: false,
2894
3388
  responded: false
2895
3389
  };
2896
3390
  service._state.activeConnections++;
3391
+ const perfPrepTime = perfLog ? performance.now() - perfPrepStart : 0;
2897
3392
  return new Promise((resolve, reject)=>{
3393
+ let cleaned = false;
2898
3394
  const cleanup = ()=>{
2899
- service._state.activeConnections--;
3395
+ if (!cleaned) {
3396
+ cleaned = true;
3397
+ service._state.activeConnections--;
3398
+ }
2900
3399
  };
2901
3400
  res.onAborted(()=>{
2902
3401
  state.aborted = true;
2903
- if (state.proxyReq && !state.proxyReq.destroyed) state.proxyReq.destroy();
2904
- if (state.proxyRes && !state.proxyRes.destroyed) state.proxyRes.destroy();
2905
3402
  cleanup();
2906
3403
  resolve();
2907
3404
  });
2908
- state.proxyReq = httpModule.request(targetUrl, {
3405
+ const perfHttpStart = perfLog ? performance.now() : 0;
3406
+ let perfTtfb = 0;
3407
+ let perfStreamStart = 0;
3408
+ let chunkCount = 0;
3409
+ let totalBytes = 0;
3410
+ const undiciClient = mapping.undiciClient;
3411
+ undiciClient.request({
3412
+ path,
2909
3413
  method,
2910
3414
  headers: proxyHeaders,
2911
- rejectUnauthorized: false
2912
- }, (proxyRes)=>{
2913
- state.proxyRes = proxyRes;
2914
- const statusCode = proxyRes.statusCode || 200;
2915
- const statusMessage = proxyRes.statusMessage || 'OK';
3415
+ body
3416
+ }).then(({ statusCode, headers, body })=>{
3417
+ if (perfLog && 0 === perfTtfb) {
3418
+ perfTtfb = performance.now() - perfHttpStart;
3419
+ perfStreamStart = performance.now();
3420
+ }
3421
+ const statusMessage = 'OK';
2916
3422
  if (state.aborted) {
2917
- proxyRes.destroy();
3423
+ body.destroy();
2918
3424
  cleanup();
2919
3425
  resolve();
2920
3426
  return;
2921
3427
  }
2922
3428
  if (101 === statusCode) {
2923
- this.logger.info({
2924
- msg: `✅ [${service.name}] WebSocket 升级成功`
2925
- });
2926
3429
  res.cork(()=>{
2927
3430
  if (state.aborted) return;
2928
3431
  res.writeStatus(`${statusCode} ${statusMessage}`);
2929
- const responseHeaders = proxyRes.headers;
2930
- for (const [key, value] of Object.entries(responseHeaders)){
3432
+ for(const key in headers){
2931
3433
  const keyLower = key.toLowerCase();
2932
- if ('connection' !== keyLower && 'transfer-encoding' !== keyLower && 'keep-alive' !== keyLower) {
2933
- if (Array.isArray(value)) for (const v of value)res.writeHeader(key, v);
2934
- else if (void 0 !== value) res.writeHeader(key, value);
2935
- }
3434
+ if ('connection' === keyLower || 'transfer-encoding' === keyLower || 'keep-alive' === keyLower) continue;
3435
+ const value = headers[key];
3436
+ if (Array.isArray(value)) for (const v of value)res.writeHeader(key, v);
3437
+ else if (void 0 !== value) res.writeHeader(key, value);
2936
3438
  }
2937
3439
  res.end();
2938
3440
  state.responded = true;
@@ -2944,61 +3446,82 @@ var __webpack_exports__ = {};
2944
3446
  res.cork(()=>{
2945
3447
  if (state.aborted) return;
2946
3448
  res.writeStatus(`${statusCode} ${statusMessage}`);
2947
- const responseHeaders = proxyRes.headers;
2948
- for (const [key, value] of Object.entries(responseHeaders)){
3449
+ for(const key in headers){
2949
3450
  const keyLower = key.toLowerCase();
2950
- if ('connection' !== keyLower && 'transfer-encoding' !== keyLower && 'keep-alive' !== keyLower) {
2951
- if (Array.isArray(value)) for (const v of value)res.writeHeader(key, v);
2952
- else if (void 0 !== value) res.writeHeader(key, value);
2953
- }
3451
+ if ('connection' === keyLower || 'transfer-encoding' === keyLower || 'keep-alive' === keyLower) continue;
3452
+ const value = headers[key];
3453
+ if (Array.isArray(value)) for (const v of value)res.writeHeader(key, v);
3454
+ else if (void 0 !== value) res.writeHeader(key, value);
2954
3455
  }
2955
3456
  });
2956
- proxyRes.on('data', (chunk)=>{
3457
+ body.on('data', (chunk)=>{
2957
3458
  if (state.aborted) {
2958
- proxyRes.destroy();
3459
+ body.destroy();
2959
3460
  return;
2960
3461
  }
3462
+ if (perfLog) {
3463
+ chunkCount++;
3464
+ totalBytes += chunk.length;
3465
+ }
2961
3466
  let writeSuccess = false;
2962
3467
  res.cork(()=>{
2963
3468
  if (state.aborted) return;
2964
3469
  writeSuccess = res.write(chunk);
2965
3470
  });
2966
3471
  if (!writeSuccess) {
2967
- proxyRes.pause();
3472
+ body.pause();
2968
3473
  res.onWritable(()=>{
2969
3474
  if (state.aborted) {
2970
- proxyRes.destroy();
3475
+ body.destroy();
2971
3476
  return false;
2972
3477
  }
2973
- proxyRes.resume();
3478
+ body.resume();
2974
3479
  return true;
2975
3480
  });
2976
3481
  }
2977
3482
  });
2978
- proxyRes.on('end', ()=>{
3483
+ body.on('end', ()=>{
2979
3484
  if (state.aborted) {
2980
3485
  cleanup();
2981
3486
  resolve();
2982
3487
  return;
2983
3488
  }
3489
+ const perfStreamTime = perfLog ? performance.now() - perfStreamStart : 0;
3490
+ const perfTotalTime = perfLog ? performance.now() - perfPrepStart : 0;
2984
3491
  res.cork(()=>{
2985
3492
  if (state.aborted) return;
2986
3493
  res.end();
2987
3494
  state.responded = true;
2988
- const responseTime = Date.now() - startTime;
2989
- this.logger.info({
2990
- msg: `📤 [${service.name}] ${method} ${path} - ${statusCode} - ${formatTime(responseTime)}`,
2991
- service: service.name,
3495
+ if (this.logging.enableRequestLog) {
3496
+ const responseTime = Date.now() - startTime;
3497
+ this.logger.info({
3498
+ msg: `📤 [${service.name}] ${method} ${path} - ${statusCode} - ${formatTime(responseTime)}`,
3499
+ service: service.name,
3500
+ method,
3501
+ path,
3502
+ statusCode,
3503
+ responseTime
3504
+ });
3505
+ }
3506
+ if (perfLog) console.error(`⚡ [${service.name}] 性能分析:`, {
2992
3507
  method,
2993
3508
  path,
2994
3509
  statusCode,
2995
- responseTime
3510
+ prepTime: perfPrepTime.toFixed(3) + 'ms',
3511
+ urlTime: perfUrlTime.toFixed(3) + 'ms',
3512
+ headersTime: perfHeadersTime.toFixed(3) + 'ms',
3513
+ ttfb: perfTtfb.toFixed(3) + 'ms',
3514
+ streamTime: perfStreamTime.toFixed(3) + 'ms',
3515
+ totalTime: perfTotalTime.toFixed(3) + 'ms',
3516
+ chunkCount,
3517
+ totalBytes,
3518
+ avgChunkSize: chunkCount > 0 ? (totalBytes / chunkCount).toFixed(1) + 'B' : '0B'
2996
3519
  });
2997
3520
  });
2998
3521
  cleanup();
2999
3522
  resolve();
3000
3523
  });
3001
- proxyRes.on('error', (err)=>{
3524
+ body.on('error', (err)=>{
3002
3525
  if (state.aborted) {
3003
3526
  cleanup();
3004
3527
  resolve();
@@ -3008,7 +3531,7 @@ var __webpack_exports__ = {};
3008
3531
  msg: `❌ [${service.name}] 代理响应错误`,
3009
3532
  error: err.message
3010
3533
  });
3011
- if (!state.responded) {
3534
+ if (!state.responded && !state.aborted) {
3012
3535
  state.responded = true;
3013
3536
  try {
3014
3537
  res.cork(()=>{
@@ -3017,13 +3540,18 @@ var __webpack_exports__ = {};
3017
3540
  res.end('Bad Gateway');
3018
3541
  }
3019
3542
  });
3020
- } catch {}
3543
+ } catch (sendErr) {
3544
+ const sendErrMsg = sendErr instanceof Error ? sendErr.message : String(sendErr);
3545
+ this.logger.error({
3546
+ msg: `❌ [${service.name}] 发送错误响应失败`,
3547
+ error: sendErrMsg
3548
+ });
3549
+ }
3021
3550
  }
3022
3551
  cleanup();
3023
3552
  reject(err);
3024
3553
  });
3025
- });
3026
- state.proxyReq.on('error', (err)=>{
3554
+ }).catch((err)=>{
3027
3555
  if (state.aborted) {
3028
3556
  cleanup();
3029
3557
  resolve();
@@ -3033,7 +3561,7 @@ var __webpack_exports__ = {};
3033
3561
  msg: `❌ [${service.name}] 代理请求错误`,
3034
3562
  error: err.message
3035
3563
  });
3036
- if (!state.responded) {
3564
+ if (!state.responded && !state.aborted) {
3037
3565
  state.responded = true;
3038
3566
  try {
3039
3567
  res.cork(()=>{
@@ -3042,45 +3570,60 @@ var __webpack_exports__ = {};
3042
3570
  res.end('Bad Gateway');
3043
3571
  }
3044
3572
  });
3045
- } catch {}
3573
+ } catch (sendErr) {
3574
+ const sendErrMsg = sendErr instanceof Error ? sendErr.message : String(sendErr);
3575
+ this.logger.error({
3576
+ msg: `❌ [${service.name}] 发送错误响应失败`,
3577
+ error: sendErrMsg
3578
+ });
3579
+ }
3046
3580
  }
3047
3581
  cleanup();
3048
3582
  reject(err);
3049
3583
  });
3050
- state.proxyReq.write(body);
3051
- state.proxyReq.end();
3052
3584
  });
3053
3585
  }
3054
3586
  async start() {
3055
- const uWS = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, "uWebSockets.js"));
3587
+ const uWS = await Promise.resolve().then(__webpack_require__.t.bind(__webpack_require__, "uWebSockets.js", 23));
3056
3588
  const host = this.config.host || '127.0.0.1';
3057
3589
  const port = this.config.port || 3000;
3058
3590
  const app = uWS.App();
3059
3591
  app.ws('/*', {
3060
3592
  upgrade: (res, req, context)=>{
3061
3593
  const hostname = req.getHeader('host')?.split(':')[0] || '';
3062
- const service = this.services.get(hostname);
3063
- if (!service) {
3594
+ const mapping = this.hostnameRoutes.get(hostname);
3595
+ if (!mapping) {
3064
3596
  res.cork(()=>{
3065
3597
  res.writeStatus('404 Not Found');
3066
3598
  res.end(`Service not found: ${hostname}`);
3067
3599
  });
3068
3600
  return;
3069
3601
  }
3602
+ const { service, target } = mapping;
3070
3603
  service._state.lastAccessTime = Date.now();
3604
+ const clientHeaders = {};
3605
+ req.forEach((key, value)=>{
3606
+ const safeValue = value.replace(/[\r\n]/g, '');
3607
+ clientHeaders[key] = safeValue;
3608
+ });
3609
+ const clientPath = req.getUrl() + (req.getQuery() ? `?${req.getQuery()}` : '');
3071
3610
  res.upgrade({
3072
3611
  hostname,
3073
- service
3612
+ service,
3613
+ target,
3614
+ clientHeaders,
3615
+ clientPath
3074
3616
  }, req.getHeader('sec-websocket-key'), req.getHeader('sec-websocket-protocol'), req.getHeader('sec-websocket-extensions'), context);
3075
- this.logger.info({
3076
- msg: `🔌 [${service.name}] WebSocket 升级请求`
3617
+ if (this.logging.enableWebSocketLog) this.logger.info({
3618
+ msg: `🔌 [${service.name}] WebSocket 升级请求: ${clientPath}`
3077
3619
  });
3078
3620
  },
3079
3621
  open: (ws)=>{
3080
3622
  const userData = ws.getUserData();
3081
3623
  const service = userData.service;
3624
+ const target = userData.target;
3082
3625
  service._state.activeConnections++;
3083
- this.logger.info({
3626
+ if (this.logging.enableWebSocketLog) this.logger.info({
3084
3627
  msg: `🔌 [${service.name}] WebSocket 连接已建立`
3085
3628
  });
3086
3629
  const wsState = {
@@ -3102,7 +3645,7 @@ var __webpack_exports__ = {};
3102
3645
  const waitStartTime = Date.now();
3103
3646
  let isReady = false;
3104
3647
  while(Date.now() - waitStartTime < service.startTimeout){
3105
- isReady = await checkTcpPort(service.base);
3648
+ isReady = await gateway_checkTcpPort(target);
3106
3649
  if (isReady) {
3107
3650
  const waitDuration = Date.now() - waitStartTime;
3108
3651
  this.logger.info({
@@ -3121,30 +3664,47 @@ var __webpack_exports__ = {};
3121
3664
  return;
3122
3665
  }
3123
3666
  service._state.status = 'online';
3667
+ service._state.startTime = Date.now();
3668
+ service._state.startCount++;
3124
3669
  }
3125
- const targetUrl = new external_node_url_namespaceObject.URL(service.base);
3126
- const wsUrl = `${'https:' === targetUrl.protocol ? 'wss:' : 'ws:'}//${targetUrl.host}/`;
3127
- this.logger.info({
3670
+ const targetUrl = new external_node_url_.URL(target);
3671
+ const userData = ws.getUserData();
3672
+ const clientPath = userData.clientPath;
3673
+ const clientHeaders = userData.clientHeaders;
3674
+ const wsUrl = `${'https:' === targetUrl.protocol ? 'wss:' : 'ws:'}//${targetUrl.host}${clientPath}`;
3675
+ if (this.logging.enableWebSocketLog) this.logger.info({
3128
3676
  msg: `🔌 [${service.name}] 连接后端 WebSocket: ${wsUrl}`
3129
3677
  });
3678
+ const backendHeaders = {};
3679
+ const skipHeaders = new Set([
3680
+ 'host',
3681
+ 'connection',
3682
+ 'upgrade',
3683
+ 'sec-websocket-key',
3684
+ 'sec-websocket-version'
3685
+ ]);
3686
+ for (const [key, value] of Object.entries(clientHeaders))if (!skipHeaders.has(key.toLowerCase())) backendHeaders[key] = value;
3687
+ backendHeaders['Host'] = targetUrl.host;
3688
+ this.logger.info({
3689
+ msg: `🔌 [${service.name}] 转发 WebSocket 请求头`,
3690
+ headers: JSON.stringify(backendHeaders, null, 2)
3691
+ });
3130
3692
  const backendWs = new wrapper(wsUrl, {
3131
- headers: {
3132
- Host: targetUrl.host
3133
- }
3693
+ headers: backendHeaders
3134
3694
  });
3135
3695
  wsState.backendWs = backendWs;
3136
3696
  backendWs.on('open', ()=>{
3137
- this.logger.info({
3697
+ if (this.logging.enableWebSocketLog) this.logger.info({
3138
3698
  msg: `✅ [${service.name}] 后端 WebSocket 连接已建立`
3139
3699
  });
3140
3700
  wsState.backendReady = true;
3141
- this.logger.info({
3701
+ if (this.logging.enableWebSocketLog) this.logger.info({
3142
3702
  msg: `📤 [${service.name}] 发送队列中的 ${wsState.messageQueue.length} 条消息`
3143
3703
  });
3144
3704
  while(wsState.messageQueue.length > 0 && backendWs.readyState === wrapper.OPEN){
3145
3705
  const msg = wsState.messageQueue.shift();
3146
3706
  if (msg) {
3147
- this.logger.info({
3707
+ if (this.logging.enableWebSocketLog) this.logger.info({
3148
3708
  msg: `📨 [${service.name}] 发送队列消息: ${msg.length} 字节`
3149
3709
  });
3150
3710
  backendWs.send(msg);
@@ -3169,7 +3729,7 @@ var __webpack_exports__ = {};
3169
3729
  }
3170
3730
  });
3171
3731
  backendWs.on('close', ()=>{
3172
- this.logger.info({
3732
+ if (this.logging.enableWebSocketLog) this.logger.info({
3173
3733
  msg: `🔌 [${service.name}] 后端 WebSocket 连接关闭`
3174
3734
  });
3175
3735
  if (null !== ws && !wsState.closing) {
@@ -3182,15 +3742,16 @@ var __webpack_exports__ = {};
3182
3742
  msg: `❌ [${service.name}] 后端 WebSocket 错误`,
3183
3743
  error: err.message
3184
3744
  });
3745
+ wsState.closing = true;
3185
3746
  if (null !== ws) ws.close();
3186
3747
  });
3187
3748
  backendWs.on('pause', ()=>{
3188
- this.logger.info({
3749
+ if (this.logging.enableWebSocketLog) this.logger.info({
3189
3750
  msg: `⏸️ [${service.name}] 后端 WebSocket 暂停(背压)`
3190
3751
  });
3191
3752
  });
3192
3753
  backendWs.on('resume', ()=>{
3193
- this.logger.info({
3754
+ if (this.logging.enableWebSocketLog) this.logger.info({
3194
3755
  msg: `▶️ [${service.name}] 后端 WebSocket 恢复`
3195
3756
  });
3196
3757
  });
@@ -3200,7 +3761,7 @@ var __webpack_exports__ = {};
3200
3761
  msg: `❌ [${service.name}] WebSocket 连接失败`,
3201
3762
  error: message
3202
3763
  });
3203
- ws.close();
3764
+ if (null !== ws) ws.close();
3204
3765
  }
3205
3766
  })();
3206
3767
  },
@@ -3210,13 +3771,13 @@ var __webpack_exports__ = {};
3210
3771
  const wsState = ws.wsState;
3211
3772
  if (wsState.backendReady && wsState.backendWs && wsState.backendWs.readyState === wrapper.OPEN) {
3212
3773
  const msgBuffer = Buffer.from(message);
3213
- this.logger.info({
3774
+ if (this.logging.enableWebSocketLog) this.logger.info({
3214
3775
  msg: `📨 [${service.name}] 转发消息到后端: ${msgBuffer.length} 字节`
3215
3776
  });
3216
3777
  wsState.backendWs.send(msgBuffer);
3217
3778
  service._state.lastAccessTime = Date.now();
3218
3779
  } else {
3219
- this.logger.info({
3780
+ if (this.logging.enableWebSocketLog) this.logger.info({
3220
3781
  msg: `📦 [${service.name}] 消息加入队列`
3221
3782
  });
3222
3783
  wsState.messageQueue.push(Buffer.from(message));
@@ -3226,7 +3787,7 @@ var __webpack_exports__ = {};
3226
3787
  const userData = ws.getUserData();
3227
3788
  const service = userData.service;
3228
3789
  service._state.activeConnections--;
3229
- this.logger.info({
3790
+ if (this.logging.enableWebSocketLog) this.logger.info({
3230
3791
  msg: `🔌 [${service.name}] 客户端 WebSocket 连接关闭`
3231
3792
  });
3232
3793
  const wsState = ws.wsState;
@@ -3247,6 +3808,195 @@ var __webpack_exports__ = {};
3247
3808
  msg: `❌ DynaPM 网关启动失败: ${host}:${port}`
3248
3809
  });
3249
3810
  });
3811
+ for (const [portNum, mapping] of this.portRoutes)this.createPortBindingListener(host, portNum, mapping);
3812
+ const adminApiConfig = this.config.adminApi;
3813
+ if (adminApiConfig && false !== adminApiConfig.enabled && adminApiConfig.port) this.createAdminApiListener(host, adminApiConfig.port);
3814
+ }
3815
+ createAdminApiListener(host, port) {
3816
+ const app = external_uWebSockets_js_default().App();
3817
+ app.any('/*', (res, req)=>{
3818
+ this.adminApi.handleAdminApi(res, req);
3819
+ });
3820
+ app.listen(host, port, (token)=>{
3821
+ if (token) this.logger.info({
3822
+ msg: `🔌 管理 API 已启动: http://${host}:${port}`
3823
+ });
3824
+ else this.logger.error({
3825
+ msg: `❌ 管理 API 启动失败: ${host}:${port}`
3826
+ });
3827
+ });
3828
+ }
3829
+ createPortBindingListener(host, portNum, mapping) {
3830
+ const { service, target } = mapping;
3831
+ const app = external_uWebSockets_js_default().App();
3832
+ app.ws('/*', {
3833
+ upgrade: (res, req, context)=>{
3834
+ service._state.lastAccessTime = Date.now();
3835
+ const clientHeaders = {};
3836
+ req.forEach((key, value)=>{
3837
+ const safeValue = value.replace(/[\r\n]/g, '');
3838
+ clientHeaders[key] = safeValue;
3839
+ });
3840
+ const clientPath = req.getUrl() + (req.getQuery() ? `?${req.getQuery()}` : '');
3841
+ res.upgrade({
3842
+ service,
3843
+ target,
3844
+ clientHeaders,
3845
+ clientPath
3846
+ }, req.getHeader('sec-websocket-key'), req.getHeader('sec-websocket-protocol'), req.getHeader('sec-websocket-extensions'), context);
3847
+ if (this.logging.enableWebSocketLog) this.logger.info({
3848
+ msg: `🔌 [${service.name}] 端口${portNum} WebSocket 升级请求: ${clientPath}`
3849
+ });
3850
+ },
3851
+ open: (ws)=>{
3852
+ const userData = ws.getUserData();
3853
+ const svc = userData.service;
3854
+ const backendTarget = userData.target;
3855
+ svc._state.activeConnections++;
3856
+ if (this.logging.enableWebSocketLog) this.logger.info({
3857
+ msg: `🔌 [${svc.name}] 端口${portNum} WebSocket 连接已建立`
3858
+ });
3859
+ const wsState = {
3860
+ backendReady: false,
3861
+ messageQueue: [],
3862
+ backendWs: void 0,
3863
+ closing: false
3864
+ };
3865
+ ws.wsState = wsState;
3866
+ (async ()=>{
3867
+ try {
3868
+ const needsStart = 'offline' === svc._state.status;
3869
+ if (needsStart) {
3870
+ this.logger.info({
3871
+ msg: `🚀 [${svc.name}] 端口${portNum} WebSocket - 启动服务...`
3872
+ });
3873
+ svc._state.status = 'starting';
3874
+ await this.serviceManager.start(svc);
3875
+ const waitStartTime = Date.now();
3876
+ let isReady = false;
3877
+ while(Date.now() - waitStartTime < svc.startTimeout){
3878
+ isReady = await gateway_checkTcpPort(backendTarget);
3879
+ if (isReady) {
3880
+ const waitDuration = Date.now() - waitStartTime;
3881
+ this.logger.info({
3882
+ msg: `✅ [${svc.name}] 端口${portNum} WebSocket 服务就绪 (等待${formatTime(waitDuration)})`
3883
+ });
3884
+ break;
3885
+ }
3886
+ await new Promise((resolve)=>setTimeout(resolve, GatewayConstants.BACKEND_READY_CHECK_DELAY));
3887
+ }
3888
+ if (!isReady) {
3889
+ svc._state.status = 'offline';
3890
+ this.logger.error({
3891
+ msg: `❌ [${svc.name}] 端口${portNum} WebSocket 服务启动超时`
3892
+ });
3893
+ ws.close();
3894
+ return;
3895
+ }
3896
+ svc._state.status = 'online';
3897
+ svc._state.startTime = Date.now();
3898
+ svc._state.startCount++;
3899
+ }
3900
+ const targetUrl = new external_node_url_.URL(backendTarget);
3901
+ const userData = ws.getUserData();
3902
+ const clientPath = userData.clientPath;
3903
+ const clientHeaders = userData.clientHeaders;
3904
+ const wsUrl = `${'https:' === targetUrl.protocol ? 'wss:' : 'ws:'}//${targetUrl.host}${clientPath}`;
3905
+ if (this.logging.enableWebSocketLog) this.logger.info({
3906
+ msg: `🔌 [${svc.name}] 端口${portNum} 连接后端 WebSocket: ${wsUrl}`
3907
+ });
3908
+ const backendHeaders = {};
3909
+ const skipHeaders = new Set([
3910
+ 'host',
3911
+ 'connection',
3912
+ 'upgrade',
3913
+ 'sec-websocket-key',
3914
+ 'sec-websocket-version'
3915
+ ]);
3916
+ for (const [key, value] of Object.entries(clientHeaders))if (!skipHeaders.has(key.toLowerCase())) backendHeaders[key] = value;
3917
+ backendHeaders['Host'] = targetUrl.host;
3918
+ const backendWs = new wrapper(wsUrl, {
3919
+ headers: backendHeaders
3920
+ });
3921
+ wsState.backendWs = backendWs;
3922
+ backendWs.on('open', ()=>{
3923
+ if (this.logging.enableWebSocketLog) this.logger.info({
3924
+ msg: `✅ [${svc.name}] 端口${portNum} 后端 WebSocket 连接已建立`
3925
+ });
3926
+ wsState.backendReady = true;
3927
+ while(wsState.messageQueue.length > 0 && backendWs.readyState === wrapper.OPEN){
3928
+ const msg = wsState.messageQueue.shift();
3929
+ if (msg) backendWs.send(msg);
3930
+ }
3931
+ });
3932
+ backendWs.on('message', (data, isBinary)=>{
3933
+ if (null !== ws) ws.send(data, isBinary, false);
3934
+ });
3935
+ backendWs.on('close', ()=>{
3936
+ if (null !== ws && !wsState.closing) {
3937
+ wsState.closing = true;
3938
+ ws.close();
3939
+ }
3940
+ });
3941
+ backendWs.on('error', ()=>{
3942
+ wsState.closing = true;
3943
+ if (null !== ws) ws.close();
3944
+ });
3945
+ } catch (error) {
3946
+ const message = error instanceof Error ? error.message : String(error);
3947
+ this.logger.error({
3948
+ msg: `❌ [${service.name}] WebSocket 连接失败`,
3949
+ error: message
3950
+ });
3951
+ if (null !== ws) ws.close();
3952
+ }
3953
+ })();
3954
+ },
3955
+ message: (ws, message, _isBinary)=>{
3956
+ const userData = ws.getUserData();
3957
+ const svc = userData.service;
3958
+ const wsState = ws.wsState;
3959
+ if (wsState.backendReady && wsState.backendWs && wsState.backendWs.readyState === wrapper.OPEN) {
3960
+ wsState.backendWs.send(Buffer.from(message));
3961
+ svc._state.lastAccessTime = Date.now();
3962
+ } else wsState.messageQueue.push(Buffer.from(message));
3963
+ },
3964
+ close: (ws)=>{
3965
+ const userData = ws.getUserData();
3966
+ const svc = userData.service;
3967
+ svc._state.activeConnections--;
3968
+ }
3969
+ });
3970
+ app.any('/*', (res, req)=>{
3971
+ this.handlePortBindingRequest(res, req, mapping);
3972
+ });
3973
+ app.listen(host, portNum, (token)=>{
3974
+ if (token) this.logger.info({
3975
+ msg: `🔌 端口绑定已启动: http://${host}:${portNum} -> ${service.name}`
3976
+ });
3977
+ else this.logger.error({
3978
+ msg: `❌ 端口绑定启动失败: ${host}:${portNum}`
3979
+ });
3980
+ });
3981
+ }
3982
+ async cleanup() {
3983
+ this.logger.info({
3984
+ msg: '🧹 正在清理所有服务...'
3985
+ });
3986
+ const cleanedServices = new Set();
3987
+ for (const mapping of this.hostnameRoutes.values())cleanedServices.add(mapping.service);
3988
+ for (const mapping of this.portRoutes.values())cleanedServices.add(mapping.service);
3989
+ const stopPromises = [];
3990
+ for (const service of cleanedServices)if ('online' === service._state.status || 'starting' === service._state.status) stopPromises.push(this.serviceManager.stop(service).catch((err)=>{
3991
+ this.logger.error({
3992
+ msg: `❌ [${service.name}] 停止失败`,
3993
+ error: err.message
3994
+ });
3995
+ }));
3996
+ await Promise.all(stopPromises);
3997
+ this.logger.info({
3998
+ msg: `✅ 已清理 ${cleanedServices.size} 个服务`
3999
+ });
3250
4000
  }
3251
4001
  }
3252
4002
  const external_c12_namespaceObject = require("c12");
@@ -3256,18 +4006,65 @@ var __webpack_exports__ = {};
3256
4006
  defaultConfig: {
3257
4007
  port: 3000,
3258
4008
  host: '127.0.0.1',
3259
- services: {}
4009
+ services: {},
4010
+ adminApi: {
4011
+ enabled: true,
4012
+ port: 4000,
4013
+ allowedIps: [
4014
+ '127.0.0.1',
4015
+ '::1'
4016
+ ]
4017
+ }
3260
4018
  }
3261
4019
  });
3262
4020
  if (!config.services || 0 === Object.keys(config.services).length) throw new Error('配置文件中至少需要一个服务');
3263
- for (const [hostname, service] of Object.entries(config.services)){
3264
- service.name = service.name || hostname;
4021
+ const mainPort = config.port || 3000;
4022
+ if (config.adminApi?.enabled && config.adminApi.port === mainPort) throw new Error(`管理 API 端口 ${mainPort} 与主端口冲突`);
4023
+ const portMap = new Map();
4024
+ const hostnameMap = new Map();
4025
+ for (const [key, service] of Object.entries(config.services)){
4026
+ service.name = service.name || key;
3265
4027
  service.idleTimeout = service.idleTimeout || 300000;
3266
4028
  service.startTimeout = service.startTimeout || 30000;
3267
4029
  service.healthCheck = service.healthCheck || {
3268
4030
  type: 'tcp'
3269
4031
  };
3270
4032
  if ('http' === service.healthCheck.type && !service.healthCheck.url) service.healthCheck.url = service.base;
4033
+ let routes = service.routes;
4034
+ if (!routes || 0 === routes.length) {
4035
+ routes = [];
4036
+ if (service.host) routes.push({
4037
+ type: 'host',
4038
+ value: service.host,
4039
+ target: service.base
4040
+ });
4041
+ if (service.port) routes.push({
4042
+ type: 'port',
4043
+ value: service.port,
4044
+ target: service.base
4045
+ });
4046
+ if (0 === routes.length) routes.push({
4047
+ type: 'host',
4048
+ value: key,
4049
+ target: service.base
4050
+ });
4051
+ service.routes = routes;
4052
+ }
4053
+ for (const route of routes)if (!route.target) throw new Error(`服务 [${service.name}] 的路由配置缺少 target 字段`);
4054
+ for (const route of routes)if ('port' === route.type) {
4055
+ const port = route.value;
4056
+ if (port === mainPort) throw new Error(`服务 [${service.name}] 的路由端口 ${port} 与主端口冲突`);
4057
+ const adminApiPort = config.adminApi?.enabled ? config.adminApi.port : void 0;
4058
+ if (adminApiPort && port === adminApiPort) throw new Error(`服务 [${service.name}] 的路由端口 ${port} 与管理 API 端口冲突`);
4059
+ const existingService = portMap.get(port);
4060
+ if (existingService) throw new Error(`端口冲突: 服务 [${service.name}] 和 [${existingService}] 都配置了端口 ${port}`);
4061
+ portMap.set(port, service.name);
4062
+ } else if ('host' === route.type) {
4063
+ const hostname = route.value;
4064
+ const existingService = hostnameMap.get(hostname);
4065
+ if (existingService && existingService !== service.name) throw new Error(`Hostname 冲突: 服务 [${service.name}] 和 [${existingService}] 都配置了 hostname ${hostname}`);
4066
+ hostnameMap.set(hostname, service.name);
4067
+ }
3271
4068
  }
3272
4069
  return config;
3273
4070
  }
@@ -3323,6 +4120,20 @@ var __webpack_exports__ = {};
3323
4120
  msg: 'DynaPM 网关已启动',
3324
4121
  port: config.port || 3000
3325
4122
  });
4123
+ const cleanup = async (signal)=>{
4124
+ logger.info({
4125
+ msg: `⚠️ 收到 ${signal} 信号,正在清理...`
4126
+ });
4127
+ await gateway.cleanup();
4128
+ process.exit(0);
4129
+ };
4130
+ process.on('SIGINT', ()=>cleanup('SIGINT'));
4131
+ process.on('SIGTERM', ()=>cleanup('SIGTERM'));
4132
+ process.on('exit', ()=>{
4133
+ logger.info({
4134
+ msg: '👋 DynaPM 网关已退出'
4135
+ });
4136
+ });
3326
4137
  } catch (error) {
3327
4138
  logger.error({
3328
4139
  msg: '启动失败',