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.
- package/lib/handlers/base.d.ts +9 -0
- package/lib/handlers/base.js +30 -0
- package/lib/handlers/fanstudio.d.ts +10 -0
- package/lib/handlers/fanstudio.js +155 -0
- package/lib/handlers/global_quake.d.ts +6 -0
- package/lib/handlers/global_quake.js +53 -0
- package/lib/handlers/index.d.ts +5 -0
- package/lib/handlers/index.js +21 -0
- package/lib/handlers/p2p.d.ts +10 -0
- package/lib/handlers/p2p.js +138 -0
- package/lib/handlers/wolfx.d.ts +10 -0
- package/lib/handlers/wolfx.js +149 -0
- package/lib/index.d.ts +55 -0
- package/lib/index.js +61 -0
- package/lib/models.d.ts +106 -0
- package/lib/models.js +55 -0
- package/lib/pusher.d.ts +17 -0
- package/lib/pusher.js +160 -0
- package/lib/service.d.ts +20 -0
- package/lib/service.js +127 -0
- package/package.json +35 -0
|
@@ -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,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,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
|
+
}
|
package/lib/models.d.ts
ADDED
|
@@ -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
|
+
}
|
package/lib/pusher.d.ts
ADDED
|
@@ -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;
|
package/lib/service.d.ts
ADDED
|
@@ -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
|
+
}
|