service-forge 0.1.28__py3-none-any.whl → 0.1.39__py3-none-any.whl

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.

Potentially problematic release.


This version of service-forge might be problematic. Click here for more details.

Files changed (72) hide show
  1. service_forge/__init__.py +0 -0
  2. service_forge/api/deprecated_websocket_api.py +91 -33
  3. service_forge/api/deprecated_websocket_manager.py +70 -53
  4. service_forge/api/http_api.py +127 -53
  5. service_forge/api/kafka_api.py +113 -25
  6. service_forge/api/routers/meta_api/meta_api_router.py +57 -0
  7. service_forge/api/routers/service/service_router.py +42 -6
  8. service_forge/api/routers/trace/trace_router.py +326 -0
  9. service_forge/api/routers/websocket/websocket_router.py +56 -1
  10. service_forge/api/service_studio.py +9 -0
  11. service_forge/execution_context.py +106 -0
  12. service_forge/frontend/static/assets/CreateNewNodeDialog-DkrEMxSH.js +1 -0
  13. service_forge/frontend/static/assets/CreateNewNodeDialog-DwFcBiGp.css +1 -0
  14. service_forge/frontend/static/assets/EditorSidePanel-BNVms9Fq.css +1 -0
  15. service_forge/frontend/static/assets/EditorSidePanel-DZbB3ILL.js +1 -0
  16. service_forge/frontend/static/assets/FeedbackPanel-CC8HX7Yo.js +1 -0
  17. service_forge/frontend/static/assets/FeedbackPanel-ClgniIVk.css +1 -0
  18. service_forge/frontend/static/assets/FormattedCodeViewer.vue_vue_type_script_setup_true_lang-BNuI1NCs.js +1 -0
  19. service_forge/frontend/static/assets/NodeDetailWrapper-BqFFM7-r.js +1 -0
  20. service_forge/frontend/static/assets/NodeDetailWrapper-pZBxv3J0.css +1 -0
  21. service_forge/frontend/static/assets/TestRunningDialog-D0GrCoYs.js +1 -0
  22. service_forge/frontend/static/assets/TestRunningDialog-dhXOsPgH.css +1 -0
  23. service_forge/frontend/static/assets/TracePanelWrapper-B9zvDSc_.js +1 -0
  24. service_forge/frontend/static/assets/TracePanelWrapper-BiednCrq.css +1 -0
  25. service_forge/frontend/static/assets/WorkflowEditor-CcaGGbko.js +3 -0
  26. service_forge/frontend/static/assets/WorkflowEditor-CmasOOYK.css +1 -0
  27. service_forge/frontend/static/assets/WorkflowList-Copuwi-a.css +1 -0
  28. service_forge/frontend/static/assets/WorkflowList-LrRJ7B7h.js +1 -0
  29. service_forge/frontend/static/assets/WorkflowStudio-CthjgII2.css +1 -0
  30. service_forge/frontend/static/assets/WorkflowStudio-FCyhGD4y.js +2 -0
  31. service_forge/frontend/static/assets/api-BDer3rj7.css +1 -0
  32. service_forge/frontend/static/assets/api-DyiqpKJK.js +1 -0
  33. service_forge/frontend/static/assets/code-editor-DBSql_sc.js +12 -0
  34. service_forge/frontend/static/assets/el-collapse-item-D4LG0FJ0.css +1 -0
  35. service_forge/frontend/static/assets/el-empty-D4ZqTl4F.css +1 -0
  36. service_forge/frontend/static/assets/el-form-item-BWkJzdQ_.css +1 -0
  37. service_forge/frontend/static/assets/el-input-D6B3r8CH.css +1 -0
  38. service_forge/frontend/static/assets/el-select-B0XIb2QK.css +1 -0
  39. service_forge/frontend/static/assets/el-tag-DljBBxJR.css +1 -0
  40. service_forge/frontend/static/assets/element-ui-D3x2y3TA.js +12 -0
  41. service_forge/frontend/static/assets/elkjs-Dm5QV7uy.js +24 -0
  42. service_forge/frontend/static/assets/highlightjs-D4ATuRwX.js +3 -0
  43. service_forge/frontend/static/assets/index-BMvodlwc.js +2 -0
  44. service_forge/frontend/static/assets/index-CjSe8i2q.css +1 -0
  45. service_forge/frontend/static/assets/js-yaml-yTPt38rv.js +32 -0
  46. service_forge/frontend/static/assets/time-DKCKV6Ug.js +1 -0
  47. service_forge/frontend/static/assets/ui-components-DQ7-U3pr.js +1 -0
  48. service_forge/frontend/static/assets/vue-core-DL-LgTX0.js +1 -0
  49. service_forge/frontend/static/assets/vue-flow-Dn7R8GPr.js +39 -0
  50. service_forge/frontend/static/index.html +16 -0
  51. service_forge/frontend/static/vite.svg +1 -0
  52. service_forge/model/meta_api/__init__.py +0 -0
  53. service_forge/model/meta_api/schema.py +29 -0
  54. service_forge/model/trace.py +82 -0
  55. service_forge/service.py +32 -11
  56. service_forge/service_config.py +14 -0
  57. service_forge/sft/config/injector.py +32 -2
  58. service_forge/sft/config/injector_default_files.py +12 -0
  59. service_forge/sft/config/sf_metadata.py +5 -0
  60. service_forge/sft/config/sft_config.py +18 -0
  61. service_forge/telemetry.py +66 -0
  62. service_forge/workflow/node.py +266 -27
  63. service_forge/workflow/triggers/fast_api_trigger.py +61 -28
  64. service_forge/workflow/triggers/websocket_api_trigger.py +31 -10
  65. service_forge/workflow/workflow.py +87 -10
  66. service_forge/workflow/workflow_callback.py +24 -2
  67. service_forge/workflow/workflow_factory.py +13 -0
  68. {service_forge-0.1.28.dist-info → service_forge-0.1.39.dist-info}/METADATA +4 -1
  69. service_forge-0.1.39.dist-info/RECORD +134 -0
  70. service_forge-0.1.28.dist-info/RECORD +0 -85
  71. {service_forge-0.1.28.dist-info → service_forge-0.1.39.dist-info}/WHEEL +0 -0
  72. {service_forge-0.1.28.dist-info → service_forge-0.1.39.dist-info}/entry_points.txt +0 -0
