koishi-plugin-codex-reset 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  - 定时轮询 AIHOT 的 Codex / ChatGPT Work 重置事件快照。
8
8
  - 支持推送新的重置预告、已确认的全员重置和重置卡事件。
9
9
  - 目标支持平台 + 频道配置,例如 `onebot` + `123456789`。
10
- - `codex.probability` / `重置概率`:基于历史快照计算经验概率。
10
+ - `codex.probability` / `重置概率`:生成精美的概率统计卡片图片;没有 Puppeteer 时回退为纯文本。
11
11
  - `codex.event` / `最近重置`:统计上一次已确认重置距今多少小时。
12
12
  - `codex.calendar` / `重置日历`:生成本月的 PNG 日历图片,右侧详情固定为命令执行当天。
13
13
  - `codex.history` / `重置历史`:生成公开重置事件的历史时间线图片。
@@ -17,12 +17,12 @@
17
17
 
18
18
  命令目前都不接收额外参数:
19
19
 
20
- - `codex.probability`,别名 `重置概率`:读取当前 API 快照,统计历史事件日率,并估算未来 24 小时和 7 天出现公开重置事件的概率。
20
+ - `codex.probability`,别名 `重置概率`:读取当前 API 快照,生成概率统计卡片,展示未来 24 小时和 7 天概率、样本概览、事件构成和最近确认日期;没有 Puppeteer 时回退为纯文本。
21
21
  - `codex.event`,别名 `最近重置`:显示上一次已确认重置发生时间、距今小时数、类型和事件名称。
22
22
  - `codex.calendar`,别名 `重置日历`:发送本月日历 PNG;右侧详情固定显示命令执行当天(北京时间)的事件。
23
23
  - `codex.history`,别名 `重置历史`:发送按北京时间排列的历史重置时间线 PNG。
24
24
 
25
- 日历和历史时间线命令需要安装并启用 `koishi-plugin-puppeteer`;概率、日历和历史命令都可以在配置中单独关闭。
25
+ 概率、日历和历史时间线命令需要安装并启用 `koishi-plugin-puppeteer` 才会发送图片;没有 Puppeteer 时概率命令仍会回退为纯文本。三个图片命令都可以在配置中单独关闭。
26
26
 
27
27
  首次轮询只建立本地基线,不会把 API 返回的历史事件全部推送;后续轮询只处理新增事件或 `announced` → `confirmed` 的状态变化。插件重启后会重新建立基线。
28
28
 
@@ -32,7 +32,7 @@
32
32
  npm install koishi-plugin-codex-reset
33
33
  ```
34
34
 
35
- 如果需要日历或历史时间线图片,再安装并启用 `koishi-plugin-puppeteer`。不安装 Puppeteer 时,监控、最近重置和概率分析仍然可以使用,图片命令会给出提示。
35
+ 如果需要概率卡片、日历或历史时间线图片,再安装并启用 `koishi-plugin-puppeteer`。不安装 Puppeteer 时,监控、最近重置和概率分析仍然可以使用,概率命令回退为纯文本,其他图片命令会给出提示。
36
36
 
37
37
  ## 配置
38
38
 
package/lib/calendar.js CHANGED
@@ -181,7 +181,7 @@ function renderDetails(selectedDate, marks) {
181
181
  const header = formatDateHeader(selectedDate);
182
182
  const selectedEvents = [...new Map((marks.get(selectedDate) ?? []).map((mark) => [mark.event.id, mark.event])).values()];
183
183
  const groups = selectedEvents.map((event) => {
184
- const status = event.status === 'confirmed' ? '已重置' : '待确认';
184
+ const status = event.status === 'confirmed' ? '已重置' : '未重置预告';
185
185
  const statusClass = event.status === 'confirmed' ? 'confirmed-state' : 'announced-state';
186
186
  const posts = event.posts.slice(0, 3).map((post) => renderPostCard(event, post)).join('');
187
187
  return `<section class="event-group">
