Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,5 @@ cython_debug/
/data/audio
/data/TEMP
/data/CSES
/typings
/data/images
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ uv sync
uv run main.py
```

#### 5. C# IPC 相关开发

您需要 .NET 9.0 来生成 .NET 程序集的 Python 存根(用于 IDE 提示)。如果您选择放弃 IDE 提示,无需安装 .NET 9.0。
程序集存放在 `data/dlls` 目录。下面是存根生成方法:

```bash
powershell ./scripts/generate-stubs.ps1
```

如果您正在使用 Linux 发行版,请先安装 [PowerShell 7](https://github.com/PowerShell/PowerShell),并将上方命令中的 `powershell` 替换为 `pwsh`。

### 贡献准则

**您为 SecRandom 贡献的功能须遵循以下准则:**
Expand Down
43 changes: 34 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,26 @@

## 📖 目录

- [🎯 为什么选择公平抽取](#-为什么选择公平抽取)
- [🌟 核心亮点](#-核心亮点)
- [📥 下载](#-下载)
- [📸 软件截图](#-软件截图)
- [🙏 贡献者](#-贡献者和特别感谢)
- [💝 捐献支持](#-捐献支持)
- [📞 联系方式](#-联系方式)
- [📄 官方文档](#-官方文档)
- [✨ Star历程](#-star历程)
- [SecRandom - 公平随机抽取系统](#secrandom---公平随机抽取系统)
- [📖 目录](#-目录)
- [🎯 为什么选择公平抽取](#-为什么选择公平抽取)
- [🌟 核心亮点](#-核心亮点)
- [🎯 智能公平抽取系统](#-智能公平抽取系统)
- [🎨 现代化用户体验](#-现代化用户体验)
- [🚀 强大功能集](#-强大功能集)
- [💻 系统兼容性](#-系统兼容性)
- [📥 下载](#-下载)
- [🌐 官方下载页面](#-官方下载页面)
- [📸 软件截图](#-软件截图)
- [🙏 贡献者和特别感谢](#-贡献者和特别感谢)
- [第三方依赖与代码](#第三方依赖与代码)
- [PythonNET-Stubs-Generator](#pythonnet-stubs-generator)
- [💝 捐献支持](#-捐献支持)
- [爱发电支持](#爱发电支持)
- [📞 联系方式](#-联系方式)
- [📄 官方文档](#-官方文档)
- [贡献指南与 Actions 构建工作流](#贡献指南与-actions-构建工作流)
- [✨ Star历程](#-star历程)

## 🎯 为什么选择公平抽取

Expand Down Expand Up @@ -139,6 +150,20 @@

<!-- ALL-CONTRIBUTORS-LIST:END -->

## 第三方依赖与代码

本项目使用了以下第三方代码:

### PythonNET-Stubs-Generator
- **路径**:`vendors/pythonnet-stub-generator/`
- **来源**:[MHDante/pythonnet-stub-generator](https://github.com/MHDante/pythonnet-stub-generator)
- **许可证**:MIT License
- **版权**
- Copyright (c) 2019 Robert McNeel & Associates
- Copyright (c) 2022 Dante Camarena
- **状态**:修改了编译目标平台为 .NET 9.0
- *注:原始 MIT License 文本保留在 `vendors/pythonnet-stub-generator/LICENSE.md` 中。*

## 💝 捐献支持

如果您觉得 SecRandom 对您有帮助,欢迎支持我们的开发工作!
Expand Down
219 changes: 219 additions & 0 deletions app/common/IPC_URL/csharp_ipc_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import asyncio
import threading
from typing import Optional
from loguru import logger

from app.tools.path_utils import get_data_path

CSHARP_AVAILABLE = False

try:
# 导入 Python.NET
from pythonnet import load
load("coreclr", runtime_config=get_data_path("dlls", "dotnet.runtimeconfig.json"))

# 加载 .NET CoreCLR 程序集
import clr
clr.AddReference("ClassIsland.Shared.IPC")
clr.AddReference("SecRandom4Ci.Interface")

# 导入程序集
from System import Action
from ClassIsland.Shared.Enums import TimeState
from ClassIsland.Shared.IPC import IpcClient, IpcRoutedNotifyIds
from ClassIsland.Shared.IPC.Abstractions.Services import IPublicLessonsService
from dotnetCampus.Ipc.CompilerServices.GeneratedProxies import GeneratedIpcFactory
from SecRandom4Ci.Interface.Services import ISecRandomService
from SecRandom4Ci.Interface.Models import CallResult, Student

CSHARP_AVAILABLE = True
except:

Check notice on line 30 in app/common/IPC_URL/csharp_ipc_handler.py

View check run for this annotation

codefactor.io / CodeFactor

app/common/IPC_URL/csharp_ipc_handler.py#L30

Do not use bare 'except'. (E722)
logger.warning("无法加载 Python.NET,将会回滚!")


if CSHARP_AVAILABLE:
class CSharpIPCHandler:
"""C# dotnetCampus.Ipc 处理器,用于连接 ClassIsland 实例"""
_instance: Optional["CSharpIPCHandler"] = None

def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance

@classmethod
def instance(cls):
"""获取单例实例"""
if cls._instance is None:
cls._instance = cls()
return cls._instance

def __init__(self):
"""
初始化 C# IPC 处理器
"""
self.ipc_client: Optional[IpcClient] = None
self.client_thread: Optional[threading.Thread] = None
self.is_running = False

def start_ipc_client(self) -> bool:
"""
启动 C# IPC 客户端

Returns:
启动成功返回True,失败返回False
"""
if self.is_running:
return True

try:
self.client_thread = threading.Thread(target=self._run_client, daemon=False)
self.client_thread.start()
self.is_running = True
return True
except Exception as e:
logger.error(f"启动 C# IPC 客户端失败: {e}")
return False

def stop_ipc_client(self):
"""停止 C# IPC 客户端"""
self.is_running = False
if self.client_thread and self.client_thread.is_alive():
self.client_thread.join(timeout=1)

def send_notification(
self,
class_name,
selected_students,
draw_count=1,
settings=None,
settings_group=None
) -> bool:
"""发送提醒"""

if settings:
display_duration = settings.get("notification_display_duration", 5)
else:
display_duration = 5

randomService = GeneratedIpcFactory.CreateIpcProxy[ISecRandomService](
self.ipc_client.Provider, self.ipc_client.PeerProxy)
result = self.convert_to_call_result(class_name, selected_students, draw_count, display_duration)
randomService.NotifyResult(result)

return True

def is_breaking(self) -> bool:
"""是否处于下课时间"""
lessonSc = GeneratedIpcFactory.CreateIpcProxy[IPublicLessonsService](
self.ipc_client.Provider, self.ipc_client.PeerProxy)
state = lessonSc.CurrentState in [getattr(TimeState, "None"), TimeState.PrepareOnClass, TimeState.Breaking, TimeState.AfterSchool]
logger.debug(f"获取到的 ClassIsland 时间状态: {lessonSc.CurrentState} 是否下课: {state}")
return state

@staticmethod
def convert_to_call_result(class_name: str, selected_students, draw_count: int, display_duration=5.0) -> CallResult:
result = CallResult()
result.ClassName = class_name
result.DrawCount = draw_count
result.DisplayDuration = display_duration
for student in selected_students:
cs_student = Student()
cs_student.StudentId = student[0]
cs_student.StudentName = student[1]
cs_student.Exists = student[2]
result.SelectedStudents.Add(cs_student)
return result

def _on_class_test(self):
lessonSc = GeneratedIpcFactory.CreateIpcProxy[IPublicLessonsService](
self.ipc_client.Provider, self.ipc_client.PeerProxy)
logger.debug(f"上课 {lessonSc.CurrentSubject.Name} 时间: {lessonSc.CurrentTimeLayoutItem}")

def _run_client(self):
"""运行 C# IPC 客户端"""

async def client():
"""异步客户端"""

self.ipc_client = IpcClient()
self.ipc_client.JsonIpcProvider.AddNotifyHandler(IpcRoutedNotifyIds.OnClassNotifyId, Action(lambda: self._on_class_test()))

task = self.ipc_client.Connect()
await loop.run_in_executor(None, lambda: task.Wait())

while self.is_running:
await asyncio.sleep(1)

self.ipc_client = None

# 启动新的 asyncio 事件循环
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(client())
loop.close()
else:
class CSharpIPCHandler:
"""C# dotnetCampus.Ipc 处理器,用于连接 ClassIsland 实例"""
_instance: Optional["CSharpIPCHandler"] = None

def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance

@classmethod
def instance(cls):
"""获取单例实例"""
if cls._instance is None:
cls._instance = cls()
return cls._instance

def __init__(self):
"""
初始化 C# IPC 处理器
"""
self.ipc_client = None
self.client_thread = None
self.is_running = False

def start_ipc_client(self) -> bool:
"""
启动 C# IPC 客户端

Returns:
启动成功返回True,失败返回False
"""
return False

def stop_ipc_client(self):
"""停止 C# IPC 客户端"""
pass

def send_notification(
self,
class_name,
selected_students,
draw_count=1,
settings=None,
settings_group=None
) -> bool:
"""发送提醒"""
return False

def is_breaking(self) -> bool:
"""是否处于下课时间"""
return False

@staticmethod
def convert_to_call_result(class_name: str, selected_students, draw_count: int, display_duration=5.0) -> object:
return object

def _on_class_test(self):
pass

def _run_client(self):
"""运行 C# IPC 客户端"""
pass
7 changes: 2 additions & 5 deletions app/common/extraction/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from qfluentwidgets import *

from app.Language.obtain_language import get_content_name_async
from app.common.IPC_URL.csharp_ipc_handler import CSharpIPCHandler
from app.common.extraction.cses_parser import CSESParser
from app.tools.path_utils import *
from app.tools.settings_access import readme_settings_async
Expand All @@ -37,11 +38,7 @@ def _is_non_class_time() -> bool:
)
logger.debug(f"是否启用了ClassIsland数据源: {use_class_island_source}")
if use_class_island_source:
class_island_break_status = readme_settings_async(
"time_settings", "current_class_island_break_status"
)
logger.debug(f"ClassIsland数据源当前课间状态: {class_island_break_status}")
return bool(class_island_break_status)
return CSharpIPCHandler.instance().is_breaking()
else:
current_day_of_week = _get_current_day_of_week()
class_times = _get_class_times_by_day(current_day_of_week)
Expand Down
41 changes: 39 additions & 2 deletions app/common/notification/notification_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from app.Language.obtain_language import get_any_position_value
from app.tools.settings_access import readme_settings_async
from app.common.IPC_URL.url_ipc_handler import URLIPCHandler
from app.common.IPC_URL.csharp_ipc_handler import CSharpIPCHandler


class NotificationContentWidget(QWidget):
Expand Down Expand Up @@ -696,8 +697,8 @@
do_not_steal_focus = readme_settings_async(
"floating_window_management", "do_not_steal_focus"
)
except Exception:
pass

Check notice on line 701 in app/common/notification/notification_service.py

View check run for this annotation

codefactor.io / CodeFactor

app/common/notification/notification_service.py#L700-L701

Try, Except, Pass detected. (B110)

# 如果没有设置"无焦点模式",则确保窗口保持在最前面并激活
if not do_not_steal_focus:
Expand Down Expand Up @@ -750,99 +751,99 @@
# 隐藏窗口
self.hide()

def update_content(
self,
student_labels,
settings=None,
font_settings_group=None,
settings_group=None,
):
"""更新通知窗口的内容

Args:
student_labels: 包含学生信息的BodyLabel控件列表
settings: 通知设置参数
font_settings_group: 字体设置组名称
settings_group: 通知设置组名称
"""
# 保存设置组,用于后续判断是否抢占焦点
if settings_group:
self.settings_group = settings_group
elif font_settings_group:
# 如果没有提供settings_group,尝试从font_settings_group推断
if font_settings_group == "roll_call_settings":
self.settings_group = "roll_call_notification_settings"
elif font_settings_group == "quick_draw_settings":
self.settings_group = "quick_draw_notification_settings"
elif font_settings_group == "lottery_settings":
self.settings_group = "lottery_notification_settings"

# 清除现有内容
while self.content_layout.count():
item = self.content_layout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()

# 添加新内容
if student_labels:
for label in student_labels:
# 如果有字体设置,则应用到标签上
if font_settings_group:
# 检查是否使用全局字体
use_global_font = readme_settings_async(
font_settings_group, "use_global_font"
)
custom_font = None
if use_global_font == 1: # 不使用全局字体,使用自定义字体
custom_font = readme_settings_async(
font_settings_group, "custom_font"
)
if custom_font and hasattr(label, "setStyleSheet"):
# 获取当前样式表并添加字体设置
current_style = label.styleSheet()
label.setStyleSheet(
f"font-family: '{custom_font}'; {current_style}"
)
self.content_layout.addWidget(label)

# 确保颜色与当前主题同步
try:
self._on_theme_changed()
except Exception as e:
logger.exception("更新内容时同步主题时出错(已忽略): {}", e)

# 调整窗口大小以适应内容
self.adjustSize()

# 检查窗口是否已经显示
if self.isVisible():
# 如果窗口已经显示,只更新内容和透明度,不重新定位窗口
if settings:
transparency = settings.get("transparency", 0.6)
self.setWindowOpacity(transparency)

# 重新应用倒计时设置,但不重新定位窗口
auto_close_time = settings.get("auto_close_time", 5)
if auto_close_time > 0:
self.auto_close_timer.stop()
self.auto_close_timer.setInterval(auto_close_time * 1000)
# 初始化倒计时并启动更新定时器
self.countdown_timer.stop()
self.remaining_time = auto_close_time
self.update_countdown_display()
self.auto_close_timer.start()
self.countdown_timer.start(1000) # 每秒更新一次
else:
# 停止倒计时更新定时器并显示手动关闭提示
self.countdown_timer.stop()
self.countdown_label.setText("连续点击3次关闭窗口")
else:
# 如果窗口未显示,完整初始化窗口
self.apply_settings(settings)

# 显示窗口
self.start_show_animation(settings)

Check notice on line 846 in app/common/notification/notification_service.py

View check run for this annotation

codefactor.io / CodeFactor

app/common/notification/notification_service.py#L754-L846

Complex Method


class FloatingNotificationManager:
Expand All @@ -863,15 +864,15 @@
self.ipc_handler = URLIPCHandler("SecRandom", "secrandom")
self._initialized = True

def send_to_classisland(
def send_to_classisland2(
self,
class_name,
selected_students,
draw_count=1,
settings=None,
settings_group=None,
):
"""发送通知到ClassIsland
"""发送通知到ClassIsland(旧版)

Args:
class_name: 班级名称
Expand Down Expand Up @@ -938,6 +939,42 @@
class_name, selected_students, draw_count, settings, settings_group
)

def send_to_classisland(
self,
class_name,
selected_students,
draw_count=1,
settings=None,
settings_group=None,
):
"""发送通知到ClassIsland

Args:
class_name: 班级名称
selected_students: 选中的学生列表 [(学号, 姓名, 是否存在), ...]
draw_count: 抽取的学生数量
settings: 通知设置参数
settings_group: 设置组名称
"""

try:
cs_ipc = CSharpIPCHandler.instance()
status = cs_ipc.send_notification(class_name, selected_students, draw_count, settings, settings_group)
if status:
logger.info("成功发送通知到ClassIsland,结果未知")
else:
logger.info("因错误回退到SecRandom通知服务")
self._show_secrandom_notification(
class_name, selected_students, draw_count, settings, settings_group
)
except Exception as e:
logger.exception("发送通知到ClassIsland时出错: {}", e)
# 如果发生异常,回退到SecRandom通知服务
logger.info("因错误回退到SecRandom通知服务")
self._show_secrandom_notification(
class_name, selected_students, draw_count, settings, settings_group
)

def _show_secrandom_notification(
self,
class_name,
Expand Down
Loading