File without changes
@@ -1,37 +1,95 @@
1
1
  from __future__ import annotations
2
- import uuid
2
+
3
3
  import asyncio
4
4
  import json
5
- from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends, Request, Query
5
+ import uuid
6
+ from dataclasses import replace
7
+ from typing import Any, Dict, Optional
8
+
9
+ from fastapi import APIRouter, Depends, Query, Request, WebSocket, WebSocketDisconnect
6
10
  from fastapi.responses import JSONResponse
7
11
  from loguru import logger
8
- from typing import Dict, Any, Optional
12
+ from opentelemetry import context as otel_context_api
13
+ from opentelemetry import propagate, trace
14
+
15
+ from service_forge.execution_context import (
16
+ ExecutionContext,
17
+ reset_current_context,
18
+ set_current_context,
19
+ )
20
+
9
21
  from .websocket_manager import websocket_manager
10
22
 
11
23
  router = APIRouter(prefix="/ws", tags=["websocket"])
24
+ tracer = trace.get_tracer("service_forge.api.websocket")
25
+
12
26
 
13
27
  @router.websocket("/connect")
14
- async def websocket_endpoint(websocket: WebSocket, client_id: Optional[str] = Query(None)):
28
+ async def websocket_endpoint(
29
+ websocket: WebSocket, client_id: Optional[str] = Query(None)
30
+ ):
15
31
  """WebSocket连接端点,支持指定客户端ID"""
16
- client_id = await websocket_manager.connect(websocket, client_id)
17
32
  try:
18
- while True:
19
- # 接收客户端消息
20
- data = await websocket.receive_text()
21
- try:
22
- message = json.loads(data)
23
- await handle_client_message(client_id, message)
24
- except json.JSONDecodeError:
25
- logger.error(f"从客户端 {client_id} 收到无效JSON消息: {data}")
26
- await websocket_manager.send_personal_message(
27
- json.dumps({"error": "Invalid JSON format"}),
28
- client_id
29
- )
30
- except WebSocketDisconnect:
31
- websocket_manager.disconnect(client_id)
32
- except Exception as e:
33
- logger.error(f"WebSocket连接处理异常: {e}")
34
- websocket_manager.disconnect(client_id)
33
+ parent_context = propagate.extract(dict(websocket.headers))
34
+ except Exception:
35
+ parent_context = otel_context_api.get_current()
36
+ if parent_context is None:
37
+ parent_context = otel_context_api.get_current()
38
+
39
+ span_name = f"WS {websocket.url.path}"
40
+ with tracer.start_as_current_span(
41
+ span_name,
42
+ context=parent_context,
43
+ kind=trace.SpanKind.SERVER,
44
+ ) as span:
45
+ if websocket.client:
46
+ span.set_attribute("net.peer.ip", websocket.client.host)
47
+ span.set_attribute("net.protocol.name", "websocket")
48
+ span.set_attribute("net.host.port", websocket.url.port or 80)
49
+
50
+ base_context = ExecutionContext(
51
+ trace_context=otel_context_api.get_current(),
52
+ span=span,
53
+ logger=logger.bind(entrypoint="websocket"),
54
+ metadata={
55
+ "entrypoint": "websocket",
56
+ "path": websocket.url.path,
57
+ },
58
+ )
59
+ token = set_current_context(base_context)
60
+
61
+ client_id = await websocket_manager.connect(websocket, client_id)
62
+ span.set_attribute("client.id", client_id)
63
+
64
+ execution_context = replace(
65
+ base_context,
66
+ metadata={**base_context.metadata, "client_id": client_id},
67
+ )
68
+ websocket_manager.set_context(client_id, execution_context)
69
+
70
+ reset_current_context(token)
71
+ token = set_current_context(execution_context)
72
+
73
+ try:
74
+ while True:
75
+ data = await websocket.receive_text()
76
+ try:
77
+ message = json.loads(data)
78
+ await handle_client_message(client_id, message)
79
+ except json.JSONDecodeError:
80
+ logger.error(f"从客户端 {client_id} 收到无效JSON消息: {data}")
81
+ await websocket_manager.send_personal_message(
82
+ json.dumps({"error": "Invalid JSON format"}), client_id
83
+ )
84
+ except WebSocketDisconnect:
85
+ websocket_manager.disconnect(client_id)
86
+ except Exception as e:
87
+ logger.error(f"WebSocket连接处理异常: {e}")
88
+ websocket_manager.disconnect(client_id)
89
+ finally:
90
+ reset_current_context(token)
91
+ websocket_manager.clear_context(client_id)
92
+
35
93
 
36
94
  async def handle_client_message(client_id: str, message: Dict[str, Any]):
37
95
  """处理来自客户端的消息"""
@@ -42,8 +100,7 @@ async def handle_client_message(client_id: str, message: Dict[str, Any]):
42
100
  task_id_str = message.get("task_id")
43
101
  if not task_id_str:
44
102
  await websocket_manager.send_personal_message(
45
- json.dumps({"error": "Missing task_id in subscribe message"}),
46
- client_id
103
+ json.dumps({"error": "Missing task_id in subscribe message"}), client_id
47
104
  )
48
105
  return
49
106
 
@@ -51,11 +108,12 @@ async def handle_client_message(client_id: str, message: Dict[str, Any]):
51
108
  task_id = uuid.UUID(task_id_str)
52
109
  success = await websocket_manager.subscribe_to_task(client_id, task_id)
53
110
  response = {"success": success}
54
- await websocket_manager.send_personal_message(json.dumps(response), client_id)
111
+ await websocket_manager.send_personal_message(
112
+ json.dumps(response), client_id
113
+ )
55
114
  except ValueError:
56
115
  await websocket_manager.send_personal_message(
57
- json.dumps({"error": "Invalid task_id format"}),
58
- client_id
116
+ json.dumps({"error": "Invalid task_id format"}), client_id
59
117
  )
60
118
 
61
119
  elif message_type == "unsubscribe":
@@ -64,7 +122,7 @@ async def handle_client_message(client_id: str, message: Dict[str, Any]):
64
122
  if not task_id_str:
65
123
  await websocket_manager.send_personal_message(
66
124
  json.dumps({"error": "Missing task_id in unsubscribe message"}),
67
- client_id
125
+ client_id,
68
126
  )
69
127
  return
70
128
 
@@ -72,15 +130,15 @@ async def handle_client_message(client_id: str, message: Dict[str, Any]):
72
130
  task_id = uuid.UUID(task_id_str)
73
131
  success = await websocket_manager.unsubscribe_from_task(client_id, task_id)
