diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..a3216e7
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,67 @@
+# AGENTS.md
+
+本文档面向 AI 编程助手,介绍本项目的结构、技术栈与开发约定。
+
+## 项目概述
+
+本项目是一个 [AstrBot](https://github.com/AstrBotDevs/AstrBot) 插件,目录名为 `astrbot_plugin_njupt_suan`,但当前代码是基于 AstrBot 官方插件模板的初始状态。插件仅实现了一个简单的 `helloworld` 指令,用于演示 AstrBot 插件的基本写法。
+
+- **技术栈**:Python 3(异步 asyncio)
+- **运行方式**:不能独立运行,必须由 AstrBot 主程序动态加载
+- **项目规模**:极简单文件插件,目前仅包含一个入口模块
+
+## 项目结构
+
+```
+.
+├── main.py # 插件主代码,注册指令与事件处理逻辑
+├── metadata.yaml # AstrBot 插件元数据(名称、版本、作者等)
+├── README.md # 面向用户的项目说明
+├── LICENSE # 许可证文件
+├── .gitignore # Python 通用 gitignore
+└── AGENTS.md # 本文档
+```
+
+> 注意:项目中**不存在** `pyproject.toml`、`setup.py`、`requirements.txt`、`package.json` 等常规构建配置文件,因为 AstrBot 插件是直接在宿主环境中运行的动态模块。
+
+## 核心文件说明
+
+### `main.py`
+- 定义插件类 `MyPlugin`,继承自 `astrbot.api.star.Star`
+- 使用 `@register(...)` 装饰器向 AstrBot 注册插件
+- 使用 `@filter.command("helloworld")` 注册聊天指令 `/helloworld`
+- 实现了可选的生命周期方法 `initialize()`(初始化)和 `terminate()`(卸载)
+
+### `metadata.yaml`
+- AstrBot 识别插件的核心配置文件
+- 关键字段:`name`(唯一标识名)、`version`、`author`、`desc`、`repo`
+- 当前内容仍为模板默认值(`helloworld`),后续开发需根据实际功能更新
+
+## 开发与修改约定
+
+1. **单文件扩展**:当前所有逻辑集中在 `main.py`。若功能变复杂,可在同目录下新增 `.py` 模块并在 `main.py` 中导入。
+2. **命名规范**:
+ - 插件唯一名(`metadata.yaml` 中的 `name`)建议以 `astrbot_plugin_` 开头
+ - 类名使用大驼峰(如 `MyPlugin`)
+ - 指令名使用小写英文+下划线
+3. **注释语言**:代码内注释以中文为主,README 为中英混合。新增注释建议保持中文。
+4. **依赖管理**:如需引入第三方 Python 包,请在 `metadata.yaml` 同级目录下创建 `requirements.txt`,AstrBot 会在安装插件时尝试自动安装。但目前项目中尚未创建该文件。
+
+## 构建与测试
+
+- **无独立构建步骤**:插件无需编译或打包成 wheel。
+- **测试方式**:需在本地部署 AstrBot 主程序,将本目录放入 AstrBot 的 `plugins/` 文件夹(或通过 AstrBot 插件市场/控制台安装),然后启动 AstrBot 进行联调。
+- **目前无单元测试**:目录内没有 `tests/` 文件夹或测试框架配置。
+
+## 部署流程
+
+1. 修改 `metadata.yaml` 中的 `name`、`display_name`、`desc`、`version`、`author`、`repo` 为实际信息。
+2. 修改 `main.py` 中的 `@register(...)` 参数与 `metadata.yaml` 保持一致。
+3. 如需额外依赖,添加 `requirements.txt`。
+4. 将代码推送到远程仓库后,可在 AstrBot 插件市场提交或通过 Git 链接直接安装。
+
+## 安全提示
+
+- 插件运行在 AstrBot 主程序的 Python 进程中,拥有与宿主相同的权限。
+- 避免在插件中执行不可信的外部命令或随意 `eval` 用户输入。
+- 若需存储持久化数据,建议使用 AstrBot 提供的配置/数据目录 API,而不是随意写入当前工作目录。
diff --git a/main.py b/main.py
index 9ac3429..988e545 100644
--- a/main.py
+++ b/main.py
@@ -8,8 +8,8 @@ from astrbot.api import logger
from astrbot.core import AstrBotConfig
from astrbot.core.message.message_event_result import MessageEventResult
-from schedule_utils import convert_tuple_schedule_to_dict
-from schedule_renderer import ScheduleRenderer
+from .schedule_utils import convert_tuple_schedule_to_dict
+from .schedule_renderer import ScheduleRenderer
class SuanPlugin(Star):
@@ -96,7 +96,7 @@ class SuanPlugin(Star):
image_data = await self.renderer.render_to_png(
html_content,
debug_path='/home/fanfan/test_daily.html',
- width=880
+ width=680
)
# 返回图片消息
diff --git a/schedule_renderer.py b/schedule_renderer.py
index 0235203..a3961bc 100644
--- a/schedule_renderer.py
+++ b/schedule_renderer.py
@@ -5,7 +5,7 @@ import tempfile
import subprocess
from astrbot.api import logger
-from schedule_utils import get_period_time, format_weeks
+from .schedule_utils import get_period_time, format_weeks
class ScheduleRenderer:
@@ -15,11 +15,14 @@ class ScheduleRenderer:
self.config = config or {}
def generate_html(self, schedule: list[dict], title: str) -> str:
- """生成课程表 HTML"""
+ """生成课程表 HTML(支持 rowspan 合并连续节次)"""
weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
weekday_short = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
- grid = [[[] for _ in range(7)] for _ in range(12)]
+
+ # grid 存储 (course_key, html_content) 或 None(被合并的单元格)
+ # course_key 用于识别同一门课:name|teacher|classroom|weeks_str
+ grid = [[None for _ in range(7)] for _ in range(12)]
for course in schedule:
name = course.get('name', '未知课程')
@@ -27,34 +30,43 @@ class ScheduleRenderer:
classroom = course.get('classroom') or ''
weeks = course.get('weeks', [])
day = course.get('day', 1)
- classes = course.get('classes', [])
+ classes = sorted(course.get('classes', []))
day_idx = day - 1
- if day_idx < 0 or day_idx > 6:
+ if day_idx < 0 or day_idx > 6 or not classes:
continue
-
+
+ # 生成课程 key 和 HTML 内容
+ weeks_str = format_weeks(weeks)
+ course_key = f"{name}|{teacher}|{classroom}|{weeks_str}"
+
course_text = f"{name}"
if teacher:
course_text += f"
{teacher}"
if classroom:
course_text += f"
@{classroom}"
if weeks:
- course_text += f"
第{format_weeks(weeks)}周"
-
- for class_idx in classes:
- if 1 <= class_idx <= 12:
- grid[class_idx - 1][day_idx].append(course_text)
+ course_text += f"
第{weeks_str}周"
+
+ # 填充到 grid(只填第一个节次)
+ first_period = min(classes)
+ if 1 <= first_period <= 12:
+ grid[first_period - 1][day_idx] = (course_key, course_text, len(classes))
# 判断是否隐藏空行空列
hide_void = self.config.get('hide_void_columns_rows', True)
- # 计算有课的行和列
+ # 计算有课的行和列(考虑 rowspan)
active_rows = set()
active_cols = set()
for row_idx in range(12):
for col_idx in range(7):
if grid[row_idx][col_idx]:
active_rows.add(row_idx)
+ # rowspan 占据的行也算有课
+ rowspan = grid[row_idx][col_idx][2]
+ for r in range(row_idx, min(row_idx + rowspan, 12)):
+ active_rows.add(r)
active_cols.add(col_idx)
# 如果没有课,默认显示全部
@@ -149,6 +161,8 @@ class ScheduleRenderer:
padding: 15px 10px;
border-radius: 12px;
text-align: center;
+ height: 190px;
+ box-sizing: border-box;
}}
td.time-col .period-num {{
@@ -168,21 +182,23 @@ class ScheduleRenderer:
border-radius: 12px;
padding: 10px;
vertical-align: top;
- height: 110px;
+ height: 190px;
+ box-sizing: border-box;
}}
.course-item {{
background: #00838f;
border-radius: 10px;
padding: 6px;
- margin: 2px 0;
+ margin: 0;
color: #e0f7fa;
line-height: 1.15;
font-size: 17px;
- height: 180px;
+ height: 100%;
text-align: center;
display: table;
width: 100%;
+ box-sizing: border-box;
}}
.course-item-inner {{
@@ -244,10 +260,14 @@ class ScheduleRenderer:
return html
def _generate_table_body(self, grid: list, active_rows: list, active_cols: list) -> str:
- """生成表格主体 HTML"""
+ """生成表格主体 HTML(支持 rowspan)"""
rows = []
course_type_counter = 0
type_classes = ['type-a', 'type-b', 'type-c', 'type-d', 'type-e']
+
+ # 跟踪哪些单元格被 rowspan 占用了
+ # occupied[row][col] = True 表示该单元格被上方的 rowspan 占用
+ occupied = [[False for _ in range(7)] for _ in range(12)]
for period in active_rows:
start_time = get_period_time(period + 1)
@@ -259,17 +279,23 @@ class ScheduleRenderer:
'''
for day in active_cols:
- courses = grid[period][day]
- if courses:
- cell_content = ''
- for c in courses:
- type_class = type_classes[course_type_counter % len(type_classes)]
- course_type_counter += 1
- cell_content += f'