koishi-plugin-disaster-warning 0.0.1

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.
@@ -0,0 +1,9 @@
1
+ import { Logger } from 'koishi';
2
+ import { DisasterEvent } from '../models';
3
+ export declare abstract class BaseDataHandler {
4
+ protected sourceId: string;
5
+ protected logger: Logger;
6
+ constructor(sourceId: string);
7
+ abstract parseMessage(message: any): DisasterEvent | null;
8
+ protected parseDateTime(timeStr: string): string | undefined;
9
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseDataHandler = void 0;
4
+ const koishi_1 = require("koishi");
5
+ const logger = new koishi_1.Logger('disaster-warning');
6
+ class BaseDataHandler {
7
+ constructor(sourceId) {
8
+ this.sourceId = sourceId;
9
+ this.logger = logger;
10
+ }
11
+ parseDateTime(timeStr) {
12
+ if (!timeStr)
13
+ return undefined;
14
+ try {
15
+ // Try to parse as ISO string or other formats
16
+ // JS Date constructor is quite flexible
17
+ const date = new Date(timeStr);
18
+ if (isNaN(date.getTime())) {
19
+ this.logger.warn(`[${this.sourceId}] Failed to parse time: ${timeStr}`);
20
+ return undefined;
21
+ }
22
+ return date.toISOString();
23
+ }
24
+ catch (e) {
25
+ this.logger.warn(`[${this.sourceId}] Failed to parse time: ${timeStr}`);
26
+ return undefined;
27
+ }
28
+ }
29
+ }
30
+ exports.BaseDataHandler = BaseDataHandler;
@@ -0,0 +1,10 @@
1
+ import { BaseDataHandler } from './base';
2
+ import { DisasterEvent } from '../models';
3
+ export declare class FanStudioHandler extends BaseDataHandler {
4
+ constructor();
5
+ parseMessage(data: any): DisasterEvent | null;
6
+ private parseEarthquakeWarning;
7
+ private parseEarthquakeInfo;
8
+ private parseWeather;
9
+ private parseTsunami;
10
+ }
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FanStudioHandler = void 0;
4
+ const base_1 = require("./base");
5
+ const models_1 = require("../models");
6
+ class FanStudioHandler extends base_1.BaseDataHandler {
7
+ constructor() {
8
+ super('fan_studio');
9
+ }
10
+ parseMessage(data) {
11
+ try {
12
+ // FanStudio data usually comes in a 'Data' or 'data' field, or just the object itself
13
+ const msgData = data.Data || data.data || data;
14
+ if (!msgData)
15
+ return null;
16
+ // Identify type based on fields
17
+ // Earthquake Warning (CEA, CWA, JMA EEW)
18
+ if (msgData.epiIntensity !== undefined || msgData.magnitude !== undefined && msgData.isFinal !== undefined) {
19
+ return this.parseEarthquakeWarning(msgData);
20
+ }
21
+ // Earthquake Info (CENC, USGS, JMA Info) - usually has 'type' or specific fields
22
+ // But FanStudio might normalize them.
23
+ // Let's look at CENC/USGS fields from previous analysis if possible, or infer.
24
+ // CENC usually has "cenc_earthquake" or similar if it was tagged, but here we just have raw data.
25
+ // If it has 'eventId' and 'magnitude' but no 'epiIntensity', it might be earthquake info.
26
+ if (msgData.eventId && msgData.magnitude !== undefined && msgData.epiIntensity === undefined) {
27
+ return this.parseEarthquakeInfo(msgData);
28
+ }
29
+ // Weather
30
+ if (msgData.headline && msgData.description) {
31
+ return this.parseWeather(msgData);
32
+ }
33
+ // Tsunami
34
+ if (msgData.warningInfo || (msgData.title && msgData.level && msgData.forecasts)) {
35
+ return this.parseTsunami(msgData);
36
+ }
37
+ return null;
38
+ }
39
+ catch (e) {
40
+ this.logger.error(`[${this.sourceId}] Error parsing message:`, e);
41
+ return null;
42
+ }
43
+ }
44
+ parseEarthquakeWarning(data) {
45
+ // Determine source more specifically if possible, otherwise default to CEA
46
+ // This is a simplification. In reality we might need to check specific fields to distinguish CEA/CWA/JMA
47
+ let source = models_1.DataSource.FAN_STUDIO_CEA;
48
+ // If it has 'scale' instead of 'intensity', it might be JMA
49
+ // If it has 'province' like '台湾', it might be CWA
50
+ // For now, let's map based on some heuristics or default to CEA
51
+ const earthquake = {
52
+ id: data.id || '',
53
+ event_id: data.eventId || data.id || '',
54
+ source: source,
55
+ disaster_type: models_1.DisasterType.EARTHQUAKE_WARNING,
56
+ shock_time: this.parseDateTime(data.shockTime) || new Date().toISOString(),
57
+ latitude: Number(data.latitude) || 0,
58
+ longitude: Number(data.longitude) || 0,
59
+ depth: Number(data.depth),
60
+ magnitude: Number(data.magnitude),
61
+ intensity: Number(data.epiIntensity),
62
+ place_name: data.placeName || '',
63
+ province: data.province,
64
+ updates: data.updates || 1,
65
+ is_final: data.isFinal || false,
66
+ is_cancel: false, // TODO: Check for cancel signal
67
+ raw_data: data
68
+ };
69
+ return {
70
+ id: earthquake.id,
71
+ data: earthquake,
72
+ source: source,
73
+ disaster_type: models_1.DisasterType.EARTHQUAKE_WARNING,
74
+ receive_time: new Date().toISOString(),
75
+ push_count: 0,
76
+ raw_data: data
77
+ };
78
+ }
79
+ parseEarthquakeInfo(data) {
80
+ // CENC or USGS
81
+ const earthquake = {
82
+ id: data.id || '',
83
+ event_id: data.eventId || data.id || '',
84
+ source: models_1.DataSource.FAN_STUDIO_CENC, // Default to CENC
85
+ disaster_type: models_1.DisasterType.EARTHQUAKE,
86
+ shock_time: this.parseDateTime(data.shockTime) || new Date().toISOString(),
87
+ latitude: Number(data.latitude) || 0,
88
+ longitude: Number(data.longitude) || 0,
89
+ depth: Number(data.depth),
90
+ magnitude: Number(data.magnitude),
91
+ place_name: data.placeName || '',
92
+ updates: 1,
93
+ is_final: true,
94
+ is_cancel: false,
95
+ raw_data: data
96
+ };
97
+ return {
98
+ id: earthquake.id,
99
+ data: earthquake,
100
+ source: models_1.DataSource.FAN_STUDIO_CENC,
101
+ disaster_type: models_1.DisasterType.EARTHQUAKE,
102
+ receive_time: new Date().toISOString(),
103
+ push_count: 0,
104
+ raw_data: data
105
+ };
106
+ }
107
+ parseWeather(data) {
108
+ const weather = {
109
+ id: data.id || `weather_${Date.now()}`,
110
+ source: models_1.DataSource.FAN_STUDIO_WEATHER,
111
+ headline: data.headline,
112
+ title: data.title || data.headline,
113
+ description: data.description,
114
+ type: data.type || 'unknown',
115
+ effective_time: this.parseDateTime(data.effectiveTime) || new Date().toISOString(),
116
+ disaster_type: models_1.DisasterType.WEATHER_ALARM,
117
+ issue_time: this.parseDateTime(data.issueTime),
118
+ affected_areas: [], // TODO: Parse affected areas if available
119
+ raw_data: data
120
+ };
121
+ return {
122
+ id: weather.id,
123
+ data: weather,
124
+ source: models_1.DataSource.FAN_STUDIO_WEATHER,
125
+ disaster_type: models_1.DisasterType.WEATHER_ALARM,
126
+ receive_time: new Date().toISOString(),
127
+ push_count: 0,
128
+ raw_data: data
129
+ };
130
+ }
131
+ parseTsunami(data) {
132
+ const tsunami = {
133
+ id: data.id || `tsunami_${Date.now()}`,
134
+ code: data.code || '',
135
+ source: models_1.DataSource.FAN_STUDIO_TSUNAMI,
136
+ title: data.title || '',
137
+ level: data.level || '',
138
+ disaster_type: models_1.DisasterType.TSUNAMI,
139
+ org_unit: data.orgUnit || '',
140
+ forecasts: data.forecasts || [],
141
+ monitoring_stations: [],
142
+ raw_data: data
143
+ };
144
+ return {
145
+ id: tsunami.id,
146
+ data: tsunami,
147
+ source: models_1.DataSource.FAN_STUDIO_TSUNAMI,
148
+ disaster_type: models_1.DisasterType.TSUNAMI,
149
+ receive_time: new Date().toISOString(),
150
+ push_count: 0,
151
+ raw_data: data
152
+ };
153
+ }
154
+ }
155
+ exports.FanStudioHandler = FanStudioHandler;
@@ -0,0 +1,6 @@
1
+ import { BaseDataHandler } from './base';
2
+ import { DisasterEvent } from '../models';
3
+ export declare class GlobalQuakeHandler extends BaseDataHandler {
4
+ constructor();
5
+ parseMessage(data: any): DisasterEvent | null;
6
+ }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GlobalQuakeHandler = void 0;
4
+ const base_1 = require("./base");
5
+ const models_1 = require("../models");
6
+ class GlobalQuakeHandler extends base_1.BaseDataHandler {
7
+ constructor() {
8
+ super('global_quake');
9
+ }
10
+ parseMessage(data) {
11
+ try {
12
+ // Assuming GlobalQuake format based on typical JSON structure or inferring from usage
13
+ // Since I didn't see explicit GlobalQuake handler code in the file list (maybe I missed it or it's simple)
14
+ // I'll assume a generic structure or try to find it.
15
+ // Wait, `global_sources.py` might contain it.
16
+ // For now, let's implement a placeholder or basic structure.
17
+ // If data has 'magnitude' and 'latitude', it's likely an earthquake.
18
+ if (!data.uuid || !data.magnitude)
19
+ return null;
20
+ const earthquake = {
21
+ id: data.uuid,
22
+ event_id: data.uuid,
23
+ source: models_1.DataSource.GLOBAL_QUAKE,
24
+ disaster_type: models_1.DisasterType.EARTHQUAKE_WARNING, // GQ is usually real-time
25
+ shock_time: this.parseDateTime(data.origin) || new Date().toISOString(),
26
+ latitude: Number(data.lat),
27
+ longitude: Number(data.lon),
28
+ depth: Number(data.depth),
29
+ magnitude: Number(data.magnitude),
30
+ place_name: data.region || 'Unknown',
31
+ updates: data.revision || 1,
32
+ is_final: false, // GQ updates frequently
33
+ is_cancel: false,
34
+ max_pga: data.maxPGA,
35
+ raw_data: data
36
+ };
37
+ return {
38
+ id: earthquake.id,
39
+ data: earthquake,
40
+ source: models_1.DataSource.GLOBAL_QUAKE,
41
+ disaster_type: models_1.DisasterType.EARTHQUAKE_WARNING,
42
+ receive_time: new Date().toISOString(),
43
+ push_count: 0,
44
+ raw_data: data
45
+ };
46
+ }
47
+ catch (e) {
48
+ this.logger.error(`[${this.sourceId}] Error parsing message:`, e);
49
+ return null;
50
+ }
51
+ }
52
+ }
53
+ exports.GlobalQuakeHandler = GlobalQuakeHandler;
@@ -0,0 +1,5 @@
1
+ export * from './base';
2
+ export * from './fanstudio';
3
+ export * from './p2p';
4
+ export * from './wolfx';
5
+ export * from './global_quake';
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./base"), exports);
18
+ __exportStar(require("./fanstudio"), exports);
19
+ __exportStar(require("./p2p"), exports);
20
+ __exportStar(require("./wolfx"), exports);
21
+ __exportStar(require("./global_quake"), exports);
@@ -0,0 +1,10 @@
1
+ import { BaseDataHandler } from './base';
2
+ import { DisasterEvent } from '../models';
3
+ export declare class P2PHandler extends BaseDataHandler {
4
+ constructor();
5
+ parseMessage(data: any): DisasterEvent | null;
6
+ private parseEEW;
7
+ private parseEarthquake;
8
+ private parseTsunami;
9
+ private convertP2PScale;
10
+ }
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.P2PHandler = void 0;
4
+ const base_1 = require("./base");
5
+ const models_1 = require("../models");
6
+ class P2PHandler extends base_1.BaseDataHandler {
7
+ constructor() {
8
+ super('p2p_earthquake');
9
+ }
10
+ parseMessage(data) {
11
+ try {
12
+ const code = data.code;
13
+ if (code === 556) {
14
+ return this.parseEEW(data);
15
+ }
16
+ else if (code === 551) {
17
+ return this.parseEarthquake(data);
18
+ }
19
+ else if (code === 552) {
20
+ return this.parseTsunami(data);
21
+ }
22
+ return null;
23
+ }
24
+ catch (e) {
25
+ this.logger.error(`[${this.sourceId}] Error parsing message:`, e);
26
+ return null;
27
+ }
28
+ }
29
+ parseEEW(data) {
30
+ const earthquakeInfo = data.earthquake || {};
31
+ const hypocenter = earthquakeInfo.hypocenter || {};
32
+ const issueInfo = data.issue || {};
33
+ const areas = data.areas || [];
34
+ if (data.cancelled) {
35
+ this.logger.info(`[${this.sourceId}] EEW Cancelled`);
36
+ // We might want to handle cancellation
37
+ return null;
38
+ }
39
+ let maxScale = -1;
40
+ if (earthquakeInfo.maxScale !== undefined)
41
+ maxScale = earthquakeInfo.maxScale;
42
+ else if (earthquakeInfo.max_scale !== undefined)
43
+ maxScale = earthquakeInfo.max_scale;
44
+ else {
45
+ // Calculate from areas
46
+ const scales = areas.map((a) => Math.max(a.scaleFrom || 0, a.scaleTo || 0));
47
+ if (scales.length > 0)
48
+ maxScale = Math.max(...scales);
49
+ }
50
+ const scale = maxScale !== -1 ? this.convertP2PScale(maxScale) : undefined;
51
+ const earthquake = {
52
+ id: data.id || '',
53
+ event_id: issueInfo.eventId || data.id || '',
54
+ source: models_1.DataSource.P2P_EEW,
55
+ disaster_type: models_1.DisasterType.EARTHQUAKE_WARNING,
56
+ shock_time: this.parseDateTime(earthquakeInfo.time || earthquakeInfo.originTime) || new Date().toISOString(),
57
+ latitude: Number(hypocenter.latitude) || 0,
58
+ longitude: Number(hypocenter.longitude) || 0,
59
+ depth: Number(hypocenter.depth),
60
+ magnitude: Number(hypocenter.magnitude),
61
+ place_name: hypocenter.name || 'Unknown',
62
+ scale: scale,
63
+ max_scale: maxScale,
64
+ is_final: data.is_final || false,
65
+ is_cancel: data.cancelled || false,
66
+ is_training: data.test || false,
67
+ serial: issueInfo.serial,
68
+ updates: 1, // P2P doesn't explicitly send update count in the same way, but serial might be it
69
+ raw_data: data
70
+ };
71
+ return {
72
+ id: earthquake.id,
73
+ data: earthquake,
74
+ source: models_1.DataSource.P2P_EEW,
75
+ disaster_type: models_1.DisasterType.EARTHQUAKE_WARNING,
76
+ receive_time: new Date().toISOString(),
77
+ push_count: 0,
78
+ raw_data: data
79
+ };
80
+ }
81
+ parseEarthquake(data) {
82
+ const earthquakeInfo = data.earthquake || {};
83
+ const hypocenter = earthquakeInfo.hypocenter || {};
84
+ const magnitude = Number(hypocenter.magnitude);
85
+ const lat = Number(hypocenter.latitude);
86
+ const lon = Number(hypocenter.longitude);
87
+ if (isNaN(magnitude) || isNaN(lat) || isNaN(lon))
88
+ return null;
89
+ const maxScale = earthquakeInfo.maxScale !== undefined ? earthquakeInfo.maxScale : -1;
90
+ const scale = maxScale !== -1 ? this.convertP2PScale(maxScale) : undefined;
91
+ let depth = Number(hypocenter.depth);
92
+ if (isNaN(depth))
93
+ depth = 0; // Or undefined
94
+ const earthquake = {
95
+ id: data.id || '',
96
+ event_id: data.id || '',
97
+ source: models_1.DataSource.P2P_EARTHQUAKE,
98
+ disaster_type: models_1.DisasterType.EARTHQUAKE,
99
+ shock_time: this.parseDateTime(earthquakeInfo.time) || new Date().toISOString(),
100
+ latitude: lat,
101
+ longitude: lon,
102
+ depth: depth,
103
+ magnitude: magnitude,
104
+ place_name: hypocenter.name || 'Unknown',
105
+ scale: scale,
106
+ max_scale: maxScale,
107
+ domestic_tsunami: earthquakeInfo.domesticTsunami,
108
+ foreign_tsunami: earthquakeInfo.foreignTsunami,
109
+ updates: 1,
110
+ is_final: true,
111
+ is_cancel: false,
112
+ raw_data: data
113
+ };
114
+ return {
115
+ id: earthquake.id,
116
+ data: earthquake,
117
+ source: models_1.DataSource.P2P_EARTHQUAKE,
118
+ disaster_type: models_1.DisasterType.EARTHQUAKE,
119
+ receive_time: new Date().toISOString(),
120
+ push_count: 0,
121
+ raw_data: data
122
+ };
123
+ }
124
+ parseTsunami(data) {
125
+ // TODO: Implement Tsunami parsing if needed
126
+ // For now return null or basic implementation
127
+ return null;
128
+ }
129
+ convertP2PScale(scale) {
130
+ const mapping = {
131
+ 10: 1.0, 20: 2.0, 30: 3.0, 40: 4.0,
132
+ 45: 4.5, 46: 4.6, 50: 5.0, 55: 5.5,
133
+ 60: 6.0, 70: 7.0
134
+ };
135
+ return mapping[scale];
136
+ }
137
+ }
138
+ exports.P2PHandler = P2PHandler;
@@ -0,0 +1,10 @@
1
+ import { BaseDataHandler } from './base';
2
+ import { DisasterEvent } from '../models';
3
+ export declare class WolfxHandler extends BaseDataHandler {
4
+ constructor(sourceId: string);
5
+ parseMessage(data: any): DisasterEvent | null;
6
+ private parseJMAEEW;
7
+ private parseCENCEEW;
8
+ private parseJMAEqList;
9
+ private parseJMAScale;
10
+ }
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WolfxHandler = void 0;
4
+ const base_1 = require("./base");
5
+ const models_1 = require("../models");
6
+ class WolfxHandler extends base_1.BaseDataHandler {
7
+ constructor(sourceId) {
8
+ super(sourceId);
9
+ }
10
+ parseMessage(data) {
11
+ try {
12
+ const type = data.type;
13
+ if (type === 'jma_eew') {
14
+ return this.parseJMAEEW(data);
15
+ }
16
+ else if (type === 'cenc_eew') {
17
+ return this.parseCENCEEW(data);
18
+ }
19
+ else if (type === 'jma_eqlist') {
20
+ return this.parseJMAEqList(data);
21
+ }
22
+ else if (type === 'cenc_eqlist') {
23
+ // TODO: Implement CENC EqList
24
+ return null;
25
+ }
26
+ return null;
27
+ }
28
+ catch (e) {
29
+ this.logger.error(`[${this.sourceId}] Error parsing message:`, e);
30
+ return null;
31
+ }
32
+ }
33
+ parseJMAEEW(data) {
34
+ const earthquake = {
35
+ id: data.EventID || '',
36
+ event_id: data.EventID || '',
37
+ source: models_1.DataSource.WOLFX_JMA_EEW,
38
+ disaster_type: models_1.DisasterType.EARTHQUAKE_WARNING,
39
+ shock_time: this.parseDateTime(data.OriginTime) || new Date().toISOString(),
40
+ latitude: Number(data.Latitude) || 0,
41
+ longitude: Number(data.Longitude) || 0,
42
+ depth: Number(data.Depth),
43
+ magnitude: Number(data.Magunitude || data.Magnitude),
44
+ place_name: data.Hypocenter || '',
45
+ scale: this.parseJMAScale(data.MaxIntensity),
46
+ is_final: data.isFinal || false,
47
+ is_cancel: data.isCancel || false,
48
+ is_training: data.isTraining || false,
49
+ updates: 1,
50
+ raw_data: data
51
+ };
52
+ return {
53
+ id: earthquake.id,
54
+ data: earthquake,
55
+ source: models_1.DataSource.WOLFX_JMA_EEW,
56
+ disaster_type: models_1.DisasterType.EARTHQUAKE_WARNING,
57
+ receive_time: new Date().toISOString(),
58
+ push_count: 0,
59
+ raw_data: data
60
+ };
61
+ }
62
+ parseCENCEEW(data) {
63
+ const earthquake = {
64
+ id: data.ID || '',
65
+ event_id: data.EventID || '',
66
+ source: models_1.DataSource.WOLFX_CENC_EEW,
67
+ disaster_type: models_1.DisasterType.EARTHQUAKE_WARNING,
68
+ shock_time: this.parseDateTime(data.OriginTime) || new Date().toISOString(),
69
+ latitude: Number(data.Latitude) || 0,
70
+ longitude: Number(data.Longitude) || 0,
71
+ depth: Number(data.Depth),
72
+ magnitude: Number(data.Magnitude),
73
+ intensity: Number(data.MaxIntensity),
74
+ place_name: data.HypoCenter || '',
75
+ updates: Number(data.ReportNum) || 1,
76
+ is_final: data.isFinal || false,
77
+ is_cancel: false,
78
+ raw_data: data
79
+ };
80
+ return {
81
+ id: earthquake.id,
82
+ data: earthquake,
83
+ source: models_1.DataSource.WOLFX_CENC_EEW,
84
+ disaster_type: models_1.DisasterType.EARTHQUAKE_WARNING,
85
+ receive_time: new Date().toISOString(),
86
+ push_count: 0,
87
+ raw_data: data
88
+ };
89
+ }
90
+ parseJMAEqList(data) {
91
+ // Find the latest earthquake (usually No1 or similar key, or we iterate)
92
+ // The original code iterated and found the one with 'No' prefix
93
+ let eqInfo = null;
94
+ for (const key in data) {
95
+ if (key.startsWith('No') && typeof data[key] === 'object') {
96
+ eqInfo = data[key];
97
+ break; // Assuming first one is latest
98
+ }
99
+ }
100
+ if (!eqInfo)
101
+ return null;
102
+ let depth = Number(eqInfo.depth);
103
+ if (isNaN(depth) && typeof eqInfo.depth === 'string' && eqInfo.depth.endsWith('km')) {
104
+ depth = Number(eqInfo.depth.replace('km', ''));
105
+ }
106
+ const earthquake = {
107
+ id: eqInfo.md5 || '',
108
+ event_id: eqInfo.md5 || '',
109
+ source: models_1.DataSource.WOLFX_JMA_EQ,
110
+ disaster_type: models_1.DisasterType.EARTHQUAKE,
111
+ shock_time: this.parseDateTime(eqInfo.time) || new Date().toISOString(),
112
+ latitude: Number(eqInfo.latitude) || 0,
113
+ longitude: Number(eqInfo.longitude) || 0,
114
+ depth: depth || 0,
115
+ magnitude: Number(eqInfo.magnitude),
116
+ place_name: eqInfo.location || '',
117
+ scale: this.parseJMAScale(eqInfo.shindo),
118
+ updates: 1,
119
+ is_final: true,
120
+ is_cancel: false,
121
+ raw_data: data
122
+ };
123
+ return {
124
+ id: earthquake.id,
125
+ data: earthquake,
126
+ source: models_1.DataSource.WOLFX_JMA_EQ,
127
+ disaster_type: models_1.DisasterType.EARTHQUAKE,
128
+ receive_time: new Date().toISOString(),
129
+ push_count: 0,
130
+ raw_data: data
131
+ };
132
+ }
133
+ parseJMAScale(scaleStr) {
134
+ if (!scaleStr)
135
+ return undefined;
136
+ const match = scaleStr.match(/(\d+)(弱|強)?/);
137
+ if (match) {
138
+ const base = parseInt(match[1]);
139
+ const suffix = match[2];
140
+ if (suffix === '弱')
141
+ return base - 0.5;
142
+ if (suffix === '強')
143
+ return base + 0.5;
144
+ return base;
145
+ }
146
+ return undefined;
147
+ }
148
+ }
149
+ exports.WolfxHandler = WolfxHandler;
package/lib/index.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "disaster-warning";
3
+ export declare const inject: string[];
4
+ export interface Config {
5
+ enabled: boolean;
6
+ target_groups: string[];
7
+ platform_name: string;
8
+ data_sources: {
9
+ fan_studio: {
10
+ enabled: boolean;
11
+ china_earthquake_warning: boolean;
12
+ taiwan_cwa_earthquake: boolean;
13
+ china_cenc_earthquake: boolean;
14
+ japan_jma_eew: boolean;
15
+ usgs_earthquake: boolean;
16
+ china_weather_alarm: boolean;
17
+ china_tsunami: boolean;
18
+ };
19
+ p2p_earthquake: {
20
+ enabled: boolean;
21
+ japan_jma_eew: boolean;
22
+ japan_jma_earthquake: boolean;
23
+ japan_jma_tsunami: boolean;
24
+ };
25
+ wolfx: {
26
+ enabled: boolean;
27
+ japan_jma_eew: boolean;
28
+ china_cenc_eew: boolean;
29
+ taiwan_cwa_eew: boolean;
30
+ japan_jma_earthquake: boolean;
31
+ china_cenc_earthquake: boolean;
32
+ };
33
+ global_quake: {
34
+ enabled: boolean;
35
+ };
36
+ };
37
+ earthquake_filters: {
38
+ intensity_filter: {
39
+ enabled: boolean;
40
+ min_magnitude: number;
41
+ min_intensity: number;
42
+ };
43
+ scale_filter: {
44
+ enabled: boolean;
45
+ min_magnitude: number;
46
+ min_scale: number;
47
+ };
48
+ magnitude_only_filter: {
49
+ enabled: boolean;
50
+ min_magnitude: number;
51
+ };
52
+ };
53
+ }
54
+ export declare const Config: Schema<Config>;
55
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js ADDED
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Config = exports.inject = exports.name = void 0;
4
+ exports.apply = apply;
5
+ const koishi_1 = require("koishi");
6
+ const service_1 = require("./service");
7
+ exports.name = 'disaster-warning';
8
+ exports.inject = ['database?']; // Optional database dependency if needed later
9
+ exports.Config = koishi_1.Schema.object({
10
+ enabled: koishi_1.Schema.boolean().default(true).description('启用灾害预警插件'),
11
+ target_groups: koishi_1.Schema.array(koishi_1.Schema.string()).default([]).description('需要推送消息的群号列表'),
12
+ platform_name: koishi_1.Schema.string().default('default').description('消息平台名称'),
13
+ data_sources: koishi_1.Schema.object({
14
+ fan_studio: koishi_1.Schema.object({
15
+ enabled: koishi_1.Schema.boolean().default(true).description('启用FAN Studio数据源'),
16
+ china_earthquake_warning: koishi_1.Schema.boolean().default(true).description('中国地震网地震预警'),
17
+ taiwan_cwa_earthquake: koishi_1.Schema.boolean().default(true).description('台湾中央气象署:强震即时警报'),
18
+ china_cenc_earthquake: koishi_1.Schema.boolean().default(true).description('中国地震台网(CENC):地震测定'),
19
+ japan_jma_eew: koishi_1.Schema.boolean().default(true).description('日本气象厅(JMA):紧急地震速报'),
20
+ usgs_earthquake: koishi_1.Schema.boolean().default(true).description('美国地质调查局(USGS):地震测定'),
21
+ china_weather_alarm: koishi_1.Schema.boolean().default(true).description('中国气象局:气象预警'),
22
+ china_tsunami: koishi_1.Schema.boolean().default(true).description('自然资源部海啸预警中心:海啸预警信息'),
23
+ }).description('FAN Studio WebSocket 数据源'),
24
+ p2p_earthquake: koishi_1.Schema.object({
25
+ enabled: koishi_1.Schema.boolean().default(true).description('启用P2P地震情報数据源'),
26
+ japan_jma_eew: koishi_1.Schema.boolean().default(true).description('日本気象庁:緊急地震速報'),
27
+ japan_jma_earthquake: koishi_1.Schema.boolean().default(true).description('日本気象庁(JMA):地震情報'),
28
+ japan_jma_tsunami: koishi_1.Schema.boolean().default(true).description('日本気象庁:津波予報'),
29
+ }).description('P2P地震情報 WebSocket 数据源'),
30
+ wolfx: koishi_1.Schema.object({
31
+ enabled: koishi_1.Schema.boolean().default(true).description('启用Wolfx数据源'),
32
+ japan_jma_eew: koishi_1.Schema.boolean().default(true).description('日本気象庁:緊急地震速報'),
33
+ china_cenc_eew: koishi_1.Schema.boolean().default(true).description('中国地震台网(CENC):地震预警'),
34
+ taiwan_cwa_eew: koishi_1.Schema.boolean().default(true).description('台湾中央气象署:地震预警'),
35
+ japan_jma_earthquake: koishi_1.Schema.boolean().default(true).description('日本気象庁(JMA):地震情報'),
36
+ china_cenc_earthquake: koishi_1.Schema.boolean().default(true).description('中国地震台网(CENC):地震测定'),
37
+ }).description('Wolfx API 数据源'),
38
+ global_quake: koishi_1.Schema.object({
39
+ enabled: koishi_1.Schema.boolean().default(true).description('启用Global Quake数据源'),
40
+ }).description('Global Quake 服务器推送'),
41
+ }).description('数据源配置'),
42
+ earthquake_filters: koishi_1.Schema.object({
43
+ intensity_filter: koishi_1.Schema.object({
44
+ enabled: koishi_1.Schema.boolean().default(true).description('启用烈度过滤器'),
45
+ min_magnitude: koishi_1.Schema.number().default(2.0).description('最小震级'),
46
+ min_intensity: koishi_1.Schema.number().default(4.0).description('最小烈度'),
47
+ }).description('基于震级和烈度的地震过滤器'),
48
+ scale_filter: koishi_1.Schema.object({
49
+ enabled: koishi_1.Schema.boolean().default(true).description('启用震度过滤器'),
50
+ min_magnitude: koishi_1.Schema.number().default(2.0).description('最小震级'),
51
+ min_scale: koishi_1.Schema.number().default(1.0).description('最小震度'),
52
+ }).description('基于震级和震度的地震过滤器'),
53
+ magnitude_only_filter: koishi_1.Schema.object({
54
+ enabled: koishi_1.Schema.boolean().default(true).description('启用仅震级过滤器'),
55
+ min_magnitude: koishi_1.Schema.number().default(4.5).description('最小震级'),
56
+ }).description('USGS震级过滤器'),
57
+ }).description('地震信息过滤器配置'),
58
+ });
59
+ function apply(ctx, config) {
60
+ ctx.plugin(service_1.DisasterWarningService, config);
61
+ }
@@ -0,0 +1,106 @@
1
+ export declare enum DisasterType {
2
+ EARTHQUAKE = "earthquake",
3
+ EARTHQUAKE_WARNING = "earthquake_warning",
4
+ TSUNAMI = "tsunami",
5
+ WEATHER_ALARM = "weather_alarm"
6
+ }
7
+ export declare enum DataSource {
8
+ FAN_STUDIO_CENC = "fan_studio_cenc",
9
+ FAN_STUDIO_CEA = "fan_studio_cea",
10
+ FAN_STUDIO_CWA = "fan_studio_cwa",
11
+ FAN_STUDIO_USGS = "fan_studio_usgs",
12
+ FAN_STUDIO_JMA = "fan_studio_jma",
13
+ FAN_STUDIO_WEATHER = "fan_studio_weather",
14
+ FAN_STUDIO_TSUNAMI = "fan_studio_tsunami",
15
+ P2P_EEW = "p2p_eew",
16
+ P2P_EARTHQUAKE = "p2p_earthquake",
17
+ P2P_TSUNAMI = "p2p_tsunami",
18
+ WOLFX_JMA_EEW = "wolfx_jma_eew",
19
+ WOLFX_CENC_EEW = "wolfx_cenc_eew",
20
+ WOLFX_CWA_EEW = "wolfx_cwa_eew",
21
+ WOLFX_CENC_EQ = "wolfx_cenc_eq",
22
+ WOLFX_JMA_EQ = "wolfx_jma_eq",
23
+ GLOBAL_QUAKE = "global_quake"
24
+ }
25
+ export declare const DATA_SOURCE_MAPPING: Record<string, DataSource>;
26
+ export declare function getDataSourceFromId(id: string): DataSource | undefined;
27
+ export interface EarthquakeData {
28
+ id: string;
29
+ event_id: string;
30
+ source: DataSource;
31
+ disaster_type: DisasterType;
32
+ shock_time: string;
33
+ latitude: number;
34
+ longitude: number;
35
+ place_name: string;
36
+ depth?: number;
37
+ magnitude?: number;
38
+ intensity?: number;
39
+ scale?: number;
40
+ max_intensity?: number;
41
+ max_scale?: number;
42
+ province?: string;
43
+ updates: number;
44
+ is_final: boolean;
45
+ is_cancel: boolean;
46
+ info_type?: string;
47
+ domestic_tsunami?: string;
48
+ foreign_tsunami?: string;
49
+ update_time?: string;
50
+ create_time?: string;
51
+ source_id?: string;
52
+ report_num?: number;
53
+ serial?: string;
54
+ is_training?: boolean;
55
+ revision?: number;
56
+ max_pga?: number;
57
+ stations?: Record<string, number>;
58
+ raw_data: any;
59
+ }
60
+ export interface TsunamiData {
61
+ id: string;
62
+ code: string;
63
+ source: DataSource;
64
+ title: string;
65
+ level: string;
66
+ disaster_type: DisasterType;
67
+ subtitle?: string;
68
+ org_unit: string;
69
+ issue_time?: string;
70
+ forecasts: any[];
71
+ monitoring_stations: any[];
72
+ source_id?: string;
73
+ estimated_arrival_time?: string;
74
+ max_wave_height?: string;
75
+ raw_data: any;
76
+ }
77
+ export interface WeatherAlarmData {
78
+ id: string;
79
+ source: DataSource;
80
+ headline: string;
81
+ title: string;
82
+ description: string;
83
+ type: string;
84
+ effective_time: string;
85
+ disaster_type: DisasterType;
86
+ issue_time?: string;
87
+ longitude?: number;
88
+ latitude?: number;
89
+ source_id?: string;
90
+ alert_level?: string;
91
+ affected_areas: string[];
92
+ raw_data: any;
93
+ }
94
+ export interface DisasterEvent {
95
+ id: string;
96
+ data: EarthquakeData | TsunamiData | WeatherAlarmData;
97
+ source: DataSource;
98
+ disaster_type: DisasterType;
99
+ receive_time: string;
100
+ source_id?: string;
101
+ processing_time?: string;
102
+ is_filtered?: boolean;
103
+ filter_reason?: string;
104
+ push_count: number;
105
+ raw_data: any;
106
+ }
package/lib/models.js ADDED
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DATA_SOURCE_MAPPING = exports.DataSource = exports.DisasterType = void 0;
4
+ exports.getDataSourceFromId = getDataSourceFromId;
5
+ var DisasterType;
6
+ (function (DisasterType) {
7
+ DisasterType["EARTHQUAKE"] = "earthquake";
8
+ DisasterType["EARTHQUAKE_WARNING"] = "earthquake_warning";
9
+ DisasterType["TSUNAMI"] = "tsunami";
10
+ DisasterType["WEATHER_ALARM"] = "weather_alarm";
11
+ })(DisasterType || (exports.DisasterType = DisasterType = {}));
12
+ var DataSource;
13
+ (function (DataSource) {
14
+ // FAN Studio
15
+ DataSource["FAN_STUDIO_CENC"] = "fan_studio_cenc";
16
+ DataSource["FAN_STUDIO_CEA"] = "fan_studio_cea";
17
+ DataSource["FAN_STUDIO_CWA"] = "fan_studio_cwa";
18
+ DataSource["FAN_STUDIO_USGS"] = "fan_studio_usgs";
19
+ DataSource["FAN_STUDIO_JMA"] = "fan_studio_jma";
20
+ DataSource["FAN_STUDIO_WEATHER"] = "fan_studio_weather";
21
+ DataSource["FAN_STUDIO_TSUNAMI"] = "fan_studio_tsunami";
22
+ // P2P
23
+ DataSource["P2P_EEW"] = "p2p_eew";
24
+ DataSource["P2P_EARTHQUAKE"] = "p2p_earthquake";
25
+ DataSource["P2P_TSUNAMI"] = "p2p_tsunami";
26
+ // Wolfx
27
+ DataSource["WOLFX_JMA_EEW"] = "wolfx_jma_eew";
28
+ DataSource["WOLFX_CENC_EEW"] = "wolfx_cenc_eew";
29
+ DataSource["WOLFX_CWA_EEW"] = "wolfx_cwa_eew";
30
+ DataSource["WOLFX_CENC_EQ"] = "wolfx_cenc_eq";
31
+ DataSource["WOLFX_JMA_EQ"] = "wolfx_jma_eq";
32
+ // Global Quake
33
+ DataSource["GLOBAL_QUAKE"] = "global_quake";
34
+ })(DataSource || (exports.DataSource = DataSource = {}));
35
+ exports.DATA_SOURCE_MAPPING = {
36
+ "cea_fanstudio": DataSource.FAN_STUDIO_CEA,
37
+ "cea_wolfx": DataSource.WOLFX_CENC_EEW,
38
+ "cwa_fanstudio": DataSource.FAN_STUDIO_CWA,
39
+ "cwa_wolfx": DataSource.WOLFX_CWA_EEW,
40
+ "jma_fanstudio": DataSource.FAN_STUDIO_JMA,
41
+ "jma_p2p": DataSource.P2P_EEW,
42
+ "jma_wolfx": DataSource.WOLFX_JMA_EEW,
43
+ "global_quake": DataSource.GLOBAL_QUAKE,
44
+ "cenc_fanstudio": DataSource.FAN_STUDIO_CENC,
45
+ "cenc_wolfx": DataSource.WOLFX_CENC_EQ,
46
+ "jma_p2p_info": DataSource.P2P_EARTHQUAKE,
47
+ "jma_wolfx_info": DataSource.WOLFX_JMA_EQ,
48
+ "usgs_fanstudio": DataSource.FAN_STUDIO_USGS,
49
+ "china_weather_fanstudio": DataSource.FAN_STUDIO_WEATHER,
50
+ "china_tsunami_fanstudio": DataSource.FAN_STUDIO_TSUNAMI,
51
+ "jma_tsunami_p2p": DataSource.P2P_TSUNAMI,
52
+ };
53
+ function getDataSourceFromId(id) {
54
+ return exports.DATA_SOURCE_MAPPING[id];
55
+ }
@@ -0,0 +1,17 @@
1
+ import { Context } from 'koishi';
2
+ import { Config } from './index';
3
+ import { DisasterEvent } from './models';
4
+ export declare class MessagePushManager {
5
+ private ctx;
6
+ private config;
7
+ constructor(ctx: Context, config: Config);
8
+ pushEvent(event: DisasterEvent): Promise<void>;
9
+ private shouldFilter;
10
+ private formatMessage;
11
+ private formatEarthquake;
12
+ private formatTsunami;
13
+ private formatWeather;
14
+ private formatTime;
15
+ private formatScale;
16
+ private broadcast;
17
+ }
package/lib/pusher.js ADDED
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MessagePushManager = void 0;
4
+ const koishi_1 = require("koishi");
5
+ const models_1 = require("./models");
6
+ const logger = new koishi_1.Logger('disaster-pusher');
7
+ class MessagePushManager {
8
+ constructor(ctx, config) {
9
+ this.ctx = ctx;
10
+ this.config = config;
11
+ }
12
+ async pushEvent(event) {
13
+ if (this.shouldFilter(event)) {
14
+ logger.debug(`Event ${event.id} filtered.`);
15
+ return;
16
+ }
17
+ const message = this.formatMessage(event);
18
+ if (!message)
19
+ return;
20
+ logger.info(`Pushing event ${event.id} to ${this.config.target_groups.length} groups.`);
21
+ await this.broadcast(message);
22
+ }
23
+ shouldFilter(event) {
24
+ if (event.disaster_type === models_1.DisasterType.EARTHQUAKE || event.disaster_type === models_1.DisasterType.EARTHQUAKE_WARNING) {
25
+ const data = event.data;
26
+ const filters = this.config.earthquake_filters;
27
+ // Intensity Filter
28
+ if (filters.intensity_filter.enabled) {
29
+ const minMag = filters.intensity_filter.min_magnitude;
30
+ const minInt = filters.intensity_filter.min_intensity;
31
+ // Pass if magnitude OR intensity condition is met (OR logic as per schema hint)
32
+ // Wait, schema hint says "Satisfy magnitude requirement OR satisfy intensity requirement"
33
+ // Usually it means if (mag >= minMag || int >= minInt) -> Pass
34
+ let magPass = false;
35
+ let intPass = false;
36
+ if (data.magnitude !== undefined && data.magnitude >= minMag)
37
+ magPass = true;
38
+ if (data.intensity !== undefined && data.intensity >= minInt)
39
+ intPass = true;
40
+ // If neither is met (and relevant fields exist), filter out
41
+ // If fields are missing, we might be lenient or strict. Let's be lenient if one is missing but other passes.
42
+ if (!magPass && !intPass)
43
+ return true;
44
+ }
45
+ // Scale Filter (Japan)
46
+ if (filters.scale_filter.enabled && data.scale !== undefined) {
47
+ const minMag = filters.scale_filter.min_magnitude;
48
+ const minScale = filters.scale_filter.min_scale;
49
+ let magPass = false;
50
+ let scalePass = false;
51
+ if (data.magnitude !== undefined && data.magnitude >= minMag)
52
+ magPass = true;
53
+ if (data.scale >= minScale)
54
+ scalePass = true;
55
+ if (!magPass && !scalePass)
56
+ return true;
57
+ }
58
+ }
59
+ return false;
60
+ }
61
+ formatMessage(event) {
62
+ switch (event.disaster_type) {
63
+ case models_1.DisasterType.EARTHQUAKE:
64
+ case models_1.DisasterType.EARTHQUAKE_WARNING:
65
+ return this.formatEarthquake(event.data);
66
+ case models_1.DisasterType.TSUNAMI:
67
+ return this.formatTsunami(event.data);
68
+ case models_1.DisasterType.WEATHER_ALARM:
69
+ return this.formatWeather(event.data);
70
+ default:
71
+ return `Unknown disaster event: ${event.disaster_type}`;
72
+ }
73
+ }
74
+ formatEarthquake(data) {
75
+ const type = data.disaster_type === models_1.DisasterType.EARTHQUAKE_WARNING ? '地震预警' : '地震信息';
76
+ const finalStr = data.is_final ? '【最终报】' : `【第${data.updates}报】`;
77
+ const cancelStr = data.is_cancel ? '【已取消】' : '';
78
+ let msg = `${cancelStr}${type} ${finalStr}\n`;
79
+ msg += `震源:${data.place_name}\n`;
80
+ msg += `时间:${this.formatTime(data.shock_time)}\n`;
81
+ msg += `震级:M${data.magnitude?.toFixed(1) || '未知'}\n`;
82
+ msg += `深度:${data.depth !== undefined ? data.depth + 'km' : '未知'}\n`;
83
+ if (data.intensity !== undefined) {
84
+ msg += `最大烈度:${data.intensity.toFixed(1)}\n`;
85
+ }
86
+ if (data.scale !== undefined) {
87
+ msg += `最大震度:${this.formatScale(data.scale)}\n`;
88
+ }
89
+ msg += `数据源:${data.source}`;
90
+ return msg;
91
+ }
92
+ formatTsunami(data) {
93
+ let msg = `【海啸预警】${data.title}\n`;
94
+ msg += `级别:${data.level}\n`;
95
+ msg += `发布单位:${data.org_unit}\n`;
96
+ if (data.forecasts && data.forecasts.length > 0) {
97
+ msg += `预报区域:\n`;
98
+ data.forecasts.slice(0, 5).forEach((f) => {
99
+ msg += `- ${f.name || f.areaName}: ${f.grade || f.level}\n`;
100
+ });
101
+ if (data.forecasts.length > 5)
102
+ msg += `...等${data.forecasts.length}个区域\n`;
103
+ }
104
+ return msg;
105
+ }
106
+ formatWeather(data) {
107
+ let msg = `【气象预警】${data.headline}\n`;
108
+ msg += `类型:${data.type}\n`;
109
+ msg += `发布时间:${this.formatTime(data.issue_time || data.effective_time)}\n`;
110
+ msg += `详情:${data.description}\n`;
111
+ return msg;
112
+ }
113
+ formatTime(isoStr) {
114
+ try {
115
+ return new Date(isoStr).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
116
+ }
117
+ catch {
118
+ return isoStr;
119
+ }
120
+ }
121
+ formatScale(scale) {
122
+ // Convert numeric scale back to JMA string (e.g. 5.5 -> 5强)
123
+ if (scale === 4.5)
124
+ return '5弱';
125
+ if (scale === 5.0)
126
+ return '5强'; // Wait, 5.0 is 5强 in my logic?
127
+ // In p2p.ts: 50 -> 5.0.
128
+ // Usually 5- is 5 Lower, 5+ is 5 Upper.
129
+ // Let's stick to simple formatting or check logic.
130
+ // 5.0 -> 5强, 4.5 -> 5弱.
131
+ // 5.5 -> 6弱, 6.0 -> 6强.
132
+ if (scale === 4.5)
133
+ return '5弱';
134
+ if (scale === 5.0)
135
+ return '5强';
136
+ if (scale === 5.5)
137
+ return '6弱';
138
+ if (scale === 6.0)
139
+ return '6强';
140
+ return scale.toString();
141
+ }
142
+ async broadcast(message) {
143
+ for (const groupId of this.config.target_groups) {
144
+ // Construct session string like "platform:groupId" or use bot.sendMessage
145
+ // Koishi's broadcast method usually takes channelIds.
146
+ // If platform_name is 'onebot', channelId is usually the group number.
147
+ // We need to find the bot first or use ctx.broadcast.
148
+ // ctx.broadcast(channels, content)
149
+ // channels can be [`${platform}:${groupId}`]
150
+ const channelId = `${this.config.platform_name}:${groupId}`;
151
+ try {
152
+ await this.ctx.broadcast([channelId], message);
153
+ }
154
+ catch (e) {
155
+ logger.error(`Failed to send to ${channelId}:`, e);
156
+ }
157
+ }
158
+ }
159
+ }
160
+ exports.MessagePushManager = MessagePushManager;
@@ -0,0 +1,20 @@
1
+ import { Context } from 'koishi';
2
+ import { Config } from './index';
3
+ export declare class DisasterWarningService {
4
+ private config;
5
+ private connections;
6
+ private reconnectTimers;
7
+ private pusher;
8
+ private ctx;
9
+ private handlers;
10
+ constructor(ctx: Context, config: Config);
11
+ start(): Promise<void>;
12
+ stop(): Promise<void>;
13
+ private connectAll;
14
+ private connectWebSocket;
15
+ private handleEvent;
16
+ private connectFanStudio;
17
+ private connectP2P;
18
+ private connectWolfx;
19
+ private connectGlobalQuake;
20
+ }
package/lib/service.js ADDED
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DisasterWarningService = void 0;
4
+ const koishi_1 = require("koishi");
5
+ const ws_1 = require("ws");
6
+ const handlers_1 = require("./handlers");
7
+ const pusher_1 = require("./pusher");
8
+ const logger = new koishi_1.Logger('disaster-warning');
9
+ class DisasterWarningService {
10
+ constructor(ctx, config) {
11
+ this.connections = {};
12
+ this.reconnectTimers = {};
13
+ this.ctx = ctx;
14
+ this.config = config;
15
+ this.pusher = new pusher_1.MessagePushManager(ctx, config);
16
+ this.handlers = {
17
+ fanStudio: new handlers_1.FanStudioHandler(),
18
+ p2p: new handlers_1.P2PHandler(),
19
+ wolfx: new handlers_1.WolfxHandler('wolfx'),
20
+ globalQuake: new handlers_1.GlobalQuakeHandler()
21
+ };
22
+ }
23
+ async start() {
24
+ if (!this.config.enabled)
25
+ return;
26
+ logger.info('Disaster Warning Service starting...');
27
+ this.connectAll();
28
+ }
29
+ async stop() {
30
+ logger.info('Disaster Warning Service stopping...');
31
+ for (const key in this.connections) {
32
+ this.connections[key].close();
33
+ }
34
+ for (const key in this.reconnectTimers) {
35
+ clearTimeout(this.reconnectTimers[key]);
36
+ }
37
+ }
38
+ connectAll() {
39
+ if (this.config.data_sources.fan_studio.enabled) {
40
+ this.connectFanStudio();
41
+ }
42
+ if (this.config.data_sources.p2p_earthquake.enabled) {
43
+ this.connectP2P();
44
+ }
45
+ if (this.config.data_sources.wolfx.enabled) {
46
+ this.connectWolfx();
47
+ }
48
+ if (this.config.data_sources.global_quake.enabled) {
49
+ this.connectGlobalQuake();
50
+ }
51
+ }
52
+ connectWebSocket(name, url, onMessage) {
53
+ if (this.connections[name]) {
54
+ this.connections[name].close();
55
+ }
56
+ logger.info(`Connecting to ${name} at ${url}...`);
57
+ const ws = new ws_1.WebSocket(url);
58
+ ws.on('open', () => {
59
+ logger.info(`Connected to ${name}`);
60
+ });
61
+ ws.on('message', (data) => {
62
+ try {
63
+ const parsed = JSON.parse(data.toString());
64
+ onMessage(parsed);
65
+ }
66
+ catch (e) {
67
+ logger.warn(`Failed to parse message from ${name}:`, e);
68
+ }
69
+ });
70
+ ws.on('close', () => {
71
+ logger.warn(`Disconnected from ${name}, reconnecting in 10s...`);
72
+ delete this.connections[name];
73
+ this.reconnectTimers[name] = setTimeout(() => {
74
+ this.connectWebSocket(name, url, onMessage);
75
+ }, 10000);
76
+ });
77
+ ws.on('error', (err) => {
78
+ logger.error(`Error in ${name} connection:`, err);
79
+ });
80
+ this.connections[name] = ws;
81
+ }
82
+ async handleEvent(event) {
83
+ if (!event)
84
+ return;
85
+ await this.pusher.pushEvent(event);
86
+ }
87
+ connectFanStudio() {
88
+ const url = "wss://ws.fanstudio.tech/all";
89
+ this.connectWebSocket('fan_studio', url, (data) => {
90
+ const event = this.handlers.fanStudio.parseMessage(data);
91
+ this.handleEvent(event);
92
+ });
93
+ }
94
+ connectP2P() {
95
+ const url = "wss://api.p2pquake.net/v2/ws";
96
+ this.connectWebSocket('p2p', url, (data) => {
97
+ const event = this.handlers.p2p.parseMessage(data);
98
+ this.handleEvent(event);
99
+ });
100
+ }
101
+ connectWolfx() {
102
+ const wolfx_sources = {
103
+ "japan_jma_eew": "wss://ws-api.wolfx.jp/jma_eew",
104
+ "china_cenc_eew": "wss://ws-api.wolfx.jp/cenc_eew",
105
+ "taiwan_cwa_eew": "wss://ws-api.wolfx.jp/cwa_eew",
106
+ "japan_jma_earthquake": "wss://ws-api.wolfx.jp/jma_eqlist",
107
+ "china_cenc_earthquake": "wss://ws-api.wolfx.jp/cenc_eqlist",
108
+ };
109
+ for (const [key, url] of Object.entries(wolfx_sources)) {
110
+ if (this.config.data_sources.wolfx[key]) {
111
+ this.connectWebSocket(`wolfx_${key}`, url, (data) => {
112
+ const handler = new handlers_1.WolfxHandler(`wolfx_${key}`);
113
+ const event = handler.parseMessage(data);
114
+ this.handleEvent(event);
115
+ });
116
+ }
117
+ }
118
+ }
119
+ connectGlobalQuake() {
120
+ const url = "wss://gqm.aloys233.top/ws";
121
+ this.connectWebSocket('global_quake', url, (data) => {
122
+ const event = this.handlers.globalQuake.parseMessage(data);
123
+ this.handleEvent(event);
124
+ });
125
+ }
126
+ }
127
+ exports.DisasterWarningService = DisasterWarningService;
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "koishi-plugin-disaster-warning",
3
+ "description": "Disaster warning plugin for Koishi, supporting Earthquake, Tsunami, and Weather alerts from multiple sources.",
4
+ "version": "0.0.1",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index.d.ts",
7
+ "files": [
8
+ "lib",
9
+ "dist"
10
+ ],
11
+ "license": "MIT",
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "lint": "eslint src --ext .ts"
15
+ },
16
+ "keywords": [
17
+ "koishi",
18
+ "plugin",
19
+ "disaster",
20
+ "earthquake",
21
+ "warning"
22
+ ],
23
+ "peerDependencies": {
24
+ "koishi": "^4.18.0"
25
+ },
26
+ "dependencies": {
27
+ "ws": "^8.18.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^20.0.0",
31
+ "@types/ws": "^8.5.10",
32
+ "koishi": "^4.18.0",
33
+ "typescript": "^5.0.0"
34
+ }
35
+ }