koishi-plugin-disaster-warning 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/lib/index.js +2 -2
- package/lib/models.d.ts +10 -3
- package/lib/models.js +55 -15
- package/lib/pusher.js +2 -2
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -15,8 +15,8 @@ exports.Config = koishi_1.Schema.object({
|
|
|
15
15
|
.default([])
|
|
16
16
|
.description('推送目标群号列表,直接填写群号即可,例如 123456789'),
|
|
17
17
|
data_types: koishi_1.Schema.object({
|
|
18
|
-
earthquake_warning: koishi_1.Schema.boolean().default(true).description('
|
|
19
|
-
earthquake_info: koishi_1.Schema.boolean().default(true).description('
|
|
18
|
+
earthquake_warning: koishi_1.Schema.boolean().default(true).description('地震预警(实时速报,震前预警,会持续推送刷屏)'),
|
|
19
|
+
earthquake_info: koishi_1.Schema.boolean().default(true).description('地震信息(震后测定报告,同一事件只推一次)'),
|
|
20
20
|
tsunami_warning: koishi_1.Schema.boolean().default(true).description('海啸预警'),
|
|
21
21
|
weather_alarm: koishi_1.Schema.boolean().default(false).description('气象预警(中国)'),
|
|
22
22
|
}).description('接收的灾害类型'),
|
package/lib/models.d.ts
CHANGED
|
@@ -106,14 +106,21 @@ export interface DisasterEvent {
|
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
108
|
* 跨数据源事件去重器
|
|
109
|
-
*
|
|
109
|
+
* 地震预警按报次去重,允许后续修正继续推送;震后测定按内容去重,同一事件只推一次。
|
|
110
110
|
*/
|
|
111
111
|
export declare class EventDeduplicator {
|
|
112
112
|
private seen;
|
|
113
|
-
private
|
|
114
|
-
|
|
113
|
+
private warningWindowMs;
|
|
114
|
+
private earthquakeInfoWindowMs;
|
|
115
|
+
constructor(warningWindowMs?: number, earthquakeInfoWindowMs?: number);
|
|
115
116
|
/** 返回 true 表示已见过(应丢弃),false 表示首次(应推送) */
|
|
116
117
|
isDuplicate(event: DisasterEvent): boolean;
|
|
117
118
|
private fingerprint;
|
|
119
|
+
private ttlFor;
|
|
120
|
+
private earthquakeInfoFingerprint;
|
|
121
|
+
private fallbackEarthquakeId;
|
|
122
|
+
private normalizeLocation;
|
|
123
|
+
private normalizeTime;
|
|
124
|
+
private normalizeNumber;
|
|
118
125
|
private evict;
|
|
119
126
|
}
|
package/lib/models.js
CHANGED
|
@@ -55,13 +55,14 @@ function getDataSourceFromId(id) {
|
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
57
|
* 跨数据源事件去重器
|
|
58
|
-
*
|
|
58
|
+
* 地震预警按报次去重,允许后续修正继续推送;震后测定按内容去重,同一事件只推一次。
|
|
59
59
|
*/
|
|
60
60
|
class EventDeduplicator {
|
|
61
|
-
constructor(
|
|
62
|
-
// fingerprint ->
|
|
61
|
+
constructor(warningWindowMs = 8 * 60 * 1000, earthquakeInfoWindowMs = 7 * 24 * 60 * 60 * 1000) {
|
|
62
|
+
// fingerprint -> expire timestamp (ms)
|
|
63
63
|
this.seen = new Map();
|
|
64
|
-
this.
|
|
64
|
+
this.warningWindowMs = warningWindowMs;
|
|
65
|
+
this.earthquakeInfoWindowMs = earthquakeInfoWindowMs;
|
|
65
66
|
}
|
|
66
67
|
/** 返回 true 表示已见过(应丢弃),false 表示首次(应推送) */
|
|
67
68
|
isDuplicate(event) {
|
|
@@ -69,26 +70,65 @@ class EventDeduplicator {
|
|
|
69
70
|
const fp = this.fingerprint(event);
|
|
70
71
|
if (this.seen.has(fp))
|
|
71
72
|
return true;
|
|
72
|
-
this.seen.set(fp, Date.now());
|
|
73
|
+
this.seen.set(fp, Date.now() + this.ttlFor(event));
|
|
73
74
|
return false;
|
|
74
75
|
}
|
|
75
76
|
fingerprint(event) {
|
|
76
77
|
const data = event.data;
|
|
77
|
-
if (event.disaster_type === DisasterType.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
if (event.disaster_type === DisasterType.EARTHQUAKE_WARNING) {
|
|
79
|
+
const eventId = data.event_id || this.fallbackEarthquakeId(data);
|
|
80
|
+
const revision = data.revision ?? data.report_num ?? data.serial ?? data.updates ?? 'unknown';
|
|
81
|
+
const status = data.is_cancel ? 'cancel' : data.is_final ? 'final' : 'update';
|
|
82
|
+
return `eqw:${eventId}|${revision}|${status}`;
|
|
83
|
+
}
|
|
84
|
+
if (event.disaster_type === DisasterType.EARTHQUAKE) {
|
|
85
|
+
return `eq:${this.earthquakeInfoFingerprint(data)}`;
|
|
84
86
|
}
|
|
85
87
|
// 海啸/气象用 id 即可
|
|
86
88
|
return `${event.disaster_type}:${event.id}`;
|
|
87
89
|
}
|
|
90
|
+
ttlFor(event) {
|
|
91
|
+
return event.disaster_type === DisasterType.EARTHQUAKE
|
|
92
|
+
? this.earthquakeInfoWindowMs
|
|
93
|
+
: this.warningWindowMs;
|
|
94
|
+
}
|
|
95
|
+
earthquakeInfoFingerprint(data) {
|
|
96
|
+
const originTime = this.normalizeTime(data.shock_time);
|
|
97
|
+
const location = this.normalizeLocation(data);
|
|
98
|
+
const magnitude = this.normalizeNumber(data.magnitude, 1);
|
|
99
|
+
const depth = this.normalizeNumber(data.depth, 1);
|
|
100
|
+
const intensity = this.normalizeNumber(data.intensity, 1);
|
|
101
|
+
const scale = this.normalizeNumber(data.scale, 1);
|
|
102
|
+
return `${location}|${originTime}|M${magnitude}|D${depth}|I${intensity}|S${scale}`;
|
|
103
|
+
}
|
|
104
|
+
fallbackEarthquakeId(data) {
|
|
105
|
+
const originTime = this.normalizeTime(data.shock_time);
|
|
106
|
+
const location = this.normalizeLocation(data);
|
|
107
|
+
return `${location}|${data.magnitude?.toFixed(1)}|${originTime}`;
|
|
108
|
+
}
|
|
109
|
+
normalizeLocation(data) {
|
|
110
|
+
const place = data.place_name?.replace(/\s+/g, '').trim();
|
|
111
|
+
if (place)
|
|
112
|
+
return place;
|
|
113
|
+
const lat = Number.isFinite(data.latitude) ? data.latitude.toFixed(2) : 'unknown';
|
|
114
|
+
const lon = Number.isFinite(data.longitude) ? data.longitude.toFixed(2) : 'unknown';
|
|
115
|
+
return `${lat},${lon}`;
|
|
116
|
+
}
|
|
117
|
+
normalizeTime(time) {
|
|
118
|
+
if (!time)
|
|
119
|
+
return 'unknown';
|
|
120
|
+
const date = new Date(time);
|
|
121
|
+
if (isNaN(date.getTime()))
|
|
122
|
+
return time.slice(0, 19);
|
|
123
|
+
return date.toISOString().slice(0, 19);
|
|
124
|
+
}
|
|
125
|
+
normalizeNumber(value, digits) {
|
|
126
|
+
return Number.isFinite(value) ? value.toFixed(digits) : 'unknown';
|
|
127
|
+
}
|
|
88
128
|
evict() {
|
|
89
|
-
const
|
|
90
|
-
for (const [fp,
|
|
91
|
-
if (
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
for (const [fp, expiresAt] of this.seen) {
|
|
131
|
+
if (expiresAt < now)
|
|
92
132
|
this.seen.delete(fp);
|
|
93
133
|
}
|
|
94
134
|
}
|
package/lib/pusher.js
CHANGED
|
@@ -60,9 +60,9 @@ class MessagePushManager {
|
|
|
60
60
|
}
|
|
61
61
|
formatEarthquake(data) {
|
|
62
62
|
const type = data.disaster_type === models_1.DisasterType.EARTHQUAKE_WARNING ? '🚨 地震预警' : '📋 地震信息';
|
|
63
|
-
const finalStr = data.is_final ? '【最终报】' :
|
|
63
|
+
const finalStr = data.is_final ? ' 【最终报】' : '';
|
|
64
64
|
const cancelStr = data.is_cancel ? '【已取消】' : '';
|
|
65
|
-
let msg = `${cancelStr}${type}
|
|
65
|
+
let msg = `${cancelStr}${type}${finalStr}\n`;
|
|
66
66
|
msg += `📍 震源:${data.place_name}\n`;
|
|
67
67
|
msg += `🕐 时间:${this.formatTime(data.shock_time)}\n`;
|
|
68
68
|
msg += `📊 震级:M${data.magnitude?.toFixed(1) || '未知'}\n`;
|