Skip to content

Commit c803388

Browse files
committed
优化IPC&URL,联动设置新增功能,支持JS的IPC连接
1 parent c59a092 commit c803388

23 files changed

Lines changed: 988 additions & 465 deletions

CHANGELOG/v2.2.5/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ v2.0 - Koharu(小鸟游星野) release 4
77
- 新增 **抽奖显示样式**,新增**更多格式**选项
88
- 新增 **数据备份**,新增**备份管理**
99
- 新增 **IPC 数据接口**,新增只读名单/历史
10+
- 新增 **课间禁用**,新增下课延迟禁用
11+
- 新增 **科目过滤**,新增课间归属选项
1012

1113
## 💡 功能优化
1214

@@ -15,6 +17,9 @@ v2.0 - Koharu(小鸟游星野) release 4
1517
- 优化 **通知服务**,精简**冗余**并优化动画
1618
- 优化 **IPC/URL**,按**软件名**定位通道
1719
- 优化 **URL协议注册**,同步启停IPC
20+
- 优化 **通知设置**,合并ClassIsland选项
21+
- 优化 **联动设置**,合并为单页
22+
- 优化 **ClassIsland联动**,新增上节结束计时
1823
- 优化 **备份管理**,还原列表可删除
1924
- 优化 **语音功能**,整合**音量控制**并降内存
2025
- 优化 **音频模块**,提升音乐播放**响应**