package/lib/history.js CHANGED
@@ -176,7 +176,7 @@ function renderHistoryHtml(events, now = new Date()) {
176
176
  <div class="stat"><span>已确认</span><strong>${confirmed.length}</strong></div>
177
177
  <div class="stat"><span>全员重置</span><strong>${directResetCount}</strong></div>
178
178
  <div class="stat"><span>重置卡</span><strong>${resetCreditCount}</strong></div>
179
- <div class="stat"><span>待确认预告</span><strong>${announcedCount}</strong></div>
179
+ <div class="stat"><span>未重置预告</span><strong>${announcedCount}</strong></div>
180
180
  <p class="summary-note">数据来自 AIHOT 公开快照。时间线仅展示公开事件,不代表账户一定获得重置。</p>
181
181
  </aside>
182
182
  </div>
package/lib/index.js CHANGED
@@ -5,6 +5,7 @@ exports.apply = apply;
5
5
  const koishi_1 = require("koishi");
6
6
  const calendar_1 = require("./calendar");
7
7
  const history_1 = require("./history");
8
+ const probability_1 = require("./probability");
8
9
  const stats_1 = require("./stats");
9
10
  exports.name = 'codex-reset';
10
11
  exports.inject = {
@@ -235,6 +236,12 @@ class CodexResetMonitor {
235
236
  return null;
236
237
  return (0, history_1.renderHistoryImage)(browser, snapshot.events);
237
238
  }
239
+ async createProbabilityImage(summary) {
240
+ const browser = this.ctx.puppeteer;
241
+ if (!browser)
242
+ return null;
243
+ return (0, probability_1.renderProbabilityImage)(browser, summary);
244
+ }
238
245
  async fetchSnapshot(ignoreEtag = false) {
239
246
  const controller = new AbortController();
240
247
  const timeout = setTimeout(() => controller.abort(), this.config.requestTimeout);
@@ -311,7 +318,19 @@ function apply(ctx, config) {
311
318
  const snapshot = await monitor.ensureSnapshot();
312
319
  if (!snapshot)
313
320
  return '暂时无法获取 Codex 重置事件数据,请稍后再试。';
314
- return (0, stats_1.formatProbability)((0, stats_1.analyzeProbability)(snapshot));
321
+ const summary = (0, stats_1.analyzeProbability)(snapshot);
322
+ if (!monitor.hasPuppeteer)
323
+ return (0, stats_1.formatProbability)(summary);
324
+ try {
325
+ const image = await monitor.createProbabilityImage(summary);
326
+ if (!image)
327
+ return (0, stats_1.formatProbability)(summary);
328
+ return koishi_1.h.image(image, 'image/png');
329
+ }
330
+ catch (error) {
331
+ ctx.logger('codex-reset').warn(`生成概率卡片失败:${error instanceof Error ? error.message : String(error)}`);
332
+ return (0, stats_1.formatProbability)(summary);
333
+ }
315
334
  });
316
335
  ctx.command('codex.event', '查看上一次 Codex 重置距今多久')
317
336
  .alias('最近重置')
@@ -0,0 +1,4 @@
1
+ import type { CalendarBrowser } from './calendar';
2
+ import type { ProbabilitySummary } from './stats';
3
+ export declare function renderProbabilityHtml(summary: ProbabilitySummary, now?: Date): string;
4
+ export declare function renderProbabilityImage(browser: CalendarBrowser, summary: ProbabilitySummary, now?: Date): Promise<Buffer>;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderProbabilityHtml = renderProbabilityHtml;
4
+ exports.renderProbabilityImage = renderProbabilityImage;
5
+ function escapeHtml(value) {
6
+ return value
7
+ .replaceAll('&', '&amp;')
8
+ .replaceAll('<', '&lt;')
9
+ .replaceAll('>', '&gt;')
10
+ .replaceAll('"', '&quot;')
11
+ .replaceAll("'", '&#39;');
12
+ }
13
+ function percentage(value) {
14
+ if (!Number.isFinite(value))
15
+ return '—';
16
+ if (value < 0.01)
17
+ return '<1%';
18
+ return `${(value * 100).toFixed(value < 0.1 ? 1 : 0)}%`;
19
+ }
20
+ function ratio(value, total) {
21
+ if (!total)
22
+ return '0%';
23
+ return `${Math.round((value / total) * 100)}%`;
24
+ }
25
+ function formatGeneratedAt(now) {
26
+ return new Intl.DateTimeFormat('zh-CN', {
27
+ timeZone: 'Asia/Shanghai',
28
+ year: 'numeric',
29
+ month: '2-digit',
30
+ day: '2-digit',
31
+ hour: '2-digit',
32
+ minute: '2-digit',
33
+ hour12: false,
34
+ }).format(now);
35
+ }
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>
41
+ </div>`;
42
+ }
43
+ function renderProbabilityHtml(summary, now = new Date()) {
44
+ const confirmedTotal = summary.directResetCount + summary.resetCreditCount;
45
+ const directRatio = ratio(summary.directResetCount, confirmedTotal);
46
+ const creditRatio = ratio(summary.resetCreditCount, confirmedTotal);
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>
134
+ </html>`;
135
+ }
136
+ async function renderProbabilityImage(browser, summary, now = new Date()) {
137
+ let page = null;
138
+ try {
139
+ page = await browser.page();
140
+ await page.setViewport({ width: 1120, height: 680, deviceScaleFactor: 1 });
141
+ await page.setContent(renderProbabilityHtml(summary, now), { waitUntil: 'domcontentloaded', timeout: 15000 });
142
+ const image = await page.screenshot({ type: 'png', fullPage: true });
143
+ return Buffer.from(image);
144
+ }
145
+ finally {
146
+ if (page)
147
+ await page.close();
148
+ }
149
+ }
package/lib/stats.js CHANGED
@@ -145,7 +145,7 @@ function formatProbability(summary) {
145
145
  const lines = [
146
146
  'Codex 重置事件概率分析(历史统计)',
147
147
  `样本:${summary.observedDays} 天,已确认事件 ${summary.confirmedCount} 条,发生过事件的日期 ${summary.activeDays} 天。`,
148
- `其中:全员重置 ${summary.directResetCount} 条,重置卡 ${summary.resetCreditCount} 条,待确认预告 ${summary.announcedCount} 条。`,
148
+ `其中:全员重置 ${summary.directResetCount} 条,重置卡 ${summary.resetCreditCount} 条,未重置预告 ${summary.announcedCount} 条。`,
149
149
  ];
150
150
  if (summary.confirmedCount === 0 || summary.activeDays === 0) {
151
151
  lines.push('有效样本不足,暂时无法估算概率。');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-codex-reset",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Monitor AIHOT Codex reset events and notify Koishi channels.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",