74
132
  response = {"success": success}
75
- await websocket_manager.send_personal_message(json.dumps(response), client_id)
133
+ await websocket_manager.send_personal_message(
134
+ json.dumps(response), client_id
135
+ )
76
136
  except ValueError:
77
137
  await websocket_manager.send_personal_message(
78
- json.dumps({"error": "Invalid task_id format"}),
79
- client_id
138
+ json.dumps({"error": "Invalid task_id format"}), client_id
80
139
  )
81
140
 
82
141
  else:
83
142
  await websocket_manager.send_personal_message(
84
- json.dumps({"error": f"Unknown message type: {message_type}"}),
85
- client_id
143
+ json.dumps({"error": f"Unknown message type: {message_type}"}), client_id
86
144
  )
@@ -1,22 +1,29 @@
1
1
  from __future__ import annotations
2
+
2
3
  import asyncio
3
- import uuid
4
4
  import json
5
- from typing import Dict, List, Set, Any
5
+ import uuid
6
+ from typing import Any
7
+
6
8
  from fastapi import WebSocket, WebSocketDisconnect
7
9
  from loguru import logger
10
+
11
+ from ..execution_context import ExecutionContext
8
12
  from .task_manager import TaskManager
9
13
 
14
+
10
15
  class WebSocketManager:
11
16
  def __init__(self):
12
17
  # 存储活动连接: {client_id: websocket}
13
- self.active_connections: Dict[str, WebSocket] = {}
18
+ self.active_connections: dict[str, WebSocket] = {}
19
+ # 存储链接的 ExecutionContext
20
+ self.connection_contexts: dict[str, ExecutionContext] = {}
14
21
  # 存储任务与客户端的映射: {task_id: client_id}
15
- self.task_client_mapping: Dict[uuid.UUID, str] = {}
22
+ self.task_client_mapping: dict[uuid.UUID, str] = {}
16
23
  # 存储客户端订阅的任务: {client_id: set(task_id)}
17
- self.client_task_subscriptions: Dict[str, Set[uuid.UUID]] = {}
24
+ self.client_task_subscriptions: dict[str, set[uuid.UUID]] = {}
18
25
  # 存储客户端历史记录,用于重连时恢复订阅: {client_id: last_active_time}
19
- self.client_history: Dict[str, float] = {}
26
+ self.client_history: dict[str, float] = {}
20
27
  # 设置客户端记录过期时间(秒),默认0.5小时
21
28
  self.client_history_expiry = 0.5 * 60 * 60
22
29
  # 初始化任务管理器
@@ -51,11 +58,14 @@ class WebSocketManager:
51
58
  "type": "connection established",
52
59
  "client_id": client_id,
53
60
  "timestamp": str(asyncio.get_event_loop().time()),
54
- "restored_subscriptions": []
61
+ "restored_subscriptions": [],
55
62
  }
56
63
 
57
64
  # 如果有历史订阅,恢复它们
58
- if client_id in self.client_task_subscriptions and self.client_task_subscriptions[client_id]:
65
+ if (
66
+ client_id in self.client_task_subscriptions
67
+ and self.client_task_subscriptions[client_id]
68
+ ):
59
69
  restored_tasks = []
60
70
  for task_id in self.client_task_subscriptions[client_id]:
61
71
  restored_tasks.append(str(task_id))
@@ -74,6 +84,7 @@ class WebSocketManager:
74
84
  # 更新客户端的最后活动时间
75
85
  self.client_history[client_id] = asyncio.get_event_loop().time()
76
86
  logger.info(f"客户端 {client_id} 已断开WebSocket连接,保留订阅信息")
87
+ self.clear_context(client_id)
77
88
 
78
89
  async def subscribe_to_task(self, client_id: str, task_id: uuid.UUID) -> bool:
79
90
  """客户端订阅任务"""
@@ -98,7 +109,22 @@ class WebSocketManager:
98
109
 
99
110
  return False
100
111
 