app/Language/modules/linkage_settings.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
"name": "上课前提前解禁时间",
1919
"description": "在上课前多少秒提前解禁(0-1440秒)",
2020
},
21+
"post_class_disable_delay": {
22+
"name": "下课后延迟禁用时间",
23+
"description": "下课后多少秒内延迟触发课间禁用(0-1440秒)",
24+
},
2125
"cses_import": {
2226
"name": "课程表导入",
2327
"description": "从CSES格式文件导入上课时间段,用于课间禁用功能",
@@ -88,6 +92,12 @@
8892
"name": "科目历史记录过滤",
8993
"description": "启用后,计算权重时只使用当前科目的历史记录",
9094
},
95+
"break_subject_name": {"name": "课间"},
96+
"subject_history_break_assignment": {
97+
"name": "课间归属",
98+
"description": "课间时段的科目历史记录归属到哪一类",
99+
"combo_items": ["课间", "上节课", "下节课"],
100+
},
91101
"data_source_settings": {
92102
"name": "数据源选择",
93103
"description": "设置课程数据源",
@@ -123,6 +133,10 @@
123133
"name": "Pre-class enable time",
124134
"description": "How many seconds before class to enable (0-1440 seconds)",
125135
},
136+
"post_class_disable_delay": {
137+
"name": "Post-class disable delay",
138+
"description": "Delay class-break disable for N seconds after class ends (0-1440 seconds)",
139+
},
126140
"cses_import": {
127141
"name": "Schedule import",
128142
"description": "Import class timetable from CSES file for auto-disable during class breaks",
@@ -207,6 +221,12 @@
207221
"name": "Subject History Filter",
208222
"description": "When enabled, only use current subject's history records for weight calculation",
209223
},
224+
"break_subject_name": {"name": "Break"},
225+
"subject_history_break_assignment": {
226+
"name": "Break assignment",
227+
"description": "Where to assign subject history during breaks",
228+
"combo_items": ["Break", "Previous class", "Next class"],
229+
},
210230
"data_source_settings": {
211231
"name": "Data Source Selection",
212232
"description": "Settings for course data source",

app/common/IPC_URL/csharp_ipc_handler.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
clr.AddReference("SecRandom4Ci.Interface")
2525

2626
# 导入程序集
27-
from System import Action
27+
from System import Action, DateTime
2828
from ClassIsland.Shared.Enums import TimeState
2929
from ClassIsland.Shared.IPC import IpcClient, IpcRoutedNotifyIds
3030
from ClassIsland.Shared.IPC.Abstractions.Services import IPublicLessonsService
@@ -69,6 +69,7 @@ def __init__(self):
6969
self.is_connected = False
7070
self._disconnect_logged = False # 跟踪是否已记录断连日志
7171
self._last_on_class_left_log_time = 0 # 上次记录距离上课时间的时间
72+
self._last_known_subject_name: Optional[str] = None
7273

7374
def start_ipc_client(self) -> bool:
7475
"""
@@ -195,7 +196,7 @@ def get_on_class_left_time(self) -> int:
195196

196197
return total_seconds
197198
except Exception as e:
198-
logger.exception(f"获取距离上课时间失败: {e}")
199+
logger.error(f"获取距离上课时间失败: {e}")
199200
return 0
200201

201202
def get_current_class_info(self) -> dict:
@@ -227,10 +228,11 @@ def get_current_class_info(self) -> dict:
227228
logger.debug("ClassIsland 当前没有课程")
228229
return {}
229230
logger.info(f"从 ClassIsland 获取当前课程: {class_name}")
231+
self._last_known_subject_name = class_name
230232
return {"name": class_name}
231233

232234
except Exception as e:
233-
logger.exception(f"从 ClassIsland 获取课程信息失败: {e}")
235+
logger.error(f"从 ClassIsland 获取课程信息失败: {e}")
234236
return {}
235237

236238
def get_next_class_info(self) -> dict:
@@ -265,9 +267,45 @@ def get_next_class_info(self) -> dict:
265267
return {"name": class_name}
266268

267269
except Exception as e:
268-
logger.exception(f"从 ClassIsland 获取下一节课信息失败: {e}")
270+
logger.error(f"从 ClassIsland 获取下一节课信息失败: {e}")
269271
return {}
270272

273+
def get_previous_class_info(self) -> dict:
274+
if not self._last_known_subject_name:
275+
return {}
276+
if self._last_known_subject_name.strip() == "???":
277+
return {}
278+
return {"name": self._last_known_subject_name}
279+
280+
def get_elapsed_since_previous_time_point_end_seconds(self) -> int:
281+
try:
282+
if not self.is_running or not self.is_connected:
283+
return 0
284+
285+
lessonSc = GeneratedIpcFactory.CreateIpcProxy[IPublicLessonsService](
286+
self.ipc_client.Provider, self.ipc_client.PeerProxy
287+
)
288+
289+
current_index = int(lessonSc.CurrentSelectedIndex)
290+
if current_index <= 0:
291+
return 0
292+
293+
class_plan = lessonSc.CurrentClassPlan
294+
if not class_plan:
295+
return 0
296+
297+
valid_items = class_plan.ValidTimeLayoutItems
298+
if not valid_items:
299+
return 0
300+
301+
previous_item = valid_items[current_index - 1]
302+
end_time = previous_item.EndTime
303+
elapsed = DateTime.Now.TimeOfDay - end_time
304+
total_seconds = int(elapsed.TotalSeconds)
305+
return max(0, total_seconds)
306+
except Exception:
307+
return 0
308+
271309
@staticmethod
272310
def convert_to_call_result(
273311
class_name: str, selected_students, draw_count: int, display_duration=5.0
@@ -288,6 +326,13 @@ def _on_class_test(self):
288326
lessonSc = GeneratedIpcFactory.CreateIpcProxy[IPublicLessonsService](
289327
self.ipc_client.Provider, self.ipc_client.PeerProxy
290328
)
329+
try:
330+
if lessonSc.CurrentSubject and lessonSc.CurrentSubject.Name:
331+
name = str(lessonSc.CurrentSubject.Name)
332+
if name and name.strip() != "???":
333+
self._last_known_subject_name = name
334+
except Exception:
335+
pass
291336
logger.debug(
292337
f"上课 {lessonSc.CurrentSubject.Name} 时间: {lessonSc.CurrentTimeLayoutItem}"
293338
)
@@ -432,6 +477,12 @@ def get_next_class_info(self) -> dict:
432477
"""
433478
return {}
434479

480+
def get_previous_class_info(self) -> dict:
481+
return {}
482+
483+
def get_elapsed_since_previous_time_point_end_seconds(self) -> int:
484+
return 0
485+
435486
@staticmethod
436487
def convert_to_call_result(
437488
class_name: str, selected_students, draw_count: int, display_duration=5.0

app/common/IPC_URL/url_ipc_handler.py

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import json
6+
import socket
67
import threading
78
from multiprocessing.connection import Client, Listener
89
from pathlib import Path
@@ -103,7 +104,7 @@ def start_ipc_server(self, port: int = 0) -> bool:
103104

104105
try:
105106
address, family = self._get_ipc_address_for_name(self.ipc_name)
106-
authkey = self._get_authkey(self.ipc_name)
107+
authkey = None
107108

108109
if family == "AF_UNIX":
109110
try:
@@ -171,13 +172,22 @@ def _run_server(self):
171172

172173
def _handle_connection(self, conn):
173174
try:
174-
data = conn.recv_bytes()
175+
if hasattr(conn, "_recv") and hasattr(conn, "_send"):
176+
data = self._recv_line_from_conn(conn)
177+
else:
178+
data = conn.recv_bytes()
175179
if not data:
176180
return
177181

178-
message = json.loads(data.decode("utf-8"))
182+
message = json.loads(data.decode("utf-8").strip())
179183
response = self._process_message(message)
180-
conn.send_bytes(json.dumps(response, ensure_ascii=False).encode("utf-8"))
184+
response_bytes = (
185+
json.dumps(response, ensure_ascii=False).encode("utf-8") + b"\n"
186+
)
187+
if hasattr(conn, "_send"):
188+
self._send_line_to_conn(conn, response_bytes)
189+
else:
190+
conn.send_bytes(response_bytes)
181191
except EOFError:
182192
return
183193
except Exception as e:
@@ -188,6 +198,34 @@ def _handle_connection(self, conn):
188198
except Exception:
189199
pass
190200

201+
def _recv_line_from_conn(self, conn, max_bytes: int = 262144) -> bytes:
202+
buf = bytearray()
203+
try:
204+
recv = getattr(conn, "_recv", None)
205+
if recv is None:
206+
return b""
207+
208+
while len(buf) < max_bytes:
209+
try:
210+
chunk = recv(1)
211+
except EOFError:
212+
break
213+
if not chunk:
214+
break
215+
buf += chunk
216+
if chunk == b"\n":
217+
break
218+
except Exception:
219+
return b""
220+
221+
return bytes(buf)
222+
223+
def _send_line_to_conn(self, conn, data: bytes) -> None:
224+
send = getattr(conn, "_send", None)
225+
if send is None:
226+
raise RuntimeError("IPC连接不支持原始发送")
227+
send(data)
228+
191229
def _process_message(self, message: Dict[str, Any]) -> Dict[str, Any]:
192230
"""处理接收到的消息"""
193231
message_type = message.get("type", "")
@@ -288,20 +326,58 @@ def send_ipc_message_by_name(
288326
try:
289327
target_name = self._normalize_ipc_name(target_ipc_name or self.ipc_name)
290328
address, family = self._get_ipc_address_for_name(target_name)
291-
authkey = self._get_authkey(target_name)
292-
293-
conn = Client(address=address, family=family, authkey=authkey)
294-
conn.send_bytes(json.dumps(message, ensure_ascii=False).encode("utf-8"))
295-
response_data = conn.recv_bytes()
296-
conn.close()
329+
request_bytes = (
330+
json.dumps(message, ensure_ascii=False).encode("utf-8") + b"\n"
331+
)
332+
response_data = self._send_stream_request(
333+
address=address,
334+
family=family,
335+
request_bytes=request_bytes,
336+
timeout=timeout,
337+
)
297338

298339
if not response_data:
299340
return None
300-
return json.loads(response_data.decode("utf-8"))
341+
return json.loads(response_data.decode("utf-8").strip())
301342
except Exception as e:
302343
logger.exception(f"发送IPC消息失败: {e}")
303344
return None
304345

346+
def _send_stream_request(
347+
self, address: str, family: str, request_bytes: bytes, timeout: float
348+
) -> Optional[bytes]:
349+
if os.name == "nt":
350+
conn = Client(address=address, family=family, authkey=None)
351+
try:
352+
conn.send_bytes(request_bytes)
353+
response = conn.recv_bytes()
354+
return response or None
355+
finally:
356+
try:
357+
conn.close()
358+
except Exception:
359+
pass
360+
361+
if family != "AF_UNIX":
362+
raise RuntimeError(f"不支持的IPC family: {family}")
363+
364+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
365+
try:
366+
sock.settimeout(max(0.1, float(timeout)))
367+
sock.connect(address)
368+
sock.sendall(request_bytes)
369+
sock_file = sock.makefile("rb")
370+
try:
371+
response = sock_file.readline()
372+
finally:
373+
sock_file.close()
374+
return response or None
375+
finally:
376+
try:
377+
sock.close()
378+
except Exception:
379+
pass
380+
305381
def send_ipc_message_to_app(
306382
self,
307383
target_app_name: str,

app/common/behind_scenes/behind_scenes_utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def get_behind_scenes_settings(use_cache=True):
3939
BehindScenesUtils._cache_timestamp = current_time
4040
return settings
4141
except Exception as e:
42-
logger.exception(f"读取内幕设置失败: {e}")
42+
logger.error(f"读取内幕设置失败: {e}")
4343
return {}
4444

4545
@staticmethod
@@ -79,7 +79,7 @@ def get_probability_settings(name, mode, pool_name=None):
7979
return {"enabled": False, "probability": 1.0}
8080
return {"enabled": False, "probability": 1.0}
8181
except Exception as e:
82-
logger.exception(f"获取概率设置失败: {e}")
82+
logger.error(f"获取概率设置失败: {e}")
8383
return {"enabled": False, "probability": 1.0}
8484

8585
@staticmethod
@@ -176,7 +176,7 @@ def apply_probability_weights(
176176

177177
return filtered_students, weights
178178
except Exception as e:
179-
logger.exception(f"应用内幕设置失败: {e}")
179+
logger.error(f"应用内幕设置失败: {e}")
180180
return students_dict_list, [1.0] * len(students_dict_list)
181181

182182
@staticmethod
@@ -239,7 +239,7 @@ def apply_probability_weights_to_items(items, mode, pool_name):
239239

240240
return filtered_items, weights
241241
except Exception as e:
242-
logger.exception(f"应用内幕设置失败: {e}")
242+
logger.error(f"应用内幕设置失败: {e}")
243243
return items, [1.0] * len(items)
244244

245245
@staticmethod
@@ -271,5 +271,5 @@ def ensure_guaranteed_selection(
271271

272272
return None
273273
except Exception as e:
274-
logger.exception(f"确保必中人员失败: {e}")
274+
logger.error(f"确保必中人员失败: {e}")
275275
return None

0 commit comments

Comments
 (0)