koishi-plugin-disaster-warning 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 CHANGED
@@ -1,61 +1,69 @@
1
- # koishi-plugin-disaster-warning
2
-
3
- [![npm](https://img.shields.io/npm/v/koishi-plugin-disaster-warning?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-disaster-warning)
4
-
5
- Koishi 灾害预警插件,支持多数据源(地震、海啸、气象预警)实时推送。
6
- 本插件移植自 [astrbot_plugin_disaster_warning](https://github.com/DBJD-CR/astrbot_plugin_disaster_warning),感谢原作者的开源贡献。
7
-
8
- ## ✨ 功能特性
9
-
10
- ### 🌍 多数据源支持
11
- 插件支持多个可自由选择启用的细粒度数据源,覆盖全球主要地震监测机构:
12
- - **中国地震网地震预警 (FAN Studio / Wolfx)** - 实时地震预警信息。
13
- - **台湾中央气象署强震即时警报 (FAN Studio / Wolfx)** - 台湾地区地震预警。
14
- - **日本气象厅紧急地震速报 (P2P / Wolfx / FAN Studio)** - 日本紧急地震速报。
15
- - **中国地震台网地震测定 (FAN Studio / Wolfx)** - 正式地震测定信息。
16
- - **日本气象厅地震情报 (P2P / Wolfx)** - 详细地震情报。
17
- - **USGS地震测定 (FAN Studio)** - 美国地质调查局地震信息。
18
- - **Global Quake服务器** - 全球地震测站实时计算推送,精度有限。
19
- - **中国气象局气象预警 (FAN Studio)** - 气象灾害预警。
20
- - **自然资源部海啸预警中心 (FAN Studio)** - 海啸预警信息。
21
- - **日本气象厅海啸预报 (P2P)** - 日本海啸预报信息。
22
-
23
- ### 🎯 智能推送控制
24
- - **阈值过滤** - 根据震级、烈度、震度设置推送阈值。
25
- - **频率控制** - 智能推送逻辑,避免短时间内刷屏。
26
- - **首报推送保证** - 确保预警信息首次下达时总是推送。
27
- - **最终报保证** - 确保最终报总是推送。
28
-
29
- ### 🔁 事件去重功能
30
- 插件具备基础的事件去重功能,防止同一地震被同一个数据源重复推送。
31
-
32
- ## 🚀 安装与使用
33
-
34
- ### 安装
35
-
36
- ```bash
37
- npm install koishi-plugin-disaster-warning
38
- ```
39
-
40
- 或者在 Koishi 插件市场搜索 `disaster-warning` 安装。
41
-
42
- ### 配置
43
-
44
- 安装完成后,在 Koishi 控制台的「插件配置」页面进行配置:
45
-
46
- 1. **启用插件**:确保 `enabled` 为 `true`。
47
- 2. **目标群组**:在 `target_groups` 中添加需要推送预警消息的群号。
48
- 3. **数据源配置**:在 `data_sources` 中按需启用或禁用各个数据源。
49
- 4. **过滤器配置**:在 `earthquake_filters` 中设置震级和烈度阈值,避免过多打扰。
50
-
51
- ## 📋 使用命令
52
-
53
- 插件提供以下指令:
54
-
55
- - `disaster` - 查看插件帮助信息
56
- - `disaster.test` - 发送一条测试预警消息,用于验证配置是否生效
57
- - `disaster.history` - 查看最近的地震记录 (数据来源:CENC)
58
-
59
- ## 📄 许可证
60
-
61
- 本项目采用 [AGPL-3.0](./LICENSE) 许可证。
1
+ # koishi-plugin-disaster-warning
2
+
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-disaster-warning?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-disaster-warning)
4
+
5
+ Koishi 灾害预警插件,支持多数据源(地震、海啸、气象预警)实时推送。
6
+ 本插件移植自 [astrbot_plugin_disaster_warning](https://github.com/DBJD-CR/astrbot_plugin_disaster_warning),感谢原作者的开源贡献。
7
+
8
+ ## ✨ 功能特性
9
+
10
+ ### 🌍 多数据源支持
11
+ 插件支持多个可自由选择启用的细粒度数据源,覆盖全球主要地震监测机构:
12
+ - **中国地震网地震预警 (FAN Studio / Wolfx)** - 实时地震预警信息。
13
+ - **台湾中央气象署强震即时警报 (FAN Studio / Wolfx)** - 台湾地区地震预警。
14
+ - **日本气象厅紧急地震速报 (P2P / Wolfx / FAN Studio)** - 日本紧急地震速报。
15
+ - **中国地震台网地震测定 (FAN Studio / Wolfx)** - 正式地震测定信息。
16
+ - **日本气象厅地震情报 (P2P / Wolfx)** - 详细地震情报。
17
+ - **USGS地震测定 (FAN Studio)** - 美国地质调查局地震信息。
18
+ - **Global Quake服务器** - 全球地震测站实时计算推送,精度有限。
19
+ - **中国气象局气象预警 (FAN Studio)** - 气象灾害预警。
20
+ - **自然资源部海啸预警中心 (FAN Studio)** - 海啸预警信息。
21
+ - **日本气象厅海啸预报 (P2P)** - 日本海啸预报信息。
22
+
23
+ ### 🎯 智能推送控制
24
+ - **阈值过滤** - 根据震级、烈度、震度设置推送阈值。
25
+ - **频率控制** - 智能推送逻辑,避免短时间内刷屏。
26
+ - **首报推送保证** - 确保预警信息首次下达时总是推送。
27
+ - **震中地区过滤** - 地震类事件会按震中地名与经纬度过滤,关闭全球地区后,CENC 等数据源发布的海外地震不会再误推送。
28
+ - **最终报保证** - 确保最终报总是推送。
29
+
30
+ ### 🔁 事件去重功能
31
+ 插件具备基础的事件去重功能,防止同一地震被同一个数据源重复推送。
32
+
33
+ ## 🚀 安装与使用
34
+
35
+ ### 安装
36
+
37
+ ```bash
38
+ npm install koishi-plugin-disaster-warning
39
+ ```
40
+
41
+ 或者在 Koishi 插件市场搜索 `disaster-warning` 安装。
42
+
43
+ ### 配置
44
+
45
+ 安装完成后,在 Koishi 控制台的「插件配置」页面进行配置:
46
+
47
+ 1. **启用插件**:确保 `enabled` 为 `true`。
48
+ 2. **目标群组**:在 `target_groups` 中添加需要推送预警消息的群号。
49
+ 3. **灾害类型**:在 `data_types` 中选择是否接收地震预警、地震信息、海啸预警、气象预警。
50
+ 4. **接收地区**:在 `regions` 中选择中国大陆、台湾、日本或全球。数据源连接会依据此项自动开启,地震类事件还会按震中位置再次过滤。
51
+ 5. **过滤器配置**:在 `filter` 中设置震级、烈度、震度阈值,避免过多打扰。
52
+
53
+ ### 地区过滤说明
54
+
55
+ `regions` 同时控制数据源连接与地震震中地区过滤。例如仅开启中国大陆和台湾、关闭日本与全球时,插件仍可连接 CENC/CWA 等相关数据源,但智利、印尼等不在已开启地区内的地震会被丢弃,不会因为由 CENC 发布就被推送。
56
+
57
+ 当前震中识别优先使用地名关键词,其次使用经纬度范围;无法识别且带有非空地名的地震默认视为全球事件。
58
+
59
+ ## 📋 使用命令
60
+
61
+ 插件提供以下指令:
62
+
63
+ - `disaster` - 查看插件帮助信息
64
+ - `disaster.test` - 发送一条测试预警消息,用于验证配置是否生效
65
+ - `disaster.history` - 查看最近的地震记录 (数据来源:CENC)
66
+
67
+ ## 📄 许可证
68
+
69
+ 本项目采用 [AGPL-3.0](./LICENSE) 许可证。
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('接收的灾害类型'),
@@ -25,7 +25,7 @@ exports.Config = koishi_1.Schema.object({
25
25
  taiwan: koishi_1.Schema.boolean().default(true).description('台湾(CWA预警与地震报告)'),
26
26
  japan: koishi_1.Schema.boolean().default(true).description('日本(JMA EEW / P2P地震情报 / 海啸)'),
27
27
  global: koishi_1.Schema.boolean().default(false).description('全球(USGS 地震信息 / GlobalQuake 实时预警)'),
28
- }).description('接收的地区(数据源连接将依据此项自动开启)'),
28
+ }).description('接收的地区(数据源连接将依据此项自动开启,地震类事件会按震中位置过滤)'),
29
29
  filter: koishi_1.Schema.object({
30
30
  min_magnitude_absolute: koishi_1.Schema.number().default(3.0).description('绝对过滤震级:低于此震级直接丢弃(不推送)'),
31
31
  min_magnitude_for_push: koishi_1.Schema.number().default(4.0).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/lib/service.d.ts CHANGED
@@ -19,6 +19,20 @@ export declare class DisasterWarningService {
19
19
  private startWolfxHttpPoller;
20
20
  private handleEvent;
21
21
  private shouldPushEvent;
22
+ private shouldPushEarthquakeByLocation;
23
+ private detectEarthquakeRegion;
24
+ private detectRegionByPlace;
25
+ private includesAny;
26
+ private validLatitude;
27
+ private validLongitude;
28
+ private isTaiwanCoordinate;
29
+ private isJapanCoordinate;
30
+ private isMainlandChinaCoordinate;
31
+ private pointInPolygon;
32
+ private fallbackRegionForMissingCoordinates;
33
+ private shouldAllowUnknownEarthquakeRegion;
34
+ private fallbackRegionBySource;
35
+ private isRegionEnabled;
22
36
  getStatus(): Record<string, {
23
37
  connected: boolean;
24
38
  retryCount: number;
package/lib/service.js CHANGED
@@ -210,8 +210,192 @@ class DisasterWarningService {
210
210
  return false;
211
211
  if (globalSources.includes(src) && !regions.global)
212
212
  return false;
213
+ if ((isEarthquakeWarning || isEarthquakeInfo) && !this.shouldPushEarthquakeByLocation(event)) {
214
+ const data = event.data;
215
+ logger.debug(`[region] Skipping earthquake outside enabled regions: ${data.place_name || event.id}`);
216
+ return false;
217
+ }
213
218
  return true;
214
219
  }
220
+ shouldPushEarthquakeByLocation(event) {
221
+ const region = this.detectEarthquakeRegion(event.data, event.source);
222
+ switch (region) {
223
+ case 'china':
224
+ return this.config.regions.china;
225
+ case 'taiwan':
226
+ return this.config.regions.taiwan;
227
+ case 'japan':
228
+ return this.config.regions.japan;
229
+ case 'global':
230
+ return this.config.regions.global;
231
+ case 'unknown':
232
+ return this.shouldAllowUnknownEarthquakeRegion(event.source);
233
+ }
234
+ }
235
+ detectEarthquakeRegion(data, source) {
236
+ const place = data.place_name?.replace(/\s+/g, '').toLowerCase() || '';
237
+ const placeRegion = this.detectRegionByPlace(place);
238
+ if (placeRegion)
239
+ return placeRegion;
240
+ const lat = this.validLatitude(data.latitude);
241
+ const lon = this.validLongitude(data.longitude);
242
+ if (lat === undefined || lon === undefined)
243
+ return this.fallbackRegionForMissingCoordinates(data, source, place);
244
+ // Many upstream cancellation packets use 0,0 when coordinates are unavailable.
245
+ if (lat === 0 && lon === 0)
246
+ return this.fallbackRegionForMissingCoordinates(data, source, place);
247
+ if (this.isTaiwanCoordinate(lat, lon))
248
+ return 'taiwan';
249
+ if (this.isJapanCoordinate(lat, lon))
250
+ return 'japan';
251
+ if (this.isMainlandChinaCoordinate(lat, lon))
252
+ return 'china';
253
+ return 'global';
254
+ }
255
+ detectRegionByPlace(place) {
256
+ if (!place)
257
+ return null;
258
+ if (this.includesAny(place, [
259
+ '台湾', '臺灣', '台灣', 'taiwan', '花莲', '花蓮', '宜兰', '宜蘭', '台东', '臺東', '台東',
260
+ '台北', '臺北', '新北', '桃园', '桃園', '台中', '臺中', '台南', '臺南', '高雄', '嘉义', '嘉義',
261
+ '屏东', '屏東', '南投', '澎湖'
262
+ ]))
263
+ return 'taiwan';
264
+ if (this.includesAny(place, [
265
+ '日本', 'japan', '北海道', '本州', '四国', '四國', '九州', '沖縄', '冲绳', '琉球', '小笠原',
266
+ '伊豆', '鳥島', '鸟岛', '青森', '岩手', '宮城', '宫城', '秋田', '山形', '福島', '福岛',
267
+ '茨城', '栃木', '群馬', '群马', '埼玉', '千葉', '千叶', '東京', '东京', '神奈川', '新潟',
268
+ '富山', '石川', '福井', '山梨', '長野', '长野', '岐阜', '静岡', '静冈', '愛知', '爱知',
269
+ '三重', '滋賀', '滋贺', '京都', '大阪', '兵庫', '兵库', '奈良', '和歌山', '鳥取', '鸟取',
270
+ '島根', '岛根', '岡山', '冈山', '広島', '广岛', '山口', '徳島', '德岛', '香川', '愛媛',
271
+ '爱媛', '高知', '福岡', '福冈', '佐賀', '佐贺', '長崎', '长崎', '熊本', '大分', '宮崎',
272
+ '宫崎', '鹿児島', '鹿儿岛'
273
+ ]))
274
+ return 'japan';
275
+ if (this.includesAny(place, [
276
+ '中国', 'china', '北京', '天津', '河北', '山西', '内蒙古', '辽宁', '遼寧', '吉林', '黑龙江',
277
+ '黑龍江', '上海', '江苏', '江蘇', '浙江', '安徽', '福建', '江西', '山东', '山東', '河南',
278
+ '湖北', '湖南', '广东', '廣東', '广西', '廣西', '海南', '重庆', '重慶', '四川', '贵州',
279
+ '貴州', '云南', '雲南', '西藏', '陕西', '陝西', '甘肃', '甘肅', '青海', '宁夏', '寧夏',
280
+ '新疆', '香港', '澳门', '澳門'
281
+ ]))
282
+ return 'china';
283
+ return null;
284
+ }
285
+ includesAny(text, keywords) {
286
+ return keywords.some((keyword) => text.includes(keyword.toLowerCase()));
287
+ }
288
+ validLatitude(value) {
289
+ return Number.isFinite(value) && value >= -90 && value <= 90 ? value : undefined;
290
+ }
291
+ validLongitude(value) {
292
+ return Number.isFinite(value) && value >= -180 && value <= 180 ? value : undefined;
293
+ }
294
+ isTaiwanCoordinate(lat, lon) {
295
+ return lat >= 21.5 && lat <= 26.5 && lon >= 119 && lon <= 123.8;
296
+ }
297
+ isJapanCoordinate(lat, lon) {
298
+ const mainIslands = lat >= 30 && lat <= 46.5 && lon >= 129 && lon <= 146.5;
299
+ const ryukyu = lat >= 24 && lat < 30 && lon >= 122 && lon <= 131.5;
300
+ const izuOgasawara = lat >= 20 && lat <= 34 && lon >= 139 && lon <= 154.5;
301
+ return mainIslands || ryukyu || izuOgasawara;
302
+ }
303
+ isMainlandChinaCoordinate(lat, lon) {
304
+ if (lat >= 18 && lat <= 20.5 && lon >= 108 && lon <= 111.5)
305
+ return true;
306
+ // Rough mainland China outline. Place-name detection runs first, this is only a coordinate fallback.
307
+ return this.pointInPolygon(lon, lat, [
308
+ [73.5, 39.5],
309
+ [74.9, 37.2],
310
+ [75.5, 35.4],
311
+ [78.4, 32.5],
312
+ [78.7, 30.2],
313
+ [80.5, 30.0],
314
+ [81.8, 28.5],
315
+ [85.2, 28.3],
316
+ [88.9, 27.3],
317
+ [91.0, 27.8],
318
+ [93.3, 28.7],
319
+ [95.5, 28.2],
320
+ [97.3, 23.8],
321
+ [98.9, 24.1],
322
+ [100.1, 21.4],
323
+ [101.8, 21.2],
324
+ [103.8, 22.5],
325
+ [106.8, 20.0],
326
+ [108.7, 18.2],
327
+ [111.3, 18.0],
328
+ [113.7, 21.8],
329
+ [116.7, 22.7],
330
+ [118.5, 24.5],
331
+ [121.8, 29.0],
332
+ [122.2, 31.8],
333
+ [121.5, 35.0],
334
+ [124.0, 39.8],
335
+ [126.8, 41.9],
336
+ [130.0, 42.3],
337
+ [134.7, 48.2],
338
+ [131.0, 47.7],
339
+ [128.0, 49.6],
340
+ [123.3, 53.5],
341
+ [119.5, 50.0],
342
+ [117.0, 49.6],
343
+ [111.0, 49.2],
344
+ [105.0, 45.0],
345
+ [97.0, 42.8],
346
+ [91.0, 45.2],
347
+ [87.0, 49.0],
348
+ [82.0, 47.2],
349
+ [79.0, 43.0],
350
+ ]);
351
+ }
352
+ pointInPolygon(lon, lat, polygon) {
353
+ let inside = false;
354
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
355
+ const xi = polygon[i][0];
356
+ const yi = polygon[i][1];
357
+ const xj = polygon[j][0];
358
+ const yj = polygon[j][1];
359
+ const intersects = ((yi > lat) !== (yj > lat)) &&
360
+ (lon < (xj - xi) * (lat - yi) / (yj - yi) + xi);
361
+ if (intersects)
362
+ inside = !inside;
363
+ }
364
+ return inside;
365
+ }
366
+ fallbackRegionForMissingCoordinates(data, source, place) {
367
+ if (place && !data.is_cancel)
368
+ return 'global';
369
+ return this.fallbackRegionBySource(source);
370
+ }
371
+ shouldAllowUnknownEarthquakeRegion(source) {
372
+ const fallback = this.fallbackRegionBySource(source);
373
+ if (fallback === 'unknown')
374
+ return this.config.regions.global;
375
+ return this.isRegionEnabled(fallback);
376
+ }
377
+ fallbackRegionBySource(source) {
378
+ if ([models_1.DataSource.FAN_STUDIO_CEA, models_1.DataSource.FAN_STUDIO_CENC, models_1.DataSource.WOLFX_CENC_EEW, models_1.DataSource.WOLFX_CENC_EQ].includes(source))
379
+ return 'china';
380
+ if ([models_1.DataSource.FAN_STUDIO_CWA, models_1.DataSource.WOLFX_CWA_EEW].includes(source))
381
+ return 'taiwan';
382
+ if ([models_1.DataSource.FAN_STUDIO_JMA, models_1.DataSource.P2P_EEW, models_1.DataSource.P2P_EARTHQUAKE, models_1.DataSource.WOLFX_JMA_EEW, models_1.DataSource.WOLFX_JMA_EQ].includes(source))
383
+ return 'japan';
384
+ if ([models_1.DataSource.FAN_STUDIO_USGS, models_1.DataSource.GLOBAL_QUAKE].includes(source))
385
+ return 'global';
386
+ return 'unknown';
387
+ }
388
+ isRegionEnabled(region) {
389
+ if (region === 'china')
390
+ return this.config.regions.china;
391
+ if (region === 'taiwan')
392
+ return this.config.regions.taiwan;
393
+ if (region === 'japan')
394
+ return this.config.regions.japan;
395
+ if (region === 'global')
396
+ return this.config.regions.global;
397
+ return false;
398
+ }
215
399
  // ---- 状态查询(供 commands 使用)------------------------------------
216
400
  getStatus() {
217
401
  const result = {};
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.3",
4
4
  "description": "Koishi 灾害预警插件,支持多数据源(地震、海啸、气象预警)",
5
5
  "contributors": [
6
6
  "lumia.wang <fenglian19980510@gmail.com>"
@@ -38,8 +38,7 @@
38
38
  "@types/node": "^20.0.0",
39
39
  "@types/ws": "^8.5.10",
40
40
  "koishi": "^4.18.0",
41
- "typescript": "^5.0.0",
42
- "ws": "^8.18.0"
41
+ "typescript": "^5.0.0"
43
42
  },
44
43
  "koishi": {
45
44
  "description": {
@@ -54,5 +53,8 @@
54
53
  "database"
55
54
  ]
56
55
  }
56
+ },
57
+ "dependencies": {
58
+ "ws": "^8.18.0"
57
59
  }
58
60
  }