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 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
- * 用 place+magnitude+分钟桶 作为指纹,窗口期内同一事件只推一次
109
+ * 地震预警按报次去重,允许后续修正继续推送;震后测定按内容去重,同一事件只推一次。
110
110
  */
111
111
  export declare class EventDeduplicator {
112
112
  private seen;
113
- private windowMs;
114
- constructor(windowMs?: number);
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
- * 用 place+magnitude+分钟桶 作为指纹,窗口期内同一事件只推一次
58
+ * 地震预警按报次去重,允许后续修正继续推送;震后测定按内容去重,同一事件只推一次。
59
59
  */
60
60
  class EventDeduplicator {
61
- constructor(windowMs = 8 * 60 * 1000) {
62
- // fingerprint -> first-seen timestamp (ms)
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.windowMs = windowMs;
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.EARTHQUAKE || event.disaster_type === DisasterType.EARTHQUAKE_WARNING) {
78
- // 优先用 event_id,相同机构的多数据源共享同一 event_id
79
- if (data.event_id)
80
- return `eq:${data.event_id}`;
81
- // 降级:地点 + 震级 + 分钟桶
82
- const bucket = data.shock_time ? data.shock_time.slice(0, 16) : 'unknown';
83
- return `eq:${data.place_name}|${data.magnitude?.toFixed(1)}|${bucket}`;
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 cutoff = Date.now() - this.windowMs;
90
- for (const [fp, ts] of this.seen) {
91
- if (ts < cutoff)
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 ? '【最终报】' : `【第${data.updates}报】`;
63
+ const finalStr = data.is_final ? ' 【最终报】' : '';
64
64
  const cancelStr = data.is_cancel ? '【已取消】' : '';
65
- let msg = `${cancelStr}${type} ${finalStr}\n`;
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`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-disaster-warning",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Koishi 灾害预警插件,支持多数据源(地震、海啸、气象预警)",
5
5
  "contributors": [
6
6
  "lumia.wang <fenglian19980510@gmail.com>"