101
- def create_task_with_client(self, task_id: uuid.UUID, client_id: str, workflow_name: str = "Unknown", steps: int = 1) -> bool:
112
+ def set_context(self, client_id: str, context: ExecutionContext) -> None:
113
+ self.connection_contexts[client_id] = context
114
+
115
+ def get_context(self, client_id: str) -> ExecutionContext | None:
116
+ return self.connection_contexts.get(client_id)
117
+
118
+ def clear_context(self, client_id: str) -> None:
119
+ self.connection_contexts.pop(client_id, None)
120
+
121
+ def create_task_with_client(
122
+ self,
123
+ task_id: uuid.UUID,
124
+ client_id: str,
125
+ workflow_name: str = "Unknown",
126
+ steps: int = 1,
127
+ ) -> bool:
102
128
  """创建任务与客户端的映射,并添加到任务管理器"""
103
129
  # 建立任务与客户端的映射
104
130
  self.task_client_mapping[task_id] = client_id
@@ -107,7 +133,7 @@ class WebSocketManager:
107
133
  if client_id not in self.client_task_subscriptions:
108
134
  self.client_task_subscriptions[client_id] = set()
109
135
  self.client_task_subscriptions[client_id].add(task_id)
110
-
136
+
111
137
  # 添加任务到任务管理器
112
138
  self.task_manager.add_task(task_id, client_id, workflow_name, steps)
113
139
 
@@ -132,11 +158,11 @@ class WebSocketManager:
132
158
  return # 没有关联的客户端
133
159
 
134
160
  client_id = self.task_client_mapping[task_id]
135
-
161
+
136
162
  # 确保task_id是字符串,避免JSON序列化问题
137
163
  if "task_id" in message and isinstance(message["task_id"], uuid.UUID):
138
164
  message["task_id"] = str(message["task_id"])
139
-
165
+
140
166
  # 递归处理嵌套字典中的UUID
141
167
  def convert_uuids(obj):
142
168
  if isinstance(obj, dict):
@@ -147,18 +173,21 @@ class WebSocketManager:
147
173
  return str(obj)
148
174
  else:
149
175
  return obj
150
-
176
+
151
177
  message = convert_uuids(message)
152
178
  message_str = json.dumps(message)
153
179
  await self.send_personal_message(message_str, client_id)
154
180
 
155
- async def send_task_status(self, task_id: uuid.UUID, status: str, node: str = None, progress: float = None, error: str = None):
181
+ async def send_task_status(
182
+ self,
183
+ task_id: uuid.UUID,
184
+ status: str,
185
+ node: str = None,
186
+ progress: float = None,
187
+ error: str = None,
188
+ ):
156
189
  """发送任务状态更新"""
157
- message = {
158
- "task_id": str(task_id),
159
- "type": "status",
160
- "status": status
161
- }
190
+ message = {"task_id": str(task_id), "type": "status", "status": status}
162
191
 
163
192
  if node is not None:
164
193
  message["node"] = node
@@ -175,7 +204,7 @@ class WebSocketManager:
175
204
  """发送任务开始执行消息"""
176
205
  # 获取客户端ID
177
206
  client_id = self.task_client_mapping.get(task_id)
178
-
207
+
179
208
  # 更新任务状态为运行中
180
209
  self.task_manager.start_task(task_id)
181
210
 
@@ -186,19 +215,16 @@ class WebSocketManager:
186
215
 
187
216
  # 获取全局任务队列信息
188
217
  global_queue_info = self.task_manager.get_global_queue_info()
189
-
218
+
190
219
  # 获取当前任务在队列中的位置
191
220
  queue_position = self.task_manager.get_queue_position(task_id)
192
221
 
193
222
  message = {
194
223
  "task_id": str(task_id),
195
224
  "type": "execution start",
196
- "client_tasks": {
197
- "total": len(client_tasks),
198
- "tasks": client_tasks
199
- },
225
+ "client_tasks": {"total": len(client_tasks), "tasks": client_tasks},
200
226
  "global_queue": global_queue_info,
201
- "queue_position": queue_position
227
+ "queue_position": queue_position,
202
228
  }
203
229
  await self.send_to_task_client(task_id, message)
204
230
 
@@ -214,7 +240,7 @@ class WebSocketManager:
214
240
 
215
241
  # 获取全局任务队列信息
216
242
  global_queue_info = self.task_manager.get_global_queue_info()
217
-
243
+
218
244
  # 获取当前任务在队列中的位置
219
245
  queue_position = self.task_manager.get_queue_position(task_id)
220
246
 
@@ -222,12 +248,9 @@ class WebSocketManager:
222
248
  "task_id": str(task_id),
223
249
  "type": "executing",
224
250
  "node": node,
