koishi-plugin-codex-reset 0.1.3 → 0.1.4

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 lumia1998
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lumia1998
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,54 +1,56 @@
1
- # koishi-plugin-codex-reset
2
-
3
- 监控 [AIHOT Codex 重置 API](https://aihot.news/api/v1/codex-resets) 的 Koishi 插件。
4
-
5
- ## 功能
6
-
7
- - 定时轮询 AIHOT 的 Codex / ChatGPT Work 重置事件快照。
8
- - 支持推送新的重置预告、已确认的全员重置和重置卡事件。
9
- - 目标支持平台 + 频道配置,例如 `onebot` + `123456789`。
10
- - `codex.probability` / `重置概率`:生成精美的概率统计卡片图片;没有 Puppeteer 时回退为纯文本。
11
- - `codex.event` / `最近重置`:生成最近一次已确认重置的浅色卡片,展示距今时间、类型和来源帖子。
12
- - `codex.calendar` / `重置日历`:生成本月的 PNG 日历图片,右侧固定展示最近发布的 2 条来源动态。
13
- - `codex.history` / `重置历史`:生成公开重置事件的历史时间线图片,默认展示最近 1 个月。
14
- - 日历中的 Tibo 头像作为本地资源随插件发布,不依赖用户访问 X 或运行时下载图片。
15
-
16
- ## 命令
17
-
18
- 命令都可以直接使用;历史时间线支持时间范围参数:
19
-
20
- - `codex.probability`,别名 `重置概率`:读取当前 API 快照,生成概率统计卡片,展示未来 24 小时和 7 天概率、样本概览、事件构成和最近确认日期;没有 Puppeteer 时回退为纯文本。
21
- - `codex.event`,别名 `最近重置`:发送最近一次已确认重置的浅色卡片;没有 Puppeteer 时回退为文本。
22
- - `codex.calendar`,别名 `重置日历`:发送本月日历 PNG;右侧固定显示最近发布的 2 条动态。
23
- - `codex.history`,别名 `重置历史`:发送历史时间线 PNG;`重置历史` 查看最近 1 个月,`重置历史 -3` 查看最近 3 个月,`重置历史 -a` 查看全部历史。
24
-
25
- 概率、最近重置、日历和历史时间线命令始终启用;安装并启用 `koishi-plugin-puppeteer` 后会发送对应图片,没有 Puppeteer 时概率和最近重置会回退为纯文本,日历和历史时间线会提示安装 Puppeteer。
1
+ # koishi-plugin-codex-reset
2
+
3
+ 监控 [AIHOT Codex 重置 API](https://aihot.news/api/v1/codex-resets) 的 Koishi 插件。
4
+
5
+ ## 功能
6
+
7
+ - 定时轮询 AIHOT 的 Codex / ChatGPT Work 重置事件快照。
8
+ - 支持推送新的重置预告、已确认的全员重置和重置卡事件。
9
+ - 目标支持平台 + 频道配置,例如 `onebot` + `123456789`。
10
+ - `codex.probability` / `重置概率`:生成精美的概率统计卡片图片;没有 Puppeteer 时回退为纯文本。
11
+ - `codex.event` / `最近重置`:生成最近一次已确认重置的浅色卡片,展示距今时间、类型和来源帖子。
12
+ - `codex.calendar` / `重置日历`:生成本月的 PNG 日历图片,右侧固定展示最近发布的 2 条来源动态。
13
+ - `codex.history` / `重置历史`:生成公开重置事件的历史时间线图片,默认展示最近 1 个月。
14
+ - 日历中的 Tibo 头像作为本地资源随插件发布,不依赖用户访问 X 或运行时下载图片。
15
+
16
+ ## 命令
17
+
18
+ 命令都可以直接使用;历史时间线支持时间范围参数:
19
+
20
+ - `codex.probability`,别名 `重置概率`:读取当前 API 快照,生成概率统计卡片,展示未来 24 小时和 7 天概率、样本概览、事件构成和最近确认日期;没有 Puppeteer 时回退为纯文本。
21
+ - `codex.event`,别名 `最近重置`:发送最近一次已确认重置的浅色卡片;没有 Puppeteer 时回退为文本。
22
+ - `codex.calendar`,别名 `重置日历`:发送本月日历 PNG;右侧固定显示最近发布的 2 条动态。
23
+ - `codex.history`,别名 `重置历史`:发送历史时间线 PNG;`重置历史` 查看最近 1 个月,`重置历史 -3` 查看最近 3 个月,`重置历史 -a` 查看全部历史。
24
+
25
+ 概率、最近重置、日历和历史时间线命令始终启用;安装并启用 `koishi-plugin-puppeteer` 后会发送对应图片,没有 Puppeteer 时概率和最近重置会回退为纯文本,日历和历史时间线会提示安装 Puppeteer。
26
+
27
+ 日历、月度统计及右侧动态仅展示已确认且日期不晚于今天的事件,不展示预告。日期优先使用实际发生日期,缺失时使用北京时间的确认日期;两者均缺失时不展示,不使用预计时间窗口代替实际日期。
26
28
 
27
29
  首次轮询只建立本地基线,不会把 API 返回的历史事件全部推送;后续轮询只处理新增事件或 `announced` → `confirmed` 的状态变化。插件重启后会重新建立基线。
28
-
29
- ## 安装
30
-
31
- ```bash
32
- npm install koishi-plugin-codex-reset
33
- ```
34
-
35
- 如果需要概率卡片、最近重置卡片、日历或历史时间线图片,再安装并启用 `koishi-plugin-puppeteer`。不安装 Puppeteer 时,监控、最近重置和概率分析仍然可以使用,概率与最近重置命令回退为纯文本,日历和历史命令会给出提示。
36
-
37
- ## 配置
38
-
39
- - `监控接口地址`:监控 API 地址,默认 `https://aihot.news/api/v1/codex-resets`。
40
- - `监控间隔`:轮询间隔,单位为秒,默认 300 秒。
41
- - `请求超时时间`:单次 API 请求超时时间,单位为毫秒,默认 15000 毫秒。
42
- - `推送目标`:推送目标表,填写“平台”和“频道或群组 ID”;平台留空会按频道尝试在线 Bot,同一平台/频道只发送一次。
43
- - `推送重置预告`:是否推送预告。
44
- - `推送已确认重置`:是否推送已确认的重置/重置卡事件。
45
- - `艾特全体`:推送通知时是否艾特全体,默认关闭;需要目标平台支持全体艾特。
46
-
47
- API 是只读公开接口,不需要 API Key。插件使用 `ETag` 条件请求,并保留来源链接。请遵守 [AIHOT 公开使用规则](https://aihot.news/terms):公开可访问不等于自动获得对外商业使用、镜像或转售授权。
48
-
49
- ## 开发
50
-
51
- ```bash
52
- npm install
53
- npm test
54
- ```
30
+
31
+ ## 安装
32
+
33
+ ```bash
34
+ npm install koishi-plugin-codex-reset
35
+ ```
36
+
37
+ 如果需要概率卡片、最近重置卡片、日历或历史时间线图片,再安装并启用 `koishi-plugin-puppeteer`。不安装 Puppeteer 时,监控、最近重置和概率分析仍然可以使用,概率与最近重置命令回退为纯文本,日历和历史命令会给出提示。
38
+
39
+ ## 配置
40
+
41
+ - `监控接口地址`:监控 API 地址,默认 `https://aihot.news/api/v1/codex-resets`。
42
+ - `监控间隔`:轮询间隔,单位为秒,默认 300 秒。
43
+ - `请求超时时间`:单次 API 请求超时时间,单位为毫秒,默认 15000 毫秒。
44
+ - `推送目标`:推送目标表,填写“平台”和“频道或群组 ID”;平台留空会按频道尝试在线 Bot,同一平台/频道只发送一次。
45
+ - `推送重置预告`:是否推送预告。
46
+ - `推送已确认重置`:是否推送已确认的重置/重置卡事件。
47
+ - `艾特全体`:推送通知时是否艾特全体,默认关闭;需要目标平台支持全体艾特。
48
+
49
+ API 是只读公开接口,不需要 API Key。插件使用 `ETag` 条件请求,并保留来源链接。请遵守 [AIHOT 公开使用规则](https://aihot.news/terms):公开可访问不等于自动获得对外商业使用、镜像或转售授权。
50
+
51
+ ## 开发
52
+
53
+ ```bash
54
+ npm install
55
+ npm test
56
+ ```
package/lib/calendar.js CHANGED
@@ -45,7 +45,8 @@ function parseDateKey(value) {
45
45
  function timestampDateKey(value) {
46
46
  if (!value)
47
47
  return null;
48
- return value.match(/^(\d{4}-\d{2}-\d{2})/)?.[1] ?? null;
48
+ const date = new Date(value);
49
+ return Number.isFinite(date.getTime()) ? beijingDateKey(date) : null;
49
50
  }
50
51
  function beijingDateKey(now) {
51
52
  const parts = new Intl.DateTimeFormat('en-US', {
@@ -69,13 +70,10 @@ function escapeHtml(value) {
69
70
  .replaceAll("'", ''');
70
71
  }
71
72
  function getEventDateRange(event) {
72
- if (event.status === 'confirmed' && event.occurredOn)
73
- return [event.occurredOn, event.occurredOn];
74
- const from = timestampDateKey(event.schedule?.from) ?? timestampDateKey(event.confirmedAt) ?? timestampDateKey(event.createdAt);
75
- if (!from)
73
+ if (event.status !== 'confirmed')
76
74
  return null;
77
- const through = timestampDateKey(event.schedule?.through) ?? from;
78
- return [from, through];
75
+ const date = event.occurredOn ?? timestampDateKey(event.confirmedAt);
76
+ return date ? [date, date] : null;
79
77
  }
80
78
  function buildMonthMarks(events, start) {
81
79
  const end = monthStart(start, 1);
@@ -133,10 +131,10 @@ function renderDayCell(date, month, marks, selectedDate, today) {
133
131
  const className = mark.type === 'direct_reset' ? 'reset-mark' : 'credit-mark';
134
132
  return `<span class="${className}">${label}</span>`;
135
133
  }).join('');
136
- return `<div class="${classes.join(' ')}" aria-label="${escapeHtml(labels)}">
137
- <span class="number">${date.getUTCDate()}</span>
138
- ${key === today ? '<span class="today-dot"></span>' : ''}
139
- <div class="marks">${marksHtml}</div>
134
+ return `<div class="${classes.join(' ')}" aria-label="${escapeHtml(labels)}">
135
+ <span class="number">${date.getUTCDate()}</span>
136
+ ${key === today ? '<span class="today-dot"></span>' : ''}
137
+ <div class="marks">${marksHtml}</div>
140
138
  </div>`;
141
139
  }
142
140
  function renderCalendarGrid(month, marks, selectedDate, today) {
@@ -153,19 +151,17 @@ function renderCalendarGrid(month, marks, selectedDate, today) {
153
151
  }
154
152
  function renderPostCard(event, post) {
155
153
  const text = post.text || post.originalText;
156
- const schedule = event.schedule?.label ? `<div class="schedule">原预告:${escapeHtml(event.schedule.label)}</div>` : '';
157
154
  const avatar = TIBO_AVATAR
158
155
  ? `<img class="avatar-image" src="${TIBO_AVATAR}" alt="">`
159
156
  : '<span class="avatar-fallback">T</span>';
160
- return `<article class="post-card">
161
- <div class="post-head">
162
- <div class="avatar">${avatar}</div>
163
- <div class="author"><strong>Tibo</strong><span>@thsottiaux</span></div>
164
- <time>${escapeHtml(formatPublishedAt(post.publishedAt))}</time>
165
- </div>
166
- <p class="post-text">${escapeHtml(text)}</p>
167
- ${schedule}
168
- <div class="post-foot">${escapeHtml(post.stage)}</div>
157
+ return `<article class="post-card">
158
+ <div class="post-head">
159
+ <div class="avatar">${avatar}</div>
160
+ <div class="author"><strong>Tibo</strong><span>@thsottiaux</span></div>
161
+ <time>${escapeHtml(formatPublishedAt(post.publishedAt))}</time>
162
+ </div>
163
+ <p class="post-text">${escapeHtml(text)}</p>
164
+ <div class="post-foot">${escapeHtml(post.stage)}</div>
169
165
  </article>`;
170
166
  }
171
167
  function renderDetails(events) {
@@ -176,19 +172,23 @@ function renderDetails(events) {
176
172
  const groups = latestPosts.map(({ event, post }) => {
177
173
  const status = event.status === 'confirmed' ? '已重置' : '未重置预告';
178
174
  const statusClass = event.status === 'confirmed' ? 'confirmed-state' : 'announced-state';
179
- return `<section class="event-group">
180
- <div class="event-state ${statusClass}"><span class="state-icon">${event.status === 'confirmed' ? '✓' : '◷'}</span>${status}</div>
181
- <h2>${escapeHtml(event.title)}</h2>
182
- ${renderPostCard(event, post)}
175
+ return `<section class="event-group">
176
+ <div class="event-state ${statusClass}"><span class="state-icon">${event.status === 'confirmed' ? '✓' : '◷'}</span>${status}</div>
177
+ <h2>${escapeHtml(event.title)}</h2>
178
+ ${renderPostCard(event, post)}
183
179
  </section>`;
184
180
  }).join('');
185
- return `<aside class="details-pane">
186
- <div class="details-header"><h2>最近 2 条动态</h2><span>北京时间</span></div>
187
- <div class="details-body">${groups || '<div class="empty-state">暂无来源帖子内容</div>'}</div>
181
+ return `<aside class="details-pane">
182
+ <div class="details-header"><h2>最近 2 条动态</h2><span>北京时间</span></div>
183
+ <div class="details-body">${groups || '<div class="empty-state">暂无来源帖子内容</div>'}</div>
188
184
  </aside>`;
189
185
  }
190
186
  function renderCalendarHtml(events, now = new Date()) {
191
187
  const today = beijingDateKey(now);
188
+ events = events.filter((event) => {
189
+ const range = getEventDateRange(event);
190
+ return range !== null && parseDateKey(range[0]) !== null && range[0] <= today;
191
+ });
192
192
  const todayDate = parseDateKey(today) ?? new Date();
193
193
  const month = monthStart(todayDate);
194
194
  const marks = buildMonthMarks(events, month);
@@ -199,81 +199,81 @@ function renderCalendarHtml(events, now = new Date()) {
199
199
  const resetCount = monthEvents.filter((event) => event.type === 'direct_reset').length;
200
200
  const creditCount = monthEvents.filter((event) => event.type === 'reset_credit').length;
201
201
  const monthLabel = `${month.getUTCFullYear()} 年 ${MONTH_NAMES[month.getUTCMonth()]}`;
202
- return `<!doctype html>
203
- <html lang="zh-CN">
204
- <head>
205
- <meta charset="utf-8">
206
- <style>
207
- * { box-sizing: border-box; }
208
- body { margin: 0; padding: 16px; background: #f5f6f4; color: #25313a; font-family: Inter, "Microsoft YaHei", "Noto Sans CJK SC", sans-serif; }
209
- .calendar-card { width: 1248px; min-height: 578px; display: grid; grid-template-columns: 1.46fr 1fr; overflow: hidden; border: 1px solid #d2dadd; border-radius: 14px; background: #fff; }
210
- .calendar-pane { padding: 30px 24px 22px; }
211
- .calendar-header { display: flex; justify-content: space-between; align-items: flex-start; padding: 0 0 18px; }
212
- .calendar-header h1 { margin: 0; font-size: 23px; line-height: 1.3; letter-spacing: .2px; }
213
- .month-summary { margin-top: 20px; color: #617079; font-size: 12px; }
214
- .calendar-nav { display: flex; align-items: center; gap: 22px; padding-top: 3px; color: #617079; font-size: 11px; }
215
- .calendar-nav .arrow { color: #617079; font-size: 20px; font-weight: 300; line-height: 1; }
216
- .weekday-row, .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
217
- .weekday-row { margin-bottom: 6px; color: #65737b; font-size: 12px; text-align: center; }
218
- .weekday-row span { padding: 8px 0; }
219
- .calendar-grid { grid-auto-rows: 70px; }
220
- .day { position: relative; min-width: 0; padding: 11px 12px 7px; border: 1px solid transparent; border-radius: 10px; background: #fafafa; }
221
- .day.outside { color: #adb6b9; background: #fcfcfc; }
222
- .day.selected { border-color: #147487; background: #edf7f6; box-shadow: inset 0 0 0 1px rgba(20, 116, 135, .12); }
223
- .day.has-reset { background: #eef6f2; }
224
- .day.has-credit { background: #f7f3ec; }
225
- .day.selected.has-reset, .day.selected.has-credit { background: #e8f3f2; }
226
- .number { display: inline-block; font-size: 14px; line-height: 1; }
227
- .outside .number { color: #aeb7ba; }
228
- .today-dot { position: absolute; top: 11px; right: 13px; width: 4px; height: 4px; border-radius: 50%; background: #147487; }
229
- .marks { display: flex; flex-direction: column; gap: 2px; margin-top: 12px; overflow: hidden; }
230
- .marks span { overflow: hidden; color: #347465; font-size: 9px; line-height: 1.2; text-overflow: ellipsis; white-space: nowrap; }
231
- .marks .credit-mark { color: #967044; }
232
- .outside .marks span { color: #b6d0c9; }
233
- .outside .marks .credit-mark { color: #cbbda6; }
234
- .calendar-note { margin-top: 18px; color: #617079; font-size: 11px; }
235
- .details-pane { min-width: 0; padding: 26px 24px 20px; border-left: 1px solid #d9dfe0; }
236
- .details-header { display: flex; justify-content: space-between; align-items: center; padding: 0 0 17px; border-bottom: 1px solid #d9dfe0; }
237
- .details-header h2 { margin: 0; font-size: 18px; }
238
- .details-header span { color: #65737b; font-size: 11px; }
239
- .details-body { padding-top: 18px; }
240
- .event-group + .event-group { margin-top: 22px; }
241
- .event-state { display: flex; align-items: center; gap: 7px; margin-bottom: 12px; font-size: 11px; font-weight: 700; }
242
- .confirmed-state { color: #29735f; }
243
- .announced-state { color: #98713d; }
244
- .state-icon { font-size: 16px; font-weight: 500; }
245
- .event-group h2 { margin: 0 0 16px; font-size: 18px; line-height: 1.35; }
246
- .post-card { margin-bottom: 12px; padding: 16px; border: 1px solid #d2dadd; border-radius: 13px; background: #fff; }
247
- .post-head { display: flex; align-items: center; min-width: 0; }
248
- .avatar { display: grid; flex: 0 0 auto; width: 36px; height: 36px; place-items: center; overflow: hidden; border-radius: 50%; background: linear-gradient(145deg, #a57954, #d3aa7d); color: #fff; font-size: 18px; font-weight: 700; }
249
- .avatar-image { display: block; width: 100%; height: 100%; object-fit: cover; }
250
- .avatar-fallback { display: block; }
251
- .author { display: flex; flex-direction: column; margin-left: 9px; line-height: 1.25; }
252
- .author strong { font-size: 13px; }
253
- .author span { color: #6f7d83; font-size: 10px; }
254
- .post-head time { margin-left: auto; color: #6f7d83; font-size: 10px; }
255
- .post-text { margin: 14px 0 10px; color: #27343c; font-size: 14px; line-height: 1.65; }
256
- .schedule { margin: 0 0 10px; color: #08768b; font-size: 11px; }
257
- .post-foot { color: #6f7d83; font-size: 10px; }
258
- .empty-state, .empty-post { color: #7a888d; font-size: 13px; text-align: center; }
259
- .empty-state { padding: 56px 0; }
260
- .empty-post { padding: 24px 12px; }
261
- </style>
262
- </head>
263
- <body>
264
- <main class="calendar-card">
265
- <section class="calendar-pane">
266
- <header class="calendar-header">
267
- <div><h1>${monthLabel}</h1><div class="month-summary">本月:${resetCount} 条全员重置&nbsp;&nbsp;·&nbsp;&nbsp;${creditCount} 张重置卡</div></div>
268
- <div class="calendar-nav"><span>回到最近</span><span class="arrow">‹</span><span class="arrow">›</span></div>
269
- </header>
270
- <div class="weekday-row">${WEEK_NAMES.map((name) => `<span>${name}</span>`).join('')}</div>
271
- <div class="calendar-grid">${renderCalendarGrid(month, marks, selectedDate, today)}</div>
272
- <div class="calendar-note">按北京时间记录;发放日期不明确的事件,按来源帖子日期展示。</div>
273
- </section>
274
- ${renderDetails(events)}
275
- </main>
276
- </body>
202
+ return `<!doctype html>
203
+ <html lang="zh-CN">
204
+ <head>
205
+ <meta charset="utf-8">
206
+ <style>
207
+ * { box-sizing: border-box; }
208
+ body { margin: 0; padding: 16px; background: #f5f6f4; color: #25313a; font-family: Inter, "Microsoft YaHei", "Noto Sans CJK SC", sans-serif; }
209
+ .calendar-card { width: 1248px; min-height: 578px; display: grid; grid-template-columns: 1.46fr 1fr; overflow: hidden; border: 1px solid #d2dadd; border-radius: 14px; background: #fff; }
210
+ .calendar-pane { padding: 30px 24px 22px; }
211
+ .calendar-header { display: flex; justify-content: space-between; align-items: flex-start; padding: 0 0 18px; }
212
+ .calendar-header h1 { margin: 0; font-size: 23px; line-height: 1.3; letter-spacing: .2px; }
213
+ .month-summary { margin-top: 20px; color: #617079; font-size: 12px; }
214
+ .calendar-nav { display: flex; align-items: center; gap: 22px; padding-top: 3px; color: #617079; font-size: 11px; }
215
+ .calendar-nav .arrow { color: #617079; font-size: 20px; font-weight: 300; line-height: 1; }
216
+ .weekday-row, .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
217
+ .weekday-row { margin-bottom: 6px; color: #65737b; font-size: 12px; text-align: center; }
218
+ .weekday-row span { padding: 8px 0; }
219
+ .calendar-grid { grid-auto-rows: 70px; }
220
+ .day { position: relative; min-width: 0; padding: 11px 12px 7px; border: 1px solid transparent; border-radius: 10px; background: #fafafa; }
221
+ .day.outside { color: #adb6b9; background: #fcfcfc; }
222
+ .day.selected { border-color: #147487; background: #edf7f6; box-shadow: inset 0 0 0 1px rgba(20, 116, 135, .12); }
223
+ .day.has-reset { background: #eef6f2; }
224
+ .day.has-credit { background: #f7f3ec; }
225
+ .day.selected.has-reset, .day.selected.has-credit { background: #e8f3f2; }
226
+ .number { display: inline-block; font-size: 14px; line-height: 1; }
227
+ .outside .number { color: #aeb7ba; }
228
+ .today-dot { position: absolute; top: 11px; right: 13px; width: 4px; height: 4px; border-radius: 50%; background: #147487; }
229
+ .marks { display: flex; flex-direction: column; gap: 2px; margin-top: 12px; overflow: hidden; }
230
+ .marks span { overflow: hidden; color: #347465; font-size: 9px; line-height: 1.2; text-overflow: ellipsis; white-space: nowrap; }
231
+ .marks .credit-mark { color: #967044; }
232
+ .outside .marks span { color: #b6d0c9; }
233
+ .outside .marks .credit-mark { color: #cbbda6; }
234
+ .calendar-note { margin-top: 18px; color: #617079; font-size: 11px; }
235
+ .details-pane { min-width: 0; padding: 26px 24px 20px; border-left: 1px solid #d9dfe0; }
236
+ .details-header { display: flex; justify-content: space-between; align-items: center; padding: 0 0 17px; border-bottom: 1px solid #d9dfe0; }
237
+ .details-header h2 { margin: 0; font-size: 18px; }
238
+ .details-header span { color: #65737b; font-size: 11px; }
239
+ .details-body { padding-top: 18px; }
240
+ .event-group + .event-group { margin-top: 22px; }
241
+ .event-state { display: flex; align-items: center; gap: 7px; margin-bottom: 12px; font-size: 11px; font-weight: 700; }
242
+ .confirmed-state { color: #29735f; }
243
+ .announced-state { color: #98713d; }
244
+ .state-icon { font-size: 16px; font-weight: 500; }
245
+ .event-group h2 { margin: 0 0 16px; font-size: 18px; line-height: 1.35; }
246
+ .post-card { margin-bottom: 12px; padding: 16px; border: 1px solid #d2dadd; border-radius: 13px; background: #fff; }
247
+ .post-head { display: flex; align-items: center; min-width: 0; }
248
+ .avatar { display: grid; flex: 0 0 auto; width: 36px; height: 36px; place-items: center; overflow: hidden; border-radius: 50%; background: linear-gradient(145deg, #a57954, #d3aa7d); color: #fff; font-size: 18px; font-weight: 700; }
249
+ .avatar-image { display: block; width: 100%; height: 100%; object-fit: cover; }
250
+ .avatar-fallback { display: block; }
251
+ .author { display: flex; flex-direction: column; margin-left: 9px; line-height: 1.25; }
252
+ .author strong { font-size: 13px; }
253
+ .author span { color: #6f7d83; font-size: 10px; }
254
+ .post-head time { margin-left: auto; color: #6f7d83; font-size: 10px; }
255
+ .post-text { margin: 14px 0 10px; color: #27343c; font-size: 14px; line-height: 1.65; }
256
+ .schedule { margin: 0 0 10px; color: #08768b; font-size: 11px; }
257
+ .post-foot { color: #6f7d83; font-size: 10px; }
258
+ .empty-state, .empty-post { color: #7a888d; font-size: 13px; text-align: center; }
259
+ .empty-state { padding: 56px 0; }
260
+ .empty-post { padding: 24px 12px; }
261
+ </style>
262
+ </head>
263
+ <body>
264
+ <main class="calendar-card">
265
+ <section class="calendar-pane">
266
+ <header class="calendar-header">
267
+ <div><h1>${monthLabel}</h1><div class="month-summary">本月:${resetCount} 条全员重置&nbsp;&nbsp;·&nbsp;&nbsp;${creditCount} 张重置卡</div></div>
268
+ <div class="calendar-nav"><span>回到最近</span><span class="arrow">‹</span><span class="arrow">›</span></div>
269
+ </header>
270
+ <div class="weekday-row">${WEEK_NAMES.map((name) => `<span>${name}</span>`).join('')}</div>
271
+ <div class="calendar-grid">${renderCalendarGrid(month, marks, selectedDate, today)}</div>
272
+ <div class="calendar-note">仅显示已确认事件;按北京时间记录实际发生日期,缺失时按确认日期展示;不含预告。</div>
273
+ </section>
274
+ ${renderDetails(events)}
275
+ </main>
276
+ </body>
277
277
  </html>`;
278
278
  }
279
279
  async function renderCalendarImage(browser, events, now = new Date()) {
package/lib/event.js CHANGED
@@ -47,86 +47,86 @@ function renderSourcePost(summary) {
47
47
  const text = post.text || post.originalText;
48
48
  const publishedAt = Date.parse(post.publishedAt);
49
49
  const time = Number.isFinite(publishedAt) ? formatDateTime(publishedAt) : '时间未知';
50
- return `<article class="post-card">
51
- <div class="post-head">
52
- <div class="avatar">${avatar}</div>
53
- <div class="author"><strong>Tibo</strong><span>@thsottiaux</span></div>
54
- <time>${escapeHtml(time)} 发布</time>
55
- </div>
56
- <p class="post-text">${escapeHtml(text)}</p>
57
- <div class="post-foot">${escapeHtml(post.stage)}</div>
50
+ return `<article class="post-card">
51
+ <div class="post-head">
52
+ <div class="avatar">${avatar}</div>
53
+ <div class="author"><strong>Tibo</strong><span>@thsottiaux</span></div>
54
+ <time>${escapeHtml(time)} 发布</time>
55
+ </div>
56
+ <p class="post-text">${escapeHtml(text)}</p>
57
+ <div class="post-foot">${escapeHtml(post.stage)}</div>
58
58
  </article>`;
59
59
  }
60
60
  function renderLatestResetHtml(summary, now = new Date()) {
61
61
  const approximate = summary.approximate ? '时间按公开日期估算' : '已记录精确确认时间';
62
62
  const event = summary.event;
63
- return `<!doctype html>
64
- <html lang="zh-CN">
65
- <head>
66
- <meta charset="utf-8">
67
- <style>
68
- * { box-sizing: border-box; }
69
- html, body { margin: 0; padding: 0; background: #f4f6f5; color: #27343c; font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif; }
70
- body { padding: 24px; }
71
- .card { position: relative; display: grid; grid-template-columns: .92fr 1.08fr; width: 1120px; min-height: 520px; overflow: hidden; border: 1px solid #d2dadd; border-radius: 16px; background: #fff; box-shadow: 0 16px 38px rgba(42, 58, 64, .10); }
72
- .card::before { content: ""; position: absolute; inset: 0; pointer-events: none; background: radial-gradient(circle at 7% 12%, rgba(8, 118, 139, .11), transparent 34%), radial-gradient(circle at 95% 96%, rgba(184, 134, 59, .10), transparent 34%); }
73
- .overview, .source { position: relative; z-index: 1; padding: 38px 40px; }
74
- .overview { display: flex; flex-direction: column; border-right: 1px solid #d9dfe0; background: linear-gradient(145deg, rgba(237, 247, 246, .8), rgba(255, 255, 255, .96) 64%); }
75
- .source { background: linear-gradient(160deg, rgba(250, 246, 238, .68), rgba(255, 255, 255, .98) 55%); }
76
- .eyebrow { color: #08768b; font-size: 11px; font-weight: 700; letter-spacing: .18em; }
77
- .source .eyebrow { color: #a27831; }
78
- h1 { margin: 18px 0 7px; color: #27343c; font-size: 31px; letter-spacing: -.04em; }
79
- .subtitle { margin: 0; color: #6f7d83; font-size: 13px; line-height: 1.6; }
80
- .elapsed { margin-top: 40px; color: #08768b; font-family: Georgia, "Times New Roman", serif; font-size: 54px; font-weight: 700; letter-spacing: -.06em; line-height: 1; }
81
- .elapsed-label { margin-top: 8px; color: #617079; font-size: 12px; }
82
- .facts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: auto; padding-top: 32px; }
83
- .fact { padding: 13px 14px; border: 1px solid #c8dedd; border-radius: 11px; background: rgba(255, 255, 255, .72); }
84
- .fact-label { color: #6f7d83; font-size: 10px; }
85
- .fact-value { margin-top: 5px; color: #27343c; font-size: 14px; font-weight: 700; }
86
- .note { margin-top: 16px; color: #7a888d; font-size: 10px; line-height: 1.6; }
87
- .source-header { display: flex; align-items: flex-start; justify-content: space-between; padding-bottom: 17px; border-bottom: 1px solid #e0e5e6; }
88
- .source-header h2 { margin: 0; color: #27343c; font-size: 19px; }
89
- .source-header span { color: #6f7d83; font-size: 10px; }
90
- .event-title { margin: 25px 0 19px; color: #27343c; font-size: 22px; line-height: 1.4; }
91
- .post-card { padding: 18px; border: 1px solid #d2dadd; border-radius: 13px; background: rgba(255, 255, 255, .86); }
92
- .post-head { display: flex; align-items: center; min-width: 0; }
93
- .avatar { display: grid; flex: 0 0 auto; width: 42px; height: 42px; place-items: center; overflow: hidden; border-radius: 50%; background: linear-gradient(145deg, #a57954, #d3aa7d); color: #fff; font-size: 19px; font-weight: 700; }
94
- .avatar-image { display: block; width: 100%; height: 100%; object-fit: cover; }
95
- .avatar-fallback { display: block; }
96
- .author { display: flex; flex-direction: column; margin-left: 10px; line-height: 1.3; }
97
- .author strong { font-size: 14px; }
98
- .author span { color: #6f7d83; font-size: 10px; }
99
- .post-head time { margin-left: auto; color: #6f7d83; font-size: 10px; }
100
- .post-text { display: -webkit-box; margin: 19px 0 15px; overflow: hidden; color: #27343c; font-size: 15px; line-height: 1.7; -webkit-box-orient: vertical; -webkit-line-clamp: 5; }
101
- .post-foot { color: #6f7d83; font-size: 10px; }
102
- .post-empty { padding: 45px 20px; border: 1px dashed #d2dadd; border-radius: 13px; color: #7a888d; font-size: 13px; text-align: center; }
103
- .source-note { margin-top: 17px; color: #7a888d; font-size: 10px; line-height: 1.6; }
104
- .mark { margin-top: 34px; color: #a27831; font-size: 10px; font-weight: 700; letter-spacing: .12em; }
105
- </style>
106
- </head>
107
- <body>
108
- <main class="card">
109
- <section class="overview">
110
- <div class="eyebrow">CODEX RESET / LATEST</div>
111
- <h1>最近重置</h1>
112
- <p class="subtitle">AIHOT 公开快照中的最近一次已确认事件</p>
113
- <div class="elapsed">${escapeHtml(formatElapsed(summary.timestamp, now))}</div>
114
- <div class="elapsed-label">距离这次重置已经过去</div>
115
- <div class="facts">
116
- <div class="fact"><div class="fact-label">发生时间</div><div class="fact-value">${escapeHtml(formatDateTime(summary.timestamp))}</div></div>
117
- <div class="fact"><div class="fact-label">事件类型</div><div class="fact-value">${typeLabel(summary)}</div></div>
118
- </div>
119
- <p class="note">${escapeHtml(approximate)}。数据来自 AIHOT 当前历史快照,不代表你的账户一定获得重置。</p>
120
- </section>
121
- <section class="source">
122
- <header class="source-header"><h2>事件来源</h2><span>北京时间</span></header>
123
- <h2 class="event-title">${escapeHtml(event.title)}</h2>
124
- ${renderSourcePost(summary)}
125
- <p class="source-note">${escapeHtml(event.scope ? `范围:${event.scope}` : '公开来源帖子记录')}</p>
126
- <div class="mark">RESET MONITOR</div>
127
- </section>
128
- </main>
129
- </body>
63
+ return `<!doctype html>
64
+ <html lang="zh-CN">
65
+ <head>
66
+ <meta charset="utf-8">
67
+ <style>
68
+ * { box-sizing: border-box; }
69
+ html, body { margin: 0; padding: 0; background: #f4f6f5; color: #27343c; font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif; }
70
+ body { padding: 24px; }
71
+ .card { position: relative; display: grid; grid-template-columns: .92fr 1.08fr; width: 1120px; min-height: 520px; overflow: hidden; border: 1px solid #d2dadd; border-radius: 16px; background: #fff; box-shadow: 0 16px 38px rgba(42, 58, 64, .10); }
72
+ .card::before { content: ""; position: absolute; inset: 0; pointer-events: none; background: radial-gradient(circle at 7% 12%, rgba(8, 118, 139, .11), transparent 34%), radial-gradient(circle at 95% 96%, rgba(184, 134, 59, .10), transparent 34%); }
73
+ .overview, .source { position: relative; z-index: 1; padding: 38px 40px; }
74
+ .overview { display: flex; flex-direction: column; border-right: 1px solid #d9dfe0; background: linear-gradient(145deg, rgba(237, 247, 246, .8), rgba(255, 255, 255, .96) 64%); }
75
+ .source { background: linear-gradient(160deg, rgba(250, 246, 238, .68), rgba(255, 255, 255, .98) 55%); }
76
+ .eyebrow { color: #08768b; font-size: 11px; font-weight: 700; letter-spacing: .18em; }
77
+ .source .eyebrow { color: #a27831; }
78
+ h1 { margin: 18px 0 7px; color: #27343c; font-size: 31px; letter-spacing: -.04em; }
79
+ .subtitle { margin: 0; color: #6f7d83; font-size: 13px; line-height: 1.6; }
80
+ .elapsed { margin-top: 40px; color: #08768b; font-family: Georgia, "Times New Roman", serif; font-size: 54px; font-weight: 700; letter-spacing: -.06em; line-height: 1; }
81
+ .elapsed-label { margin-top: 8px; color: #617079; font-size: 12px; }
82
+ .facts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: auto; padding-top: 32px; }
83
+ .fact { padding: 13px 14px; border: 1px solid #c8dedd; border-radius: 11px; background: rgba(255, 255, 255, .72); }
84
+ .fact-label { color: #6f7d83; font-size: 10px; }
85
+ .fact-value { margin-top: 5px; color: #27343c; font-size: 14px; font-weight: 700; }
86
+ .note { margin-top: 16px; color: #7a888d; font-size: 10px; line-height: 1.6; }
87
+ .source-header { display: flex; align-items: flex-start; justify-content: space-between; padding-bottom: 17px; border-bottom: 1px solid #e0e5e6; }
88
+ .source-header h2 { margin: 0; color: #27343c; font-size: 19px; }
89
+ .source-header span { color: #6f7d83; font-size: 10px; }
90
+ .event-title { margin: 25px 0 19px; color: #27343c; font-size: 22px; line-height: 1.4; }
91
+ .post-card { padding: 18px; border: 1px solid #d2dadd; border-radius: 13px; background: rgba(255, 255, 255, .86); }
92
+ .post-head { display: flex; align-items: center; min-width: 0; }
93
+ .avatar { display: grid; flex: 0 0 auto; width: 42px; height: 42px; place-items: center; overflow: hidden; border-radius: 50%; background: linear-gradient(145deg, #a57954, #d3aa7d); color: #fff; font-size: 19px; font-weight: 700; }
94
+ .avatar-image { display: block; width: 100%; height: 100%; object-fit: cover; }
95
+ .avatar-fallback { display: block; }
96
+ .author { display: flex; flex-direction: column; margin-left: 10px; line-height: 1.3; }
97
+ .author strong { font-size: 14px; }
98
+ .author span { color: #6f7d83; font-size: 10px; }
99
+ .post-head time { margin-left: auto; color: #6f7d83; font-size: 10px; }
100
+ .post-text { display: -webkit-box; margin: 19px 0 15px; overflow: hidden; color: #27343c; font-size: 15px; line-height: 1.7; -webkit-box-orient: vertical; -webkit-line-clamp: 5; }
101
+ .post-foot { color: #6f7d83; font-size: 10px; }
102
+ .post-empty { padding: 45px 20px; border: 1px dashed #d2dadd; border-radius: 13px; color: #7a888d; font-size: 13px; text-align: center; }
103
+ .source-note { margin-top: 17px; color: #7a888d; font-size: 10px; line-height: 1.6; }
104
+ .mark { margin-top: 34px; color: #a27831; font-size: 10px; font-weight: 700; letter-spacing: .12em; }
105
+ </style>
106
+ </head>
107
+ <body>
108
+ <main class="card">
109
+ <section class="overview">
110
+ <div class="eyebrow">CODEX RESET / LATEST</div>
111
+ <h1>最近重置</h1>
112
+ <p class="subtitle">AIHOT 公开快照中的最近一次已确认事件</p>
113
+ <div class="elapsed">${escapeHtml(formatElapsed(summary.timestamp, now))}</div>
114
+ <div class="elapsed-label">距离这次重置已经过去</div>
115
+ <div class="facts">
116
+ <div class="fact"><div class="fact-label">发生时间</div><div class="fact-value">${escapeHtml(formatDateTime(summary.timestamp))}</div></div>
117
+ <div class="fact"><div class="fact-label">事件类型</div><div class="fact-value">${typeLabel(summary)}</div></div>
118
+ </div>
119
+ <p class="note">${escapeHtml(approximate)}。数据来自 AIHOT 当前历史快照,不代表你的账户一定获得重置。</p>
120
+ </section>
121
+ <section class="source">
122
+ <header class="source-header"><h2>事件来源</h2><span>北京时间</span></header>
123
+ <h2 class="event-title">${escapeHtml(event.title)}</h2>
124
+ ${renderSourcePost(summary)}
125
+ <p class="source-note">${escapeHtml(event.scope ? `范围:${event.scope}` : '公开来源帖子记录')}</p>
126
+ <div class="mark">RESET MONITOR</div>
127
+ </section>
128
+ </main>
129
+ </body>
130
130
  </html>`;
131
131
  }
132
132
  async function renderLatestResetImage(browser, summary, now = new Date()) {
package/lib/history.js CHANGED
@@ -93,28 +93,28 @@ function renderPostPreview(event) {
93
93
  const avatar = (0, calendar_1.getTiboAvatarDataUri)()
94
94
  ? `<img class="avatar-image" src="${(0, calendar_1.getTiboAvatarDataUri)()}" alt="">`
95
95
  : '<span class="avatar-fallback">T</span>';
96
- return `<div class="post-preview">
97
- <div class="post-head">
98
- <div class="avatar">${avatar}</div>
99
- <div class="author"><strong>Tibo</strong><span>@thsottiaux</span></div>
100
- <time>${escapeHtml(timeLabel(parseTimestamp(post.publishedAt) ?? eventTimestamp(event)))} 发布</time>
101
- </div>
102
- <p>${escapeHtml(text)}</p>
96
+ return `<div class="post-preview">
97
+ <div class="post-head">
98
+ <div class="avatar">${avatar}</div>
99
+ <div class="author"><strong>Tibo</strong><span>@thsottiaux</span></div>
100
+ <time>${escapeHtml(timeLabel(parseTimestamp(post.publishedAt) ?? eventTimestamp(event)))} 发布</time>
101
+ </div>
102
+ <p>${escapeHtml(text)}</p>
103
103
  </div>`;
104
104
  }
105
105
  function renderTimelineItem(event) {
106
106
  const timestamp = eventTimestamp(event);
107
107
  const statusClass = event.status === 'confirmed' ? 'confirmed' : 'announced';
108
108
  const schedule = event.schedule?.label ? `<div class="schedule">时间:${escapeHtml(event.schedule.label)}</div>` : '';
109
- return `<article class="timeline-item">
110
- <div class="timeline-date"><strong>${escapeHtml(dateLabel(timestamp))}</strong><span>${escapeHtml(timeLabel(timestamp))}</span></div>
111
- <div class="timeline-dot ${statusClass}"></div>
112
- <div class="event-card">
113
- <div class="event-meta"><span class="type-label">${typeLabel(event)}</span><span class="status ${statusClass}">${statusLabel(event)}</span></div>
114
- <h2>${escapeHtml(event.title)}</h2>
115
- ${schedule}
116
- ${renderPostPreview(event)}
117
- </div>
109
+ return `<article class="timeline-item">
110
+ <div class="timeline-date"><strong>${escapeHtml(dateLabel(timestamp))}</strong><span>${escapeHtml(timeLabel(timestamp))}</span></div>
111
+ <div class="timeline-dot ${statusClass}"></div>
112
+ <div class="event-card">
113
+ <div class="event-meta"><span class="type-label">${typeLabel(event)}</span><span class="status ${statusClass}">${statusLabel(event)}</span></div>
114
+ <h2>${escapeHtml(event.title)}</h2>
115
+ ${schedule}
116
+ ${renderPostPreview(event)}
117
+ </div>
118
118
  </article>`;
119
119
  }
120
120
  function renderHistoryHtml(events, now = new Date()) {
@@ -126,81 +126,81 @@ function renderHistoryHtml(events, now = new Date()) {
126
126
  const firstTimestamp = sorted.at(-1) ? eventTimestamp(sorted.at(-1)) : now.getTime();
127
127
  const lastTimestamp = sorted[0] ? eventTimestamp(sorted[0]) : now.getTime();
128
128
  const period = `${dateLabel(firstTimestamp)} 至 ${dateLabel(lastTimestamp)}`;
129
- return `<!doctype html>
130
- <html lang="zh-CN">
131
- <head>
132
- <meta charset="utf-8">
133
- <style>
134
- * { box-sizing: border-box; }
135
- html, body { margin: 0; padding: 0; background: #f4f6f5; color: #27343c; font-family: "Microsoft YaHei", "Noto Sans CJK SC", Arial, sans-serif; }
136
- body { padding: 24px; }
137
- .history-shell { max-width: 1280px; margin: 0 auto; padding: 28px 30px 34px; border: 1px solid #d2dadd; border-radius: 16px; background: #fff; }
138
- .page-header { display: flex; align-items: flex-start; justify-content: space-between; padding-bottom: 20px; border-bottom: 1px solid #e0e5e6; }
139
- .eyebrow { color: #08768b; font-size: 12px; letter-spacing: .08em; }
140
- h1 { margin: 7px 0 8px; font-size: 28px; line-height: 1.2; }
141
- .subtitle { margin: 0; color: #6f7d83; font-size: 13px; }
142
- .generated { color: #6f7d83; font-size: 11px; text-align: right; line-height: 1.6; }
143
- .layout { display: grid; grid-template-columns: minmax(0, 1fr) 286px; gap: 28px; padding-top: 26px; }
144
- .timeline { position: relative; }
145
- .timeline::before { content: ""; position: absolute; left: 112px; top: 16px; bottom: 16px; width: 1px; background: #cbd7d9; }
146
- .timeline-item { position: relative; display: grid; grid-template-columns: 94px 36px minmax(0, 1fr); gap: 0; margin-bottom: 18px; }
147
- .timeline-date { padding-top: 17px; padding-right: 14px; text-align: right; line-height: 1.5; }
148
- .timeline-date strong { display: block; color: #27343c; font-size: 11px; white-space: nowrap; }
149
- .timeline-date span { color: #89969a; font-size: 10px; }
150
- .timeline-dot { position: relative; z-index: 1; width: 12px; height: 12px; margin: 22px auto 0; border: 3px solid #fff; border-radius: 50%; background: #08768b; box-shadow: 0 0 0 1px #08768b; }
151
- .timeline-dot.announced { background: #b8863b; box-shadow: 0 0 0 1px #b8863b; }
152
- .event-card { padding: 15px 17px 16px; border: 1px solid #d2dadd; border-radius: 13px; background: #fff; }
153
- .event-meta { display: flex; gap: 7px; align-items: center; margin-bottom: 7px; }
154
- .type-label, .status { display: inline-block; padding: 3px 7px; border-radius: 999px; font-size: 10px; }
155
- .type-label { color: #08768b; background: #edf7f8; }
156
- .status.confirmed { color: #177255; background: #edf8f1; }
157
- .status.announced { color: #8b6426; background: #fbf4e6; }
158
- .event-card h2 { margin: 0 0 10px; font-size: 17px; line-height: 1.35; }
159
- .schedule { margin-bottom: 10px; color: #08768b; font-size: 11px; }
160
- .post-preview { padding: 11px 12px; border: 1px solid #e0e5e6; border-radius: 10px; background: #fbfcfc; }
161
- .post-preview.empty { color: #89969a; font-size: 12px; }
162
- .post-head { display: flex; align-items: center; min-width: 0; }
163
- .avatar { display: grid; flex: 0 0 auto; width: 28px; height: 28px; place-items: center; overflow: hidden; border-radius: 50%; background: linear-gradient(145deg, #a57954, #d3aa7d); color: #fff; font-size: 14px; font-weight: 700; }
164
- .avatar-image { display: block; width: 100%; height: 100%; object-fit: cover; }
165
- .avatar-fallback { display: block; }
166
- .author { display: flex; flex-direction: column; margin-left: 8px; line-height: 1.2; }
167
- .author strong { font-size: 11px; }
168
- .author span { color: #89969a; font-size: 9px; }
169
- .post-head time { margin-left: auto; color: #89969a; font-size: 10px; }
170
- .post-preview p { margin: 10px 0 0; color: #27343c; font-size: 12px; line-height: 1.55; }
171
- .summary { align-self: start; padding: 18px; border: 1px solid #d2dadd; border-radius: 13px; background: #fbfcfc; }
172
- .summary h2 { margin: 0 0 15px; font-size: 17px; }
173
- .summary-period { margin-bottom: 18px; color: #6f7d83; font-size: 11px; line-height: 1.5; }
174
- .stat { display: flex; align-items: baseline; justify-content: space-between; padding: 11px 0; border-top: 1px solid #e0e5e6; }
175
- .stat span { color: #6f7d83; font-size: 12px; }
176
- .stat strong { color: #27343c; font-size: 22px; }
177
- .summary-note { margin: 17px 0 0; color: #89969a; font-size: 10px; line-height: 1.6; }
178
- .empty { padding: 54px 0; color: #89969a; text-align: center; }
179
- </style>
180
- </head>
181
- <body>
182
- <main class="history-shell">
183
- <header class="page-header">
184
- <div><div class="eyebrow">CODEX RESET MONITOR</div><h1>Codex 重置历史</h1><p class="subtitle">按北京时间排列的公开重置事件时间线</p></div>
185
- <div class="generated">生成时间<br>${escapeHtml(formatGeneratedAt(now))}</div>
186
- </header>
187
- <div class="layout">
188
- <section class="timeline">
189
- ${sorted.length ? sorted.map(renderTimelineItem).join('') : '<div class="empty">暂无历史事件</div>'}
190
- </section>
191
- <aside class="summary">
192
- <h2>历史概览</h2>
193
- <div class="summary-period">统计区间<br>${escapeHtml(period)}</div>
194
- <div class="stat"><span>事件总数</span><strong>${events.length}</strong></div>
195
- <div class="stat"><span>已确认</span><strong>${confirmed.length}</strong></div>
196
- <div class="stat"><span>全员重置</span><strong>${directResetCount}</strong></div>
197
- <div class="stat"><span>重置卡</span><strong>${resetCreditCount}</strong></div>
198
- <div class="stat"><span>未重置预告</span><strong>${announcedCount}</strong></div>
199
- <p class="summary-note">数据来自 AIHOT 公开快照。时间线仅展示公开事件,不代表账户一定获得重置。</p>
200
- </aside>
201
- </div>
202
- </main>
203
- </body>
129
+ return `<!doctype html>
130
+ <html lang="zh-CN">
131
+ <head>
132
+ <meta charset="utf-8">
133
+ <style>
134
+ * { box-sizing: border-box; }
135
+ html, body { margin: 0; padding: 0; background: #f4f6f5; color: #27343c; font-family: "Microsoft YaHei", "Noto Sans CJK SC", Arial, sans-serif; }
136
+ body { padding: 24px; }
137
+ .history-shell { max-width: 1280px; margin: 0 auto; padding: 28px 30px 34px; border: 1px solid #d2dadd; border-radius: 16px; background: #fff; }
138
+ .page-header { display: flex; align-items: flex-start; justify-content: space-between; padding-bottom: 20px; border-bottom: 1px solid #e0e5e6; }
139
+ .eyebrow { color: #08768b; font-size: 12px; letter-spacing: .08em; }
140
+ h1 { margin: 7px 0 8px; font-size: 28px; line-height: 1.2; }
141
+ .subtitle { margin: 0; color: #6f7d83; font-size: 13px; }
142
+ .generated { color: #6f7d83; font-size: 11px; text-align: right; line-height: 1.6; }
143
+ .layout { display: grid; grid-template-columns: minmax(0, 1fr) 286px; gap: 28px; padding-top: 26px; }
144
+ .timeline { position: relative; }
145
+ .timeline::before { content: ""; position: absolute; left: 112px; top: 16px; bottom: 16px; width: 1px; background: #cbd7d9; }
146
+ .timeline-item { position: relative; display: grid; grid-template-columns: 94px 36px minmax(0, 1fr); gap: 0; margin-bottom: 18px; }
147
+ .timeline-date { padding-top: 17px; padding-right: 14px; text-align: right; line-height: 1.5; }
148
+ .timeline-date strong { display: block; color: #27343c; font-size: 11px; white-space: nowrap; }
149
+ .timeline-date span { color: #89969a; font-size: 10px; }
150
+ .timeline-dot { position: relative; z-index: 1; width: 12px; height: 12px; margin: 22px auto 0; border: 3px solid #fff; border-radius: 50%; background: #08768b; box-shadow: 0 0 0 1px #08768b; }
151
+ .timeline-dot.announced { background: #b8863b; box-shadow: 0 0 0 1px #b8863b; }
152
+ .event-card { padding: 15px 17px 16px; border: 1px solid #d2dadd; border-radius: 13px; background: #fff; }
153
+ .event-meta { display: flex; gap: 7px; align-items: center; margin-bottom: 7px; }
154
+ .type-label, .status { display: inline-block; padding: 3px 7px; border-radius: 999px; font-size: 10px; }
155
+ .type-label { color: #08768b; background: #edf7f8; }
156
+ .status.confirmed { color: #177255; background: #edf8f1; }
157
+ .status.announced { color: #8b6426; background: #fbf4e6; }
158
+ .event-card h2 { margin: 0 0 10px; font-size: 17px; line-height: 1.35; }
159
+ .schedule { margin-bottom: 10px; color: #08768b; font-size: 11px; }
160
+ .post-preview { padding: 11px 12px; border: 1px solid #e0e5e6; border-radius: 10px; background: #fbfcfc; }
161
+ .post-preview.empty { color: #89969a; font-size: 12px; }
162
+ .post-head { display: flex; align-items: center; min-width: 0; }
163
+ .avatar { display: grid; flex: 0 0 auto; width: 28px; height: 28px; place-items: center; overflow: hidden; border-radius: 50%; background: linear-gradient(145deg, #a57954, #d3aa7d); color: #fff; font-size: 14px; font-weight: 700; }
164
+ .avatar-image { display: block; width: 100%; height: 100%; object-fit: cover; }
165
+ .avatar-fallback { display: block; }
166
+ .author { display: flex; flex-direction: column; margin-left: 8px; line-height: 1.2; }
167
+ .author strong { font-size: 11px; }
168
+ .author span { color: #89969a; font-size: 9px; }
169
+ .post-head time { margin-left: auto; color: #89969a; font-size: 10px; }
170
+ .post-preview p { margin: 10px 0 0; color: #27343c; font-size: 12px; line-height: 1.55; }
171
+ .summary { align-self: start; padding: 18px; border: 1px solid #d2dadd; border-radius: 13px; background: #fbfcfc; }
172
+ .summary h2 { margin: 0 0 15px; font-size: 17px; }
173
+ .summary-period { margin-bottom: 18px; color: #6f7d83; font-size: 11px; line-height: 1.5; }
174
+ .stat { display: flex; align-items: baseline; justify-content: space-between; padding: 11px 0; border-top: 1px solid #e0e5e6; }
175
+ .stat span { color: #6f7d83; font-size: 12px; }
176
+ .stat strong { color: #27343c; font-size: 22px; }
177
+ .summary-note { margin: 17px 0 0; color: #89969a; font-size: 10px; line-height: 1.6; }
178
+ .empty { padding: 54px 0; color: #89969a; text-align: center; }
179
+ </style>
180
+ </head>
181
+ <body>
182
+ <main class="history-shell">
183
+ <header class="page-header">
184
+ <div><div class="eyebrow">CODEX RESET MONITOR</div><h1>Codex 重置历史</h1><p class="subtitle">按北京时间排列的公开重置事件时间线</p></div>
185
+ <div class="generated">生成时间<br>${escapeHtml(formatGeneratedAt(now))}</div>
186
+ </header>
187
+ <div class="layout">
188
+ <section class="timeline">
189
+ ${sorted.length ? sorted.map(renderTimelineItem).join('') : '<div class="empty">暂无历史事件</div>'}
190
+ </section>
191
+ <aside class="summary">
192
+ <h2>历史概览</h2>
193
+ <div class="summary-period">统计区间<br>${escapeHtml(period)}</div>
194
+ <div class="stat"><span>事件总数</span><strong>${events.length}</strong></div>
195
+ <div class="stat"><span>已确认</span><strong>${confirmed.length}</strong></div>
196
+ <div class="stat"><span>全员重置</span><strong>${directResetCount}</strong></div>
197
+ <div class="stat"><span>重置卡</span><strong>${resetCreditCount}</strong></div>
198
+ <div class="stat"><span>未重置预告</span><strong>${announcedCount}</strong></div>
199
+ <p class="summary-note">数据来自 AIHOT 公开快照。时间线仅展示公开事件,不代表账户一定获得重置。</p>
200
+ </aside>
201
+ </div>
202
+ </main>
203
+ </body>
204
204
  </html>`;
205
205
  }
206
206
  async function renderHistoryImage(browser, events, now = new Date()) {
@@ -34,10 +34,10 @@ function formatGeneratedAt(now) {
34
34
  }).format(now);
35
35
  }
36
36
  function metricCard(label, value, note, tone) {
37
- return `<div class="metric ${tone}">
38
- <div class="metric-label">${escapeHtml(label)}</div>
39
- <div class="metric-value">${escapeHtml(value)}</div>
40
- <div class="metric-note">${escapeHtml(note)}</div>
37
+ return `<div class="metric ${tone}">
38
+ <div class="metric-label">${escapeHtml(label)}</div>
39
+ <div class="metric-value">${escapeHtml(value)}</div>
40
+ <div class="metric-note">${escapeHtml(note)}</div>
41
41
  </div>`;
42
42
  }
43
43
  function renderProbabilityHtml(summary, now = new Date()) {
@@ -45,92 +45,92 @@ function renderProbabilityHtml(summary, now = new Date()) {
45
45
  const directRatio = ratio(summary.directResetCount, confirmedTotal);
46
46
  const creditRatio = ratio(summary.resetCreditCount, confirmedTotal);
47
47
  const recentDate = summary.lastConfirmedDate ?? '暂无记录';
48
- return `<!doctype html>
49
- <html lang="zh-CN">
50
- <head>
51
- <meta charset="utf-8">
52
- <style>
53
- * { box-sizing: border-box; }
54
- html, body { margin: 0; padding: 0; background: #f4f6f5; color: #27343c; font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif; }
55
- body { padding: 26px; }
56
- .card { position: relative; width: 1120px; min-height: 680px; overflow: hidden; padding: 38px 44px 30px; border: 1px solid #d2dadd; border-radius: 16px; background: radial-gradient(circle at 82% 12%, rgba(8, 118, 139, .07), transparent 30%), radial-gradient(circle at 10% 96%, rgba(184, 134, 59, .07), transparent 28%), #fff; box-shadow: 0 16px 38px rgba(42, 58, 64, .10); }
57
- .card::before { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: .32; background-image: linear-gradient(rgba(8, 118, 139, .05) 1px, transparent 1px), linear-gradient(90deg, rgba(8, 118, 139, .05) 1px, transparent 1px); background-size: 34px 34px; mask-image: linear-gradient(to bottom, rgba(0,0,0,.42), transparent 72%); }
58
- .content { position: relative; z-index: 1; }
59
- .topline { display: flex; align-items: flex-start; justify-content: space-between; }
60
- .eyebrow { color: #08768b; font-size: 11px; font-weight: 700; letter-spacing: .22em; }
61
- .source { color: #6f7d83; font-size: 11px; line-height: 1.6; text-align: right; }
62
- h1 { margin: 22px 0 8px; color: #27343c; font-size: 34px; font-weight: 700; letter-spacing: -.04em; }
63
- .subtitle { margin: 0; color: #6f7d83; font-size: 14px; }
64
- .rule { width: 100%; height: 1px; margin: 26px 0 22px; background: linear-gradient(90deg, rgba(8, 118, 139, .36), rgba(8, 118, 139, .08), transparent); }
65
- .metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
66
- .metric { position: relative; min-height: 186px; padding: 22px 24px; overflow: hidden; border: 1px solid #c8dedd; border-radius: 13px; background: #eef7f6; }
67
- .metric::after { content: ""; position: absolute; right: -28px; bottom: -58px; width: 180px; height: 180px; border: 1px solid rgba(8, 118, 139, .18); border-radius: 50%; box-shadow: 0 0 0 18px rgba(8, 118, 139, .035), 0 0 0 36px rgba(8, 118, 139, .018); }
68
- .metric.seven { background: #faf6ee; border-color: #e2d8c5; }
69
- .metric.seven::after { border-color: rgba(184, 134, 59, .22); box-shadow: 0 0 0 18px rgba(184, 134, 59, .035), 0 0 0 36px rgba(184, 134, 59, .018); }
70
- .metric-label { color: #6f7d83; font-size: 13px; }
71
- .metric-value { margin-top: 7px; color: #08768b; font-family: Georgia, "Times New Roman", serif; font-size: 66px; font-weight: 700; letter-spacing: -.07em; line-height: 1; }
72
- .metric.seven .metric-value { color: #a27831; }
73
- .metric-note { margin-top: 14px; color: #52646a; font-size: 12px; }
74
- .lower { display: grid; grid-template-columns: 1.15fr .85fr; gap: 16px; margin-top: 16px; }
75
- .panel { padding: 18px 20px; border: 1px solid #d2dadd; border-radius: 13px; background: #fbfcfc; }
76
- .panel h2 { margin: 0 0 15px; color: #27343c; font-size: 14px; }
77
- .sample-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
78
- .sample { padding-right: 10px; border-right: 1px solid #e0e5e6; }
79
- .sample:last-child { border-right: 0; }
80
- .sample-value { color: #27343c; font-size: 22px; font-weight: 700; }
81
- .sample-label { margin-top: 4px; color: #6f7d83; font-size: 11px; }
82
- .bar-row { display: grid; grid-template-columns: 76px 1fr 42px; align-items: center; gap: 9px; margin: 12px 0; color: #52646a; font-size: 11px; }
83
- .bar { height: 7px; overflow: hidden; border-radius: 99px; background: #e4ecec; }
84
- .bar span { display: block; height: 100%; border-radius: inherit; background: #71c8bd; }
85
- .bar-row.credit .bar span { background: #d8ad5f; }
86
- .bar-percent { color: #27343c; text-align: right; }
87
- .recent { display: flex; align-items: center; justify-content: space-between; padding-top: 15px; margin-top: 16px; border-top: 1px solid #e0e5e6; }
88
- .recent-label { color: #6f7d83; font-size: 11px; }
89
- .recent-date { color: #a27831; font-size: 15px; font-weight: 700; }
90
- .footer { display: flex; align-items: flex-end; justify-content: space-between; gap: 30px; margin-top: 22px; color: #7a888d; font-size: 10px; line-height: 1.65; }
91
- .footer strong { color: #52646a; font-weight: 600; }
92
- .mark { color: #08768b; font-size: 11px; font-weight: 700; letter-spacing: .08em; white-space: nowrap; }
93
- </style>
94
- </head>
95
- <body>
96
- <main class="card">
97
- <div class="content">
98
- <header class="topline">
99
- <div class="eyebrow">CODEX RESET / PUBLIC HISTORY</div>
100
- <div class="source">AIHOT · ASIA/SHANGHAI<br>生成于 ${escapeHtml(formatGeneratedAt(now))}</div>
101
- </header>
102
- <h1>Codex 重置概率分析</h1>
103
- <p class="subtitle">基于当前公开历史快照的经验统计,不是官方预测</p>
104
- <div class="rule"></div>
105
-
106
- <section class="metrics">
107
- ${metricCard('未来 24 小时', percentage(summary.next24hProbability), '至少出现一个公开重置事件', 'day')}
108
- ${metricCard('未来 7 天', percentage(summary.next7dProbability), '至少出现一个公开重置事件', 'seven')}
109
- </section>
110
-
111
- <section class="lower">
112
- <div class="panel">
113
- <h2>样本概览</h2>
114
- <div class="sample-grid">
115
- <div class="sample"><div class="sample-value">${summary.observedDays}</div><div class="sample-label">天观察样本</div></div>
116
- <div class="sample"><div class="sample-value">${summary.confirmedCount}</div><div class="sample-label">条已确认</div></div>
117
- <div class="sample"><div class="sample-value">${summary.activeDays}</div><div class="sample-label">个事件日</div></div>
118
- <div class="sample"><div class="sample-value">${summary.announcedCount}</div><div class="sample-label">条未重置预告</div></div>
119
- </div>
120
- <div class="recent"><span class="recent-label">最近确认日期</span><span class="recent-date">${escapeHtml(recentDate)}</span></div>
121
- </div>
122
- <div class="panel">
123
- <h2>已确认事件构成</h2>
124
- <div class="bar-row"><span>全员重置</span><div class="bar"><span style="width:${directRatio}"></span></div><span class="bar-percent">${summary.directResetCount}</span></div>
125
- <div class="bar-row credit"><span>重置卡</span><div class="bar"><span style="width:${creditRatio}"></span></div><span class="bar-percent">${summary.resetCreditCount}</span></div>
126
- <div class="recent"><span class="recent-label">历史事件日率</span><span class="recent-date">${(summary.dailyEventDayRate * 100).toFixed(2)}% / 天</span></div>
127
- </div>
128
- </section>
129
-
130
- <footer class="footer"><span><strong>说明:</strong>概率由历史事件日率进行经验估计,不代表你的账户一定会获得重置。</span><span class="mark">RESET MONITOR</span></footer>
131
- </div>
132
- </main>
133
- </body>
48
+ return `<!doctype html>
49
+ <html lang="zh-CN">
50
+ <head>
51
+ <meta charset="utf-8">
52
+ <style>
53
+ * { box-sizing: border-box; }
54
+ html, body { margin: 0; padding: 0; background: #f4f6f5; color: #27343c; font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif; }
55
+ body { padding: 26px; }
56
+ .card { position: relative; width: 1120px; min-height: 680px; overflow: hidden; padding: 38px 44px 30px; border: 1px solid #d2dadd; border-radius: 16px; background: radial-gradient(circle at 82% 12%, rgba(8, 118, 139, .07), transparent 30%), radial-gradient(circle at 10% 96%, rgba(184, 134, 59, .07), transparent 28%), #fff; box-shadow: 0 16px 38px rgba(42, 58, 64, .10); }
57
+ .card::before { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: .32; background-image: linear-gradient(rgba(8, 118, 139, .05) 1px, transparent 1px), linear-gradient(90deg, rgba(8, 118, 139, .05) 1px, transparent 1px); background-size: 34px 34px; mask-image: linear-gradient(to bottom, rgba(0,0,0,.42), transparent 72%); }
58
+ .content { position: relative; z-index: 1; }
59
+ .topline { display: flex; align-items: flex-start; justify-content: space-between; }
60
+ .eyebrow { color: #08768b; font-size: 11px; font-weight: 700; letter-spacing: .22em; }
61
+ .source { color: #6f7d83; font-size: 11px; line-height: 1.6; text-align: right; }
62
+ h1 { margin: 22px 0 8px; color: #27343c; font-size: 34px; font-weight: 700; letter-spacing: -.04em; }
63
+ .subtitle { margin: 0; color: #6f7d83; font-size: 14px; }
64
+ .rule { width: 100%; height: 1px; margin: 26px 0 22px; background: linear-gradient(90deg, rgba(8, 118, 139, .36), rgba(8, 118, 139, .08), transparent); }
65
+ .metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
66
+ .metric { position: relative; min-height: 186px; padding: 22px 24px; overflow: hidden; border: 1px solid #c8dedd; border-radius: 13px; background: #eef7f6; }
67
+ .metric::after { content: ""; position: absolute; right: -28px; bottom: -58px; width: 180px; height: 180px; border: 1px solid rgba(8, 118, 139, .18); border-radius: 50%; box-shadow: 0 0 0 18px rgba(8, 118, 139, .035), 0 0 0 36px rgba(8, 118, 139, .018); }
68
+ .metric.seven { background: #faf6ee; border-color: #e2d8c5; }
69
+ .metric.seven::after { border-color: rgba(184, 134, 59, .22); box-shadow: 0 0 0 18px rgba(184, 134, 59, .035), 0 0 0 36px rgba(184, 134, 59, .018); }
70
+ .metric-label { color: #6f7d83; font-size: 13px; }
71
+ .metric-value { margin-top: 7px; color: #08768b; font-family: Georgia, "Times New Roman", serif; font-size: 66px; font-weight: 700; letter-spacing: -.07em; line-height: 1; }
72
+ .metric.seven .metric-value { color: #a27831; }
73
+ .metric-note { margin-top: 14px; color: #52646a; font-size: 12px; }
74
+ .lower { display: grid; grid-template-columns: 1.15fr .85fr; gap: 16px; margin-top: 16px; }
75
+ .panel { padding: 18px 20px; border: 1px solid #d2dadd; border-radius: 13px; background: #fbfcfc; }
76
+ .panel h2 { margin: 0 0 15px; color: #27343c; font-size: 14px; }
77
+ .sample-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
78
+ .sample { padding-right: 10px; border-right: 1px solid #e0e5e6; }
79
+ .sample:last-child { border-right: 0; }
80
+ .sample-value { color: #27343c; font-size: 22px; font-weight: 700; }
81
+ .sample-label { margin-top: 4px; color: #6f7d83; font-size: 11px; }
82
+ .bar-row { display: grid; grid-template-columns: 76px 1fr 42px; align-items: center; gap: 9px; margin: 12px 0; color: #52646a; font-size: 11px; }
83
+ .bar { height: 7px; overflow: hidden; border-radius: 99px; background: #e4ecec; }
84
+ .bar span { display: block; height: 100%; border-radius: inherit; background: #71c8bd; }
85
+ .bar-row.credit .bar span { background: #d8ad5f; }
86
+ .bar-percent { color: #27343c; text-align: right; }
87
+ .recent { display: flex; align-items: center; justify-content: space-between; padding-top: 15px; margin-top: 16px; border-top: 1px solid #e0e5e6; }
88
+ .recent-label { color: #6f7d83; font-size: 11px; }
89
+ .recent-date { color: #a27831; font-size: 15px; font-weight: 700; }
90
+ .footer { display: flex; align-items: flex-end; justify-content: space-between; gap: 30px; margin-top: 22px; color: #7a888d; font-size: 10px; line-height: 1.65; }
91
+ .footer strong { color: #52646a; font-weight: 600; }
92
+ .mark { color: #08768b; font-size: 11px; font-weight: 700; letter-spacing: .08em; white-space: nowrap; }
93
+ </style>
94
+ </head>
95
+ <body>
96
+ <main class="card">
97
+ <div class="content">
98
+ <header class="topline">
99
+ <div class="eyebrow">CODEX RESET / PUBLIC HISTORY</div>
100
+ <div class="source">AIHOT · ASIA/SHANGHAI<br>生成于 ${escapeHtml(formatGeneratedAt(now))}</div>
101
+ </header>
102
+ <h1>Codex 重置概率分析</h1>
103
+ <p class="subtitle">基于当前公开历史快照的经验统计,不是官方预测</p>
104
+ <div class="rule"></div>
105
+
106
+ <section class="metrics">
107
+ ${metricCard('未来 24 小时', percentage(summary.next24hProbability), '至少出现一个公开重置事件', 'day')}
108
+ ${metricCard('未来 7 天', percentage(summary.next7dProbability), '至少出现一个公开重置事件', 'seven')}
109
+ </section>
110
+
111
+ <section class="lower">
112
+ <div class="panel">
113
+ <h2>样本概览</h2>
114
+ <div class="sample-grid">
115
+ <div class="sample"><div class="sample-value">${summary.observedDays}</div><div class="sample-label">天观察样本</div></div>
116
+ <div class="sample"><div class="sample-value">${summary.confirmedCount}</div><div class="sample-label">条已确认</div></div>
117
+ <div class="sample"><div class="sample-value">${summary.activeDays}</div><div class="sample-label">个事件日</div></div>
118
+ <div class="sample"><div class="sample-value">${summary.announcedCount}</div><div class="sample-label">条未重置预告</div></div>
119
+ </div>
120
+ <div class="recent"><span class="recent-label">最近确认日期</span><span class="recent-date">${escapeHtml(recentDate)}</span></div>
121
+ </div>
122
+ <div class="panel">
123
+ <h2>已确认事件构成</h2>
124
+ <div class="bar-row"><span>全员重置</span><div class="bar"><span style="width:${directRatio}"></span></div><span class="bar-percent">${summary.directResetCount}</span></div>
125
+ <div class="bar-row credit"><span>重置卡</span><div class="bar"><span style="width:${creditRatio}"></span></div><span class="bar-percent">${summary.resetCreditCount}</span></div>
126
+ <div class="recent"><span class="recent-label">历史事件日率</span><span class="recent-date">${(summary.dailyEventDayRate * 100).toFixed(2)}% / 天</span></div>
127
+ </div>
128
+ </section>
129
+
130
+ <footer class="footer"><span><strong>说明:</strong>概率由历史事件日率进行经验估计,不代表你的账户一定会获得重置。</span><span class="mark">RESET MONITOR</span></footer>
131
+ </div>
132
+ </main>
133
+ </body>
134
134
  </html>`;
135
135
  }
136
136
  async function renderProbabilityImage(browser, summary, now = new Date()) {
package/package.json CHANGED
@@ -1,51 +1,51 @@
1
- {
2
- "name": "koishi-plugin-codex-reset",
3
- "version": "0.1.3",
4
- "description": "Monitor AIHOT Codex reset events and notify Koishi channels.",
5
- "main": "lib/index.js",
6
- "types": "lib/index.d.ts",
7
- "files": [
8
- "lib",
9
- "resources",
10
- "README.md",
11
- "LICENSE"
12
- ],
13
- "keywords": [
14
- "koishi",
15
- "koishi-plugin",
16
- "codex",
17
- "openai",
18
- "reset"
19
- ],
20
- "repository": {
21
- "type": "git",
22
- "url": "git+https://github.com/lumia1998/koishi-plugin-codex-reset.git"
23
- },
24
- "homepage": "https://github.com/lumia1998/koishi-plugin-codex-reset#readme",
25
- "bugs": {
26
- "url": "https://github.com/lumia1998/koishi-plugin-codex-reset/issues"
27
- },
28
- "license": "MIT",
29
- "engines": {
30
- "node": ">=18.0.0"
31
- },
32
- "scripts": {
33
- "build": "tsc",
34
- "prepare": "npm run build",
35
- "test": "npm run build && node --test tests/*.test.cjs"
36
- },
37
- "peerDependencies": {
38
- "koishi": ">=4.18.0",
39
- "koishi-plugin-puppeteer": "*"
40
- },
41
- "peerDependenciesMeta": {
42
- "koishi-plugin-puppeteer": {
43
- "optional": true
44
- }
45
- },
46
- "devDependencies": {
47
- "@types/node": "^22.0.0",
48
- "koishi": "^4.18.9",
49
- "typescript": "^5.7.0"
50
- }
51
- }
1
+ {
2
+ "name": "koishi-plugin-codex-reset",
3
+ "version": "0.1.4",
4
+ "description": "Monitor AIHOT Codex reset events and notify Koishi channels.",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "files": [
8
+ "lib",
9
+ "resources",
10
+ "README.md",
11
+ "LICENSE"
12
+ ],
13
+ "keywords": [
14
+ "koishi",
15
+ "koishi-plugin",
16
+ "codex",
17
+ "openai",
18
+ "reset"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/lumia1998/koishi-plugin-codex-reset.git"
23
+ },
24
+ "homepage": "https://github.com/lumia1998/koishi-plugin-codex-reset#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/lumia1998/koishi-plugin-codex-reset/issues"
27
+ },
28
+ "license": "MIT",
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc",
34
+ "prepare": "npm run build",
35
+ "test": "npm run build && node --test tests/*.test.cjs"
36
+ },
37
+ "peerDependencies": {
38
+ "koishi": ">=4.18.0",
39
+ "koishi-plugin-puppeteer": "*"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "koishi-plugin-puppeteer": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.0.0",
48
+ "koishi": "^4.18.9",
49
+ "typescript": "^5.7.0"
50
+ }
51
+ }