koishi-plugin-codex-reset 0.1.1 → 0.1.3
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 +11 -14
- package/lib/calendar.js +13 -21
- package/lib/event.d.ts +4 -0
- package/lib/event.js +145 -0
- package/lib/history.d.ts +2 -0
- package/lib/history.js +20 -1
- package/lib/index.js +58 -26
- package/lib/probability.d.ts +4 -0
- package/lib/probability.js +149 -0
- package/lib/stats.js +1 -1
- package/lib/types.d.ts +0 -3
- 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.probability` /
|
|
11
|
-
- `codex.event` /
|
|
12
|
-
- `codex.calendar` / `重置日历`:生成本月的 PNG
|
|
13
|
-
- `codex.history` /
|
|
10
|
+
- `codex.probability` / `重置概率`:生成精美的概率统计卡片图片;没有 Puppeteer 时回退为纯文本。
|
|
11
|
+
- `codex.event` / `最近重置`:生成最近一次已确认重置的浅色卡片,展示距今时间、类型和来源帖子。
|
|
12
|
+
- `codex.calendar` / `重置日历`:生成本月的 PNG 日历图片,右侧固定展示最近发布的 2 条来源动态。
|
|
13
|
+
- `codex.history` / `重置历史`:生成公开重置事件的历史时间线图片,默认展示最近 1 个月。
|
|
14
14
|
- 日历中的 Tibo 头像作为本地资源随插件发布,不依赖用户访问 X 或运行时下载图片。
|
|
15
15
|
|
|
16
16
|
## 命令
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
命令都可以直接使用;历史时间线支持时间范围参数:
|
|
19
19
|
|
|
20
|
-
- `codex.probability`,别名 `重置概率`:读取当前 API
|
|
21
|
-
- `codex.event`,别名
|
|
22
|
-
- `codex.calendar`,别名 `重置日历`:发送本月日历 PNG
|
|
23
|
-
- `codex.history`,别名
|
|
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
24
|
|
|
25
|
-
|
|
25
|
+
概率、最近重置、日历和历史时间线命令始终启用;安装并启用 `koishi-plugin-puppeteer` 后会发送对应图片,没有 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
|
|
|
@@ -43,9 +43,6 @@ npm install koishi-plugin-codex-reset
|
|
|
43
43
|
- `推送重置预告`:是否推送预告。
|
|
44
44
|
- `推送已确认重置`:是否推送已确认的重置/重置卡事件。
|
|
45
45
|
- `艾特全体`:推送通知时是否艾特全体,默认关闭;需要目标平台支持全体艾特。
|
|
46
|
-
- `启用重置概率命令`:是否启用概率命令。
|
|
47
|
-
- `启用重置日历命令`:是否启用日历图片命令。
|
|
48
|
-
- `启用重置历史命令`:是否启用历史时间线图片命令。
|
|
49
46
|
|
|
50
47
|
API 是只读公开接口,不需要 API Key。插件使用 `ETag` 条件请求,并保留来源链接。请遵守 [AIHOT 公开使用规则](https://aihot.news/terms):公开可访问不等于自动获得对外商业使用、镜像或转售授权。
|
|
51
48
|
|
package/lib/calendar.js
CHANGED
|
@@ -98,15 +98,6 @@ function buildMonthMarks(events, start) {
|
|
|
98
98
|
}
|
|
99
99
|
return marks;
|
|
100
100
|
}
|
|
101
|
-
function formatDateHeader(value) {
|
|
102
|
-
const date = parseDateKey(value);
|
|
103
|
-
if (!date)
|
|
104
|
-
return { day: value, year: '' };
|
|
105
|
-
return {
|
|
106
|
-
day: `${date.getUTCMonth() + 1} 月 ${date.getUTCDate()} 日`,
|
|
107
|
-
year: String(date.getUTCFullYear()),
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
101
|
function formatPublishedAt(value) {
|
|
111
102
|
const date = new Date(value);
|
|
112
103
|
if (Number.isNaN(date.getTime()))
|
|
@@ -177,22 +168,23 @@ function renderPostCard(event, post) {
|
|
|
177
168
|
<div class="post-foot">${escapeHtml(post.stage)}</div>
|
|
178
169
|
</article>`;
|
|
179
170
|
}
|
|
180
|
-
function renderDetails(
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
171
|
+
function renderDetails(events) {
|
|
172
|
+
const latestPosts = events
|
|
173
|
+
.flatMap((event) => event.posts.map((post) => ({ event, post })))
|
|
174
|
+
.sort((a, b) => Date.parse(b.post.publishedAt) - Date.parse(a.post.publishedAt))
|
|
175
|
+
.slice(0, 2);
|
|
176
|
+
const groups = latestPosts.map(({ event, post }) => {
|
|
177
|
+
const status = event.status === 'confirmed' ? '已重置' : '未重置预告';
|
|
185
178
|
const statusClass = event.status === 'confirmed' ? 'confirmed-state' : 'announced-state';
|
|
186
|
-
const posts = event.posts.slice(0, 3).map((post) => renderPostCard(event, post)).join('');
|
|
187
179
|
return `<section class="event-group">
|
|
188
180
|
<div class="event-state ${statusClass}"><span class="state-icon">${event.status === 'confirmed' ? '✓' : '◷'}</span>${status}</div>
|
|
189
181
|
<h2>${escapeHtml(event.title)}</h2>
|
|
190
|
-
${
|
|
182
|
+
${renderPostCard(event, post)}
|
|
191
183
|
</section>`;
|
|
192
184
|
}).join('');
|
|
193
185
|
return `<aside class="details-pane">
|
|
194
|
-
<div class="details-header"><h2
|
|
195
|
-
<div class="details-body">${groups || '<div class="empty-state"
|
|
186
|
+
<div class="details-header"><h2>最近 2 条动态</h2><span>北京时间</span></div>
|
|
187
|
+
<div class="details-body">${groups || '<div class="empty-state">暂无来源帖子内容</div>'}</div>
|
|
196
188
|
</aside>`;
|
|
197
189
|
}
|
|
198
190
|
function renderCalendarHtml(events, now = new Date()) {
|
|
@@ -200,8 +192,8 @@ function renderCalendarHtml(events, now = new Date()) {
|
|
|
200
192
|
const todayDate = parseDateKey(today) ?? new Date();
|
|
201
193
|
const month = monthStart(todayDate);
|
|
202
194
|
const marks = buildMonthMarks(events, month);
|
|
203
|
-
//
|
|
204
|
-
//
|
|
195
|
+
// The selected day remains the command date on the calendar grid, while the
|
|
196
|
+
// detail pane shows the two most recently published source posts.
|
|
205
197
|
const selectedDate = today;
|
|
206
198
|
const monthEvents = [...new Map([...marks.values()].flat().map((mark) => [mark.event.id, mark.event])).values()];
|
|
207
199
|
const resetCount = monthEvents.filter((event) => event.type === 'direct_reset').length;
|
|
@@ -279,7 +271,7 @@ function renderCalendarHtml(events, now = new Date()) {
|
|
|
279
271
|
<div class="calendar-grid">${renderCalendarGrid(month, marks, selectedDate, today)}</div>
|
|
280
272
|
<div class="calendar-note">按北京时间记录;发放日期不明确的事件,按来源帖子日期展示。</div>
|
|
281
273
|
</section>
|
|
282
|
-
${renderDetails(
|
|
274
|
+
${renderDetails(events)}
|
|
283
275
|
</main>
|
|
284
276
|
</body>
|
|
285
277
|
</html>`;
|
package/lib/event.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CalendarBrowser } from './calendar';
|
|
2
|
+
import type { LatestResetSummary } from './stats';
|
|
3
|
+
export declare function renderLatestResetHtml(summary: LatestResetSummary, now?: Date): string;
|
|
4
|
+
export declare function renderLatestResetImage(browser: CalendarBrowser, summary: LatestResetSummary, now?: Date): Promise<Buffer>;
|
package/lib/event.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderLatestResetHtml = renderLatestResetHtml;
|
|
4
|
+
exports.renderLatestResetImage = renderLatestResetImage;
|
|
5
|
+
const calendar_1 = require("./calendar");
|
|
6
|
+
function escapeHtml(value) {
|
|
7
|
+
return value
|
|
8
|
+
.replaceAll('&', '&')
|
|
9
|
+
.replaceAll('<', '<')
|
|
10
|
+
.replaceAll('>', '>')
|
|
11
|
+
.replaceAll('"', '"')
|
|
12
|
+
.replaceAll("'", ''');
|
|
13
|
+
}
|
|
14
|
+
function formatDateTime(timestamp) {
|
|
15
|
+
return new Intl.DateTimeFormat('zh-CN', {
|
|
16
|
+
timeZone: 'Asia/Shanghai',
|
|
17
|
+
year: 'numeric',
|
|
18
|
+
month: 'numeric',
|
|
19
|
+
day: 'numeric',
|
|
20
|
+
hour: '2-digit',
|
|
21
|
+
minute: '2-digit',
|
|
22
|
+
hour12: false,
|
|
23
|
+
}).format(new Date(timestamp));
|
|
24
|
+
}
|
|
25
|
+
function formatElapsed(timestamp, now) {
|
|
26
|
+
const elapsedHours = Math.max(0, (now.getTime() - timestamp) / (60 * 60 * 1000));
|
|
27
|
+
if (elapsedHours < 1)
|
|
28
|
+
return `${Math.max(1, Math.round(elapsedHours * 60))} 分钟前`;
|
|
29
|
+
if (elapsedHours < 24)
|
|
30
|
+
return `${Math.round(elapsedHours * 10) / 10} 小时前`;
|
|
31
|
+
return `${Math.round(elapsedHours / 24 * 10) / 10} 天前`;
|
|
32
|
+
}
|
|
33
|
+
function typeLabel(summary) {
|
|
34
|
+
return summary.event.type === 'direct_reset' ? '全员重置' : '重置卡';
|
|
35
|
+
}
|
|
36
|
+
function latestPost(summary) {
|
|
37
|
+
return [...summary.event.posts]
|
|
38
|
+
.sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt))[0] ?? null;
|
|
39
|
+
}
|
|
40
|
+
function renderSourcePost(summary) {
|
|
41
|
+
const post = latestPost(summary);
|
|
42
|
+
if (!post)
|
|
43
|
+
return '<div class="post-empty">暂无来源帖子内容</div>';
|
|
44
|
+
const avatar = (0, calendar_1.getTiboAvatarDataUri)()
|
|
45
|
+
? `<img class="avatar-image" src="${(0, calendar_1.getTiboAvatarDataUri)()}" alt="">`
|
|
46
|
+
: '<span class="avatar-fallback">T</span>';
|
|
47
|
+
const text = post.text || post.originalText;
|
|
48
|
+
const publishedAt = Date.parse(post.publishedAt);
|
|
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>
|
|
58
|
+
</article>`;
|
|
59
|
+
}
|
|
60
|
+
function renderLatestResetHtml(summary, now = new Date()) {
|
|
61
|
+
const approximate = summary.approximate ? '时间按公开日期估算' : '已记录精确确认时间';
|
|
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>
|
|
130
|
+
</html>`;
|
|
131
|
+
}
|
|
132
|
+
async function renderLatestResetImage(browser, summary, now = new Date()) {
|
|
133
|
+
let page = null;
|
|
134
|
+
try {
|
|
135
|
+
page = await browser.page();
|
|
136
|
+
await page.setViewport({ width: 1120, height: 570, deviceScaleFactor: 1 });
|
|
137
|
+
await page.setContent(renderLatestResetHtml(summary, now), { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
138
|
+
const image = await page.screenshot({ type: 'png', fullPage: true });
|
|
139
|
+
return Buffer.from(image);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
if (page)
|
|
143
|
+
await page.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
package/lib/history.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { CodexResetEvent } from './types';
|
|
2
2
|
import { CalendarBrowser } from './calendar';
|
|
3
|
+
export type HistoryRange = number | 'all';
|
|
4
|
+
export declare function filterHistoryEvents(events: CodexResetEvent[], now?: Date, range?: HistoryRange): CodexResetEvent[];
|
|
3
5
|
export declare function renderHistoryHtml(events: CodexResetEvent[], now?: Date): string;
|
|
4
6
|
export declare function renderHistoryImage(browser: CalendarBrowser, events: CodexResetEvent[], now?: Date): Promise<Buffer>;
|
package/lib/history.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filterHistoryEvents = filterHistoryEvents;
|
|
3
4
|
exports.renderHistoryHtml = renderHistoryHtml;
|
|
4
5
|
exports.renderHistoryImage = renderHistoryImage;
|
|
5
6
|
const calendar_1 = require("./calendar");
|
|
@@ -31,6 +32,24 @@ function eventTimestamp(event) {
|
|
|
31
32
|
}
|
|
32
33
|
return 0;
|
|
33
34
|
}
|
|
35
|
+
function filterHistoryEvents(events, now = new Date(), range = 1) {
|
|
36
|
+
if (range === 'all')
|
|
37
|
+
return [...events];
|
|
38
|
+
const months = Math.max(1, Math.floor(Math.abs(range)));
|
|
39
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
40
|
+
timeZone: 'Asia/Shanghai',
|
|
41
|
+
year: 'numeric',
|
|
42
|
+
month: 'numeric',
|
|
43
|
+
day: 'numeric',
|
|
44
|
+
}).formatToParts(now);
|
|
45
|
+
const values = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
|
46
|
+
const cutoff = Date.UTC(Number(values.year), Number(values.month) - 1 - months, Number(values.day), -8);
|
|
47
|
+
const current = now.getTime();
|
|
48
|
+
return events.filter((event) => {
|
|
49
|
+
const timestamp = eventTimestamp(event);
|
|
50
|
+
return timestamp >= cutoff && timestamp <= current;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
34
53
|
function dateLabel(timestamp) {
|
|
35
54
|
const parts = new Intl.DateTimeFormat('zh-CN', {
|
|
36
55
|
timeZone: 'Asia/Shanghai',
|
|
@@ -176,7 +195,7 @@ function renderHistoryHtml(events, now = new Date()) {
|
|
|
176
195
|
<div class="stat"><span>已确认</span><strong>${confirmed.length}</strong></div>
|
|
177
196
|
<div class="stat"><span>全员重置</span><strong>${directResetCount}</strong></div>
|
|
178
197
|
<div class="stat"><span>重置卡</span><strong>${resetCreditCount}</strong></div>
|
|
179
|
-
<div class="stat"><span
|
|
198
|
+
<div class="stat"><span>未重置预告</span><strong>${announcedCount}</strong></div>
|
|
180
199
|
<p class="summary-note">数据来自 AIHOT 公开快照。时间线仅展示公开事件,不代表账户一定获得重置。</p>
|
|
181
200
|
</aside>
|
|
182
201
|
</div>
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,8 @@ 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 event_1 = require("./event");
|
|
9
|
+
const probability_1 = require("./probability");
|
|
8
10
|
const stats_1 = require("./stats");
|
|
9
11
|
exports.name = 'codex-reset';
|
|
10
12
|
exports.inject = {
|
|
@@ -39,15 +41,6 @@ exports.Config = koishi_1.Schema.object({
|
|
|
39
41
|
mentionAll: koishi_1.Schema.boolean()
|
|
40
42
|
.default(false)
|
|
41
43
|
.description('推送通知时是否艾特全体。需要平台和频道支持全体艾特。'),
|
|
42
|
-
enableProbabilityCommand: koishi_1.Schema.boolean()
|
|
43
|
-
.default(true)
|
|
44
|
-
.description('启用 codex-reset.probability(别名:重置概率)。'),
|
|
45
|
-
enableCalendarCommand: koishi_1.Schema.boolean()
|
|
46
|
-
.default(true)
|
|
47
|
-
.description('启用日历图片命令,需要 Puppeteer 服务生成 PNG。'),
|
|
48
|
-
enableHistoryCommand: koishi_1.Schema.boolean()
|
|
49
|
-
.default(true)
|
|
50
|
-
.description('启用历史时间线图片命令,需要 Puppeteer 服务生成 PNG。'),
|
|
51
44
|
}).i18n({
|
|
52
45
|
'zh-CN': {
|
|
53
46
|
apiUrl: '监控接口地址',
|
|
@@ -61,9 +54,6 @@ exports.Config = koishi_1.Schema.object({
|
|
|
61
54
|
pushAnnouncements: '推送重置预告',
|
|
62
55
|
pushConfirmedResets: '推送已确认重置',
|
|
63
56
|
mentionAll: '艾特全体',
|
|
64
|
-
enableProbabilityCommand: '启用重置概率命令',
|
|
65
|
-
enableCalendarCommand: '启用重置日历命令',
|
|
66
|
-
enableHistoryCommand: '启用重置历史命令',
|
|
67
57
|
},
|
|
68
58
|
});
|
|
69
59
|
function isRecord(value) {
|
|
@@ -228,12 +218,24 @@ class CodexResetMonitor {
|
|
|
228
218
|
return null;
|
|
229
219
|
return (0, calendar_1.renderCalendarImage)(browser, snapshot.events);
|
|
230
220
|
}
|
|
231
|
-
async createHistoryImage() {
|
|
221
|
+
async createHistoryImage(events, now = new Date()) {
|
|
232
222
|
const snapshot = await this.ensureSnapshot();
|
|
233
223
|
const browser = this.ctx.puppeteer;
|
|
234
224
|
if (!snapshot || !browser)
|
|
235
225
|
return null;
|
|
236
|
-
return (0, history_1.renderHistoryImage)(browser, snapshot.events);
|
|
226
|
+
return (0, history_1.renderHistoryImage)(browser, events ?? snapshot.events, now);
|
|
227
|
+
}
|
|
228
|
+
async createProbabilityImage(summary) {
|
|
229
|
+
const browser = this.ctx.puppeteer;
|
|
230
|
+
if (!browser)
|
|
231
|
+
return null;
|
|
232
|
+
return (0, probability_1.renderProbabilityImage)(browser, summary);
|
|
233
|
+
}
|
|
234
|
+
async createLatestResetImage(summary, now = new Date()) {
|
|
235
|
+
const browser = this.ctx.puppeteer;
|
|
236
|
+
if (!browser)
|
|
237
|
+
return null;
|
|
238
|
+
return (0, event_1.renderLatestResetImage)(browser, summary, now);
|
|
237
239
|
}
|
|
238
240
|
async fetchSnapshot(ignoreEtag = false) {
|
|
239
241
|
const controller = new AbortController();
|
|
@@ -241,7 +243,7 @@ class CodexResetMonitor {
|
|
|
241
243
|
try {
|
|
242
244
|
const headers = {
|
|
243
245
|
Accept: 'application/json',
|
|
244
|
-
'User-Agent': 'koishi-plugin-codex-reset/0.1.
|
|
246
|
+
'User-Agent': 'koishi-plugin-codex-reset/0.1.3',
|
|
245
247
|
};
|
|
246
248
|
if (!ignoreEtag && this.etag && this.snapshot)
|
|
247
249
|
headers['If-None-Match'] = this.etag;
|
|
@@ -306,12 +308,22 @@ function apply(ctx, config) {
|
|
|
306
308
|
ctx.command('codex.probability', '分析 Codex 重置事件的历史概率')
|
|
307
309
|
.alias('重置概率')
|
|
308
310
|
.action(async () => {
|
|
309
|
-
if (!config.enableProbabilityCommand)
|
|
310
|
-
return '重置概率命令已关闭。';
|
|
311
311
|
const snapshot = await monitor.ensureSnapshot();
|
|
312
312
|
if (!snapshot)
|
|
313
313
|
return '暂时无法获取 Codex 重置事件数据,请稍后再试。';
|
|
314
|
-
|
|
314
|
+
const summary = (0, stats_1.analyzeProbability)(snapshot);
|
|
315
|
+
if (!monitor.hasPuppeteer)
|
|
316
|
+
return (0, stats_1.formatProbability)(summary);
|
|
317
|
+
try {
|
|
318
|
+
const image = await monitor.createProbabilityImage(summary);
|
|
319
|
+
if (!image)
|
|
320
|
+
return (0, stats_1.formatProbability)(summary);
|
|
321
|
+
return koishi_1.h.image(image, 'image/png');
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
ctx.logger('codex-reset').warn(`生成概率卡片失败:${error instanceof Error ? error.message : String(error)}`);
|
|
325
|
+
return (0, stats_1.formatProbability)(summary);
|
|
326
|
+
}
|
|
315
327
|
});
|
|
316
328
|
ctx.command('codex.event', '查看上一次 Codex 重置距今多久')
|
|
317
329
|
.alias('最近重置')
|
|
@@ -319,13 +331,26 @@ function apply(ctx, config) {
|
|
|
319
331
|
const snapshot = await monitor.ensureSnapshot();
|
|
320
332
|
if (!snapshot)
|
|
321
333
|
return '暂时无法获取 Codex 重置事件数据,请稍后再试。';
|
|
322
|
-
|
|
334
|
+
const now = new Date();
|
|
335
|
+
const latest = (0, stats_1.findLatestConfirmedReset)(snapshot, now);
|
|
336
|
+
if (!latest)
|
|
337
|
+
return (0, stats_1.formatLatestReset)(snapshot, now);
|
|
338
|
+
if (!monitor.hasPuppeteer)
|
|
339
|
+
return (0, stats_1.formatLatestReset)(snapshot, now);
|
|
340
|
+
try {
|
|
341
|
+
const image = await monitor.createLatestResetImage(latest, now);
|
|
342
|
+
if (!image)
|
|
343
|
+
return (0, stats_1.formatLatestReset)(snapshot, now);
|
|
344
|
+
return koishi_1.h.image(image, 'image/png');
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
ctx.logger('codex-reset').warn(`生成最近重置卡片失败:${error instanceof Error ? error.message : String(error)}`);
|
|
348
|
+
return (0, stats_1.formatLatestReset)(snapshot, now);
|
|
349
|
+
}
|
|
323
350
|
});
|
|
324
351
|
ctx.command('codex.calendar', '发送本月的 Codex 重置日历图片')
|
|
325
352
|
.alias('重置日历')
|
|
326
353
|
.action(async () => {
|
|
327
|
-
if (!config.enableCalendarCommand)
|
|
328
|
-
return '重置日历命令已关闭。';
|
|
329
354
|
if (!monitor.hasPuppeteer)
|
|
330
355
|
return '发送日历图片需要先安装并启用 koishi-plugin-puppeteer。';
|
|
331
356
|
try {
|
|
@@ -339,15 +364,22 @@ function apply(ctx, config) {
|
|
|
339
364
|
return '日历图片生成失败,请检查 Puppeteer 服务和中文字体。';
|
|
340
365
|
}
|
|
341
366
|
});
|
|
342
|
-
ctx.command('codex.history', '发送 Codex 重置历史时间线图片')
|
|
367
|
+
ctx.command('codex.history [range:number]', '发送 Codex 重置历史时间线图片')
|
|
343
368
|
.alias('重置历史')
|
|
344
|
-
.
|
|
345
|
-
|
|
346
|
-
return '重置历史命令已关闭。';
|
|
369
|
+
.option('all', '-a 查看全部历史')
|
|
370
|
+
.action(async (argv, range) => {
|
|
347
371
|
if (!monitor.hasPuppeteer)
|
|
348
372
|
return '发送历史时间线图片需要先安装并启用 koishi-plugin-puppeteer。';
|
|
349
373
|
try {
|
|
350
|
-
const
|
|
374
|
+
const snapshot = await monitor.ensureSnapshot();
|
|
375
|
+
if (!snapshot)
|
|
376
|
+
return '暂时无法获取 Codex 重置事件数据,请稍后再试。';
|
|
377
|
+
const now = new Date();
|
|
378
|
+
const historyRange = argv.options?.all
|
|
379
|
+
? 'all'
|
|
380
|
+
: Math.max(1, Math.min(120, Math.floor(Math.abs(range ?? 1))));
|
|
381
|
+
const events = (0, history_1.filterHistoryEvents)(snapshot.events, now, historyRange);
|
|
382
|
+
const image = await monitor.createHistoryImage(events, now);
|
|
351
383
|
if (!image)
|
|
352
384
|
return '暂时无法生成历史时间线图片,请稍后再试。';
|
|
353
385
|
return koishi_1.h.image(image, 'image/png');
|
|
@@ -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('有效样本不足,暂时无法估算概率。');
|
package/lib/types.d.ts
CHANGED