225
- "client_tasks": {
226
- "total": len(client_tasks),
227
- "tasks": client_tasks
228
- },
251
+ "client_tasks": {"total": len(client_tasks), "tasks": client_tasks},
229
252
  "global_queue": global_queue_info,
230
- "queue_position": queue_position
253
+ "queue_position": queue_position,
231
254
  }
232
255
  await self.send_to_task_client(task_id, message)
233
256
 
@@ -243,7 +266,7 @@ class WebSocketManager:
243
266
 
244
267
  # 获取全局任务队列信息
245
268
  global_queue_info = self.task_manager.get_global_queue_info()
246
-
269
+
247
270
  # 获取当前任务在队列中的位置
248
271
  queue_position = self.task_manager.get_queue_position(task_id)
249
272
 
@@ -252,12 +275,9 @@ class WebSocketManager:
252
275
  "type": "progress",
253
276
  "node": node,
254
277
  "progress": progress,
255
- "client_tasks": {
256
- "total": len(client_tasks),
257
- "tasks": client_tasks
258
- },
278
+ "client_tasks": {"total": len(client_tasks), "tasks": client_tasks},
259
279
  "global_queue": global_queue_info,
260
- "queue_position": queue_position
280
+ "queue_position": queue_position,
261
281
  }
262
282
  await self.send_to_task_client(task_id, message)
263
283
 
@@ -273,7 +293,7 @@ class WebSocketManager:
273
293
 
274
294
  # 获取全局任务队列信息
275
295
  global_queue_info = self.task_manager.get_global_queue_info()
276
-
296
+
277
297
  # 获取当前任务在队列中的位置
278
298
  queue_position = self.task_manager.get_queue_position(task_id)
279
299
 
@@ -281,17 +301,15 @@ class WebSocketManager:
281
301
  "task_id": str(task_id),
282
302
  "type": "executed",
283
303
  "node": node,
284
- "client_tasks": {
285
- "total": len(client_tasks),
286
- "tasks": client_tasks
287
- },
304
+ "client_tasks": {"total": len(client_tasks), "tasks": client_tasks},
288
305
  "global_queue": global_queue_info,
289
- "queue_position": queue_position
306
+ "queue_position": queue_position,
290
307
  }
291
308
 
292
309
  if result is not None:
293
310
  # 检查是否为协程对象
294
311
  import asyncio
312
+
295
313
  if asyncio.iscoroutine(result):
296
314
  message["result"] = "<coroutine object>"
297
315
  else:
@@ -307,7 +325,7 @@ class WebSocketManager:
307
325
  """发送执行错误消息"""
308
326
  # 获取客户端ID
309
327
  client_id = self.task_client_mapping.get(task_id)
310
-
328
+
311
329
  # 更新任务状态为失败
312
330
  self.task_manager.fail_task(task_id, error)
313
331
 
@@ -318,7 +336,7 @@ class WebSocketManager:
318
336
 
319
337
  # 获取全局任务队列信息
320
338
  global_queue_info = self.task_manager.get_global_queue_info()
321
-
339
+
322
340
  # 获取当前任务在队列中的位置
323
341
  queue_position = self.task_manager.get_queue_position(task_id)
324
342
 
@@ -327,12 +345,9 @@ class WebSocketManager:
327
345
  "type": "execution error",
328
346
  "node": node,
329
347
  "error": error,
330
- "client_tasks": {
331
- "total": len(client_tasks),
332
- "tasks": client_tasks
333
- },
348
+ "client_tasks": {"total": len(client_tasks), "tasks": client_tasks},
334
349
  "global_queue": global_queue_info,
335
- "queue_position": queue_position
350
+ "queue_position": queue_position,
336
351
  }
337
352
  await self.send_to_task_client(task_id, message)
338
353
 
@@ -360,8 +375,10 @@ class WebSocketManager:
360
375
  # 查找过期的客户端记录
361
376
  for client_id, last_active_time in self.client_history.items():
362
377
  # 如果客户端不在活动连接中且超过过期时间,则标记为过期
363
- if (client_id not in self.active_connections and
364
- current_time - last_active_time > self.client_history_expiry):
378
+ if (
379
+ client_id not in self.active_connections
380
+ and current_time - last_active_time > self.client_history_expiry
381
+ ):
365
382
  expired_clients.append(client_id)
366
383
 
367
384
  # 清理过期客户端的订阅记录