koishi-plugin-codex-reset 0.1.0 → 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 +10 -10
- package/lib/calendar.js +1 -1
- package/lib/history.js +1 -1
- package/lib/index.js +24 -9
- package/lib/probability.d.ts +4 -0
- package/lib/probability.js +149 -0
- package/lib/stats.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,22 +7,22 @@
|
|
|
7
7
|
- 定时轮询 AIHOT 的 Codex / ChatGPT Work 重置事件快照。
|
|
8
8
|
- 支持推送新的重置预告、已确认的全员重置和重置卡事件。
|
|
9
9
|
- 目标支持平台 + 频道配置,例如 `onebot` + `123456789`。
|
|
10
|
-
- `codex
|
|
11
|
-
- `codex
|
|
12
|
-
- `codex
|
|
13
|
-
- `codex
|
|
10
|
+
- `codex.probability` / `重置概率`:生成精美的概率统计卡片图片;没有 Puppeteer 时回退为纯文本。
|
|
11
|
+
- `codex.event` / `最近重置`:统计上一次已确认重置距今多少小时。
|
|
12
|
+
- `codex.calendar` / `重置日历`:生成本月的 PNG 日历图片,右侧详情固定为命令执行当天。
|
|
13
|
+
- `codex.history` / `重置历史`:生成公开重置事件的历史时间线图片。
|
|
14
14
|
- 日历中的 Tibo 头像作为本地资源随插件发布,不依赖用户访问 X 或运行时下载图片。
|
|
15
15
|
|
|
16
16
|
## 命令
|
|
17
17
|
|
|
18
18
|
命令目前都不接收额外参数:
|
|
19
19
|
|
|
20
|
-
- `codex
|
|
21
|
-
- `codex
|
|
22
|
-
- `codex
|
|
23
|
-
- `codex
|
|
20
|
+
- `codex.probability`,别名 `重置概率`:读取当前 API 快照,生成概率统计卡片,展示未来 24 小时和 7 天概率、样本概览、事件构成和最近确认日期;没有 Puppeteer 时回退为纯文本。
|
|
21
|
+
- `codex.event`,别名 `最近重置`:显示上一次已确认重置发生时间、距今小时数、类型和事件名称。
|
|
22
|
+
- `codex.calendar`,别名 `重置日历`:发送本月日历 PNG;右侧详情固定显示命令执行当天(北京时间)的事件。
|
|
23
|
+
- `codex.history`,别名 `重置历史`:发送按北京时间排列的历史重置时间线 PNG。
|
|
24
24
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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);
|
|
@@ -303,8 +310,7 @@ function apply(ctx, config) {
|
|
|
303
310
|
ctx.setInterval(() => void monitor.poll(), config.pollInterval * 1000);
|
|
304
311
|
});
|
|
305
312
|
ctx.on('dispose', () => monitor.dispose());
|
|
306
|
-
ctx.command('codex
|
|
307
|
-
.alias('codex-reset.probability')
|
|
313
|
+
ctx.command('codex.probability', '分析 Codex 重置事件的历史概率')
|
|
308
314
|
.alias('重置概率')
|
|
309
315
|
.action(async () => {
|
|
310
316
|
if (!config.enableProbabilityCommand)
|
|
@@ -312,10 +318,21 @@ function apply(ctx, config) {
|
|
|
312
318
|
const snapshot = await monitor.ensureSnapshot();
|
|
313
319
|
if (!snapshot)
|
|
314
320
|
return '暂时无法获取 Codex 重置事件数据,请稍后再试。';
|
|
315
|
-
|
|
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
|
+
}
|
|
316
334
|
});
|
|
317
|
-
ctx.command('codex
|
|
318
|
-
.alias('codex-reset.event')
|
|
335
|
+
ctx.command('codex.event', '查看上一次 Codex 重置距今多久')
|
|
319
336
|
.alias('最近重置')
|
|
320
337
|
.action(async () => {
|
|
321
338
|
const snapshot = await monitor.ensureSnapshot();
|
|
@@ -323,8 +340,7 @@ function apply(ctx, config) {
|
|
|
323
340
|
return '暂时无法获取 Codex 重置事件数据,请稍后再试。';
|
|
324
341
|
return (0, stats_1.formatLatestReset)(snapshot);
|
|
325
342
|
});
|
|
326
|
-
ctx.command('codex
|
|
327
|
-
.alias('codex-reset.calendar')
|
|
343
|
+
ctx.command('codex.calendar', '发送本月的 Codex 重置日历图片')
|
|
328
344
|
.alias('重置日历')
|
|
329
345
|
.action(async () => {
|
|
330
346
|
if (!config.enableCalendarCommand)
|
|
@@ -342,8 +358,7 @@ function apply(ctx, config) {
|
|
|
342
358
|
return '日历图片生成失败,请检查 Puppeteer 服务和中文字体。';
|
|
343
359
|
}
|
|
344
360
|
});
|
|
345
|
-
ctx.command('codex
|
|
346
|
-
.alias('codex-reset.history')
|
|
361
|
+
ctx.command('codex.history', '发送 Codex 重置历史时间线图片')
|
|
347
362
|
.alias('重置历史')
|
|
348
363
|
.action(async () => {
|
|
349
364
|
if (!config.enableHistoryCommand)
|
|
@@ -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('&', '&')
|
|
8
|
+
.replaceAll('<', '<')
|
|
9
|
+
.replaceAll('>', '>')
|
|
10
|
+
.replaceAll('"', '"')
|
|
11
|
+
.replaceAll("'", ''');
|
|
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}
|
|
148
|
+
`其中:全员重置 ${summary.directResetCount} 条,重置卡 ${summary.resetCreditCount} 条,未重置预告 ${summary.announcedCount} 条。`,
|
|
149
149
|
];
|
|
150
150
|
if (summary.confirmedCount === 0 || summary.activeDays === 0) {
|
|
151
151
|
lines.push('有效样本不足,暂时无法估算概率。');
|