koishi-plugin-disaster-warning 0.0.10 → 0.1.0
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/commands.js +59 -30
- package/lib/handlers/wolfx.d.ts +10 -0
- package/lib/handlers/wolfx.js +24 -0
- package/lib/index.d.ts +12 -4
- package/lib/index.js +16 -11
- package/lib/models.d.ts +13 -0
- package/lib/models.js +42 -1
- package/lib/pusher.d.ts +5 -0
- package/lib/pusher.js +48 -43
- package/lib/service.d.ts +19 -10
- package/lib/service.js +157 -132
- package/package.json +3 -5
package/lib/commands.js
CHANGED
|
@@ -5,46 +5,75 @@ const koishi_1 = require("koishi");
|
|
|
5
5
|
const logger = new koishi_1.Logger('disaster-commands');
|
|
6
6
|
function applyCommands(ctx, config, service) {
|
|
7
7
|
ctx.command('disaster', '灾害预警插件');
|
|
8
|
+
// disaster.status — 查看各 WebSocket 连接状态
|
|
9
|
+
ctx.command('disaster.status', '查看各数据源连接状态')
|
|
10
|
+
.action(() => {
|
|
11
|
+
const status = service.getStatus();
|
|
12
|
+
const lines = ['📡 数据源连接状态:'];
|
|
13
|
+
for (const [name, s] of Object.entries(status)) {
|
|
14
|
+
const icon = s.connected ? '🟢' : '🔴';
|
|
15
|
+
const retry = s.retryCount > 0 ? ` (重试: ${s.retryCount})` : '';
|
|
16
|
+
lines.push(`${icon} ${name}${retry}`);
|
|
17
|
+
}
|
|
18
|
+
return lines.join('\n');
|
|
19
|
+
});
|
|
20
|
+
// disaster.test — 发送测试消息
|
|
8
21
|
ctx.command('disaster.test', '发送测试预警消息')
|
|
9
|
-
.action(
|
|
10
|
-
|
|
11
|
-
return;
|
|
12
|
-
await session.send('正在发送测试消息...');
|
|
13
|
-
const msg = [
|
|
22
|
+
.action(() => {
|
|
23
|
+
return [
|
|
14
24
|
'【测试消息】',
|
|
15
25
|
'这是一个测试用的地震预警消息。',
|
|
16
26
|
'震源:测试地点',
|
|
17
|
-
`时间:${new Date().toLocaleString()}`,
|
|
27
|
+
`时间:${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`,
|
|
18
28
|
'震级:M5.0',
|
|
19
29
|
'最大烈度:4.0'
|
|
20
30
|
].join('\n');
|
|
21
|
-
return msg;
|
|
22
31
|
});
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
// disaster.history — 查询最近地震记录
|
|
33
|
+
ctx.command('disaster.history [source:string] [count:number]', '查询最近地震记录')
|
|
34
|
+
.usage('source: cenc(中国)或 jma(日本),默认 cenc\ncount: 条数,默认 5,最多 10')
|
|
35
|
+
.action(async ({ session }, source = 'cenc', count = 5) => {
|
|
36
|
+
source = source.toLowerCase();
|
|
37
|
+
if (source !== 'cenc' && source !== 'jma') {
|
|
38
|
+
return '❌ 数据源只支持 cenc 或 jma';
|
|
39
|
+
}
|
|
40
|
+
count = Math.min(Math.max(1, count), 10);
|
|
41
|
+
// 先尝试从内存缓存读取(避免重复 HTTP 请求)
|
|
42
|
+
const cache = service.getEqListCache();
|
|
43
|
+
let data = cache[source];
|
|
44
|
+
// 缓存为空时实时拉取
|
|
45
|
+
if (!data || Object.keys(data).length === 0) {
|
|
46
|
+
try {
|
|
47
|
+
const url = source === 'cenc'
|
|
48
|
+
? 'https://api.wolfx.jp/cenc_eqlist.json'
|
|
49
|
+
: 'https://api.wolfx.jp/jma_eqlist.json';
|
|
50
|
+
data = await ctx.http.get(url);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
logger.error('Failed to fetch eqlist:', e);
|
|
54
|
+
return '❌ 获取数据失败,请检查网络或日志。';
|
|
37
55
|
}
|
|
38
|
-
if (list.length === 0)
|
|
39
|
-
return '未找到最近地震记录';
|
|
40
|
-
const messages = list.map(eq => {
|
|
41
|
-
return `时间: ${eq.time}\n地点: ${eq.location}\n震级: M${eq.magnitude}\n深度: ${eq.depth}km`;
|
|
42
|
-
});
|
|
43
|
-
return messages.join('\n\n');
|
|
44
56
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
if (!data)
|
|
58
|
+
return '❌ 未获取到数据';
|
|
59
|
+
const items = [];
|
|
60
|
+
let n = 1;
|
|
61
|
+
while (items.length < count && data[`No${n}`]) {
|
|
62
|
+
items.push(data[`No${n}`]);
|
|
63
|
+
n++;
|
|
48
64
|
}
|
|
65
|
+
if (!items.length)
|
|
66
|
+
return '暂无地震记录';
|
|
67
|
+
const sourceName = source === 'cenc' ? 'CENC 中国地震台网' : 'JMA 日本气象厅';
|
|
68
|
+
const lines = [`📋 最近 ${items.length} 条地震记录(${sourceName}):`, ''];
|
|
69
|
+
items.forEach((eq, i) => {
|
|
70
|
+
const intensity = eq.shindo
|
|
71
|
+
? `震度 ${eq.shindo}`
|
|
72
|
+
: eq.intensity
|
|
73
|
+
? `烈度 ${eq.intensity}`
|
|
74
|
+
: '';
|
|
75
|
+
lines.push(`[${i + 1}] ${eq.time}`, ` 📍 ${eq.location} M${eq.magnitude} 深度 ${eq.depth}${intensity ? ` ${intensity}` : ''}`, '');
|
|
76
|
+
});
|
|
77
|
+
return lines.join('\n').trim();
|
|
49
78
|
});
|
|
50
79
|
}
|
package/lib/handlers/wolfx.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { BaseDataHandler } from './base';
|
|
2
2
|
import { DisasterEvent } from '../models';
|
|
3
3
|
export declare class WolfxHandler extends BaseDataHandler {
|
|
4
|
+
private eqListCache;
|
|
4
5
|
constructor(sourceId: string);
|
|
6
|
+
getEqListCache(): {
|
|
7
|
+
cenc: Record<string, any>;
|
|
8
|
+
jma: Record<string, any>;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* 解析 Wolfx HTTP eqlist JSON(cenc 或 jma),
|
|
12
|
+
* 提取最新一条记录作为 DisasterEvent 推送。
|
|
13
|
+
*/
|
|
14
|
+
parseEqList(data: any, type: 'cenc' | 'jma'): DisasterEvent | null;
|
|
5
15
|
parseMessage(data: any): DisasterEvent | null;
|
|
6
16
|
private parseJMAEEW;
|
|
7
17
|
private parseCENCEEW;
|
package/lib/handlers/wolfx.js
CHANGED
|
@@ -6,6 +6,30 @@ const models_1 = require("../models");
|
|
|
6
6
|
class WolfxHandler extends base_1.BaseDataHandler {
|
|
7
7
|
constructor(sourceId) {
|
|
8
8
|
super(sourceId);
|
|
9
|
+
// HTTP 轮询缓存(用于 commands history 复用,避免重复请求)
|
|
10
|
+
this.eqListCache = { cenc: {}, jma: {} };
|
|
11
|
+
}
|
|
12
|
+
getEqListCache() {
|
|
13
|
+
return this.eqListCache;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 解析 Wolfx HTTP eqlist JSON(cenc 或 jma),
|
|
17
|
+
* 提取最新一条记录作为 DisasterEvent 推送。
|
|
18
|
+
*/
|
|
19
|
+
parseEqList(data, type) {
|
|
20
|
+
try {
|
|
21
|
+
// 更新缓存
|
|
22
|
+
this.eqListCache[type] = data;
|
|
23
|
+
// 提取 No1(最新一条)
|
|
24
|
+
const eqInfo = data['No1'];
|
|
25
|
+
if (!eqInfo || typeof eqInfo !== 'object')
|
|
26
|
+
return null;
|
|
27
|
+
return type === 'cenc' ? this.parseCENCEqList(data) : this.parseJMAEqList(data);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
this.logger.error(`[${this.sourceId}] parseEqList error:`, e);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
9
33
|
}
|
|
10
34
|
parseMessage(data) {
|
|
11
35
|
try {
|
package/lib/index.d.ts
CHANGED
|
@@ -19,10 +19,18 @@ export interface Config {
|
|
|
19
19
|
japan: boolean;
|
|
20
20
|
global: boolean;
|
|
21
21
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
data_sources: {
|
|
23
|
+
fan_studio: boolean;
|
|
24
|
+
wolfx: boolean;
|
|
25
|
+
p2p: boolean;
|
|
26
|
+
global_quake: boolean;
|
|
27
|
+
};
|
|
28
|
+
filter: {
|
|
29
|
+
min_magnitude_absolute: number;
|
|
30
|
+
min_magnitude_for_push: number;
|
|
31
|
+
min_intensity_for_push: number;
|
|
32
|
+
min_scale_for_push: number;
|
|
33
|
+
};
|
|
26
34
|
}
|
|
27
35
|
export declare const Config: Schema<Config>;
|
|
28
36
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -12,7 +12,9 @@ exports.inject = {
|
|
|
12
12
|
};
|
|
13
13
|
exports.Config = koishi_1.Schema.object({
|
|
14
14
|
enabled: koishi_1.Schema.boolean().default(true).description('启用灾害预警插件'),
|
|
15
|
-
target_groups: koishi_1.Schema.array(koishi_1.Schema.string())
|
|
15
|
+
target_groups: koishi_1.Schema.array(koishi_1.Schema.string())
|
|
16
|
+
.default([])
|
|
17
|
+
.description('推送目标群号列表,直接填写群号即可,例如 123456789'),
|
|
16
18
|
data_types: koishi_1.Schema.object({
|
|
17
19
|
earthquake_warning: koishi_1.Schema.boolean().default(true).description('地震预警(实时速报,震前预警)'),
|
|
18
20
|
earthquake_info: koishi_1.Schema.boolean().default(true).description('地震信息(震后测定报告)'),
|
|
@@ -23,17 +25,20 @@ exports.Config = koishi_1.Schema.object({
|
|
|
23
25
|
china: koishi_1.Schema.boolean().default(true).description('中国大陆'),
|
|
24
26
|
taiwan: koishi_1.Schema.boolean().default(true).description('台湾'),
|
|
25
27
|
japan: koishi_1.Schema.boolean().default(true).description('日本'),
|
|
26
|
-
global: koishi_1.Schema.boolean().default(false).description('全球(USGS/GlobalQuake)'),
|
|
28
|
+
global: koishi_1.Schema.boolean().default(false).description('全球(USGS / GlobalQuake)'),
|
|
27
29
|
}).description('接收的地区'),
|
|
28
|
-
|
|
29
|
-
koishi_1.Schema.
|
|
30
|
-
koishi_1.Schema.
|
|
31
|
-
koishi_1.Schema.
|
|
32
|
-
koishi_1.Schema.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
data_sources: koishi_1.Schema.object({
|
|
31
|
+
fan_studio: koishi_1.Schema.boolean().default(true).description('FAN Studio(中国预警/台湾/USGS/日本/气象/海啸)'),
|
|
32
|
+
wolfx: koishi_1.Schema.boolean().default(true).description('Wolfx(中国/台湾/日本 EEW,以及中国/日本地震列表)'),
|
|
33
|
+
p2p: koishi_1.Schema.boolean().default(true).description('P2P地震情報(日本 EEW / 地震情报 / 海啸)'),
|
|
34
|
+
global_quake: koishi_1.Schema.boolean().default(false).description('GlobalQuake(全球实时地震,流量较大)'),
|
|
35
|
+
}).description('数据源开关(可单独禁用某个来源)'),
|
|
36
|
+
filter: koishi_1.Schema.object({
|
|
37
|
+
min_magnitude_absolute: koishi_1.Schema.number().default(3.0).description('绝对过滤震级:低于此震级直接丢弃(不推送)'),
|
|
38
|
+
min_magnitude_for_push: koishi_1.Schema.number().default(4.0).description('推送震级门槛:震级达到此值则推送'),
|
|
39
|
+
min_intensity_for_push: koishi_1.Schema.number().default(4.0).description('推送烈度门槛(中国):最大烈度达到此值则推送'),
|
|
40
|
+
min_scale_for_push: koishi_1.Schema.number().default(4.0).description('推送震度门槛(日本):最大震度达到此值则推送(4 = 震度4)'),
|
|
41
|
+
}).description('过滤阈值(地震类事件,海啸/气象不受此限制)'),
|
|
37
42
|
});
|
|
38
43
|
function apply(ctx, config) {
|
|
39
44
|
const service = new service_1.DisasterWarningService(ctx, config);
|
package/lib/models.d.ts
CHANGED
|
@@ -104,3 +104,16 @@ export interface DisasterEvent {
|
|
|
104
104
|
push_count: number;
|
|
105
105
|
raw_data: any;
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* 跨数据源事件去重器
|
|
109
|
+
* 用 place+magnitude+分钟桶 作为指纹,窗口期内同一事件只推一次
|
|
110
|
+
*/
|
|
111
|
+
export declare class EventDeduplicator {
|
|
112
|
+
private seen;
|
|
113
|
+
private windowMs;
|
|
114
|
+
constructor(windowMs?: number);
|
|
115
|
+
/** 返回 true 表示已见过(应丢弃),false 表示首次(应推送) */
|
|
116
|
+
isDuplicate(event: DisasterEvent): boolean;
|
|
117
|
+
private fingerprint;
|
|
118
|
+
private evict;
|
|
119
|
+
}
|
package/lib/models.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DATA_SOURCE_MAPPING = exports.DataSource = exports.DisasterType = void 0;
|
|
3
|
+
exports.EventDeduplicator = exports.DATA_SOURCE_MAPPING = exports.DataSource = exports.DisasterType = void 0;
|
|
4
4
|
exports.getDataSourceFromId = getDataSourceFromId;
|
|
5
5
|
var DisasterType;
|
|
6
6
|
(function (DisasterType) {
|
|
@@ -53,3 +53,44 @@ exports.DATA_SOURCE_MAPPING = {
|
|
|
53
53
|
function getDataSourceFromId(id) {
|
|
54
54
|
return exports.DATA_SOURCE_MAPPING[id];
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* 跨数据源事件去重器
|
|
58
|
+
* 用 place+magnitude+分钟桶 作为指纹,窗口期内同一事件只推一次
|
|
59
|
+
*/
|
|
60
|
+
class EventDeduplicator {
|
|
61
|
+
constructor(windowMs = 5 * 60 * 1000) {
|
|
62
|
+
// fingerprint -> first-seen timestamp (ms)
|
|
63
|
+
this.seen = new Map();
|
|
64
|
+
this.windowMs = windowMs;
|
|
65
|
+
}
|
|
66
|
+
/** 返回 true 表示已见过(应丢弃),false 表示首次(应推送) */
|
|
67
|
+
isDuplicate(event) {
|
|
68
|
+
this.evict();
|
|
69
|
+
const fp = this.fingerprint(event);
|
|
70
|
+
if (this.seen.has(fp))
|
|
71
|
+
return true;
|
|
72
|
+
this.seen.set(fp, Date.now());
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
fingerprint(event) {
|
|
76
|
+
const data = event.data;
|
|
77
|
+
if (event.disaster_type === DisasterType.EARTHQUAKE || event.disaster_type === DisasterType.EARTHQUAKE_WARNING) {
|
|
78
|
+
// 优先用 event_id,相同机构的多数据源共享同一 event_id
|
|
79
|
+
if (data.event_id)
|
|
80
|
+
return `eq:${data.event_id}`;
|
|
81
|
+
// 降级:地点 + 震级 + 分钟桶
|
|
82
|
+
const bucket = data.shock_time ? data.shock_time.slice(0, 16) : 'unknown';
|
|
83
|
+
return `eq:${data.place_name}|${data.magnitude?.toFixed(1)}|${bucket}`;
|
|
84
|
+
}
|
|
85
|
+
// 海啸/气象用 id 即可
|
|
86
|
+
return `${event.disaster_type}:${event.id}`;
|
|
87
|
+
}
|
|
88
|
+
evict() {
|
|
89
|
+
const cutoff = Date.now() - this.windowMs;
|
|
90
|
+
for (const [fp, ts] of this.seen) {
|
|
91
|
+
if (ts < cutoff)
|
|
92
|
+
this.seen.delete(fp);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.EventDeduplicator = EventDeduplicator;
|
package/lib/pusher.d.ts
CHANGED
|
@@ -14,5 +14,10 @@ export declare class MessagePushManager {
|
|
|
14
14
|
private formatTime;
|
|
15
15
|
private formatScale;
|
|
16
16
|
private formatSource;
|
|
17
|
+
/**
|
|
18
|
+
* 直接向每个群号广播。
|
|
19
|
+
* target_groups 里存的是纯群号(如 "123456789"),
|
|
20
|
+
* 通过 ctx.bots 遍历所有在线 Bot 发送,这样无需硬编码平台前缀。
|
|
21
|
+
*/
|
|
17
22
|
private broadcast;
|
|
18
23
|
}
|
package/lib/pusher.js
CHANGED
|
@@ -4,10 +4,6 @@ exports.MessagePushManager = void 0;
|
|
|
4
4
|
const koishi_1 = require("koishi");
|
|
5
5
|
const models_1 = require("./models");
|
|
6
6
|
const logger = new koishi_1.Logger('disaster-pusher');
|
|
7
|
-
// Hardcoded filter thresholds - no user configuration needed
|
|
8
|
-
const FILTER_THRESHOLDS = {
|
|
9
|
-
MIN_MAGNITUDE_ABSOLUTE: 3.0, // Always ignore earthquakes below M3.0
|
|
10
|
-
};
|
|
11
7
|
class MessagePushManager {
|
|
12
8
|
constructor(ctx, config) {
|
|
13
9
|
this.ctx = ctx;
|
|
@@ -15,48 +11,40 @@ class MessagePushManager {
|
|
|
15
11
|
}
|
|
16
12
|
async pushEvent(event) {
|
|
17
13
|
if (this.shouldFilter(event)) {
|
|
18
|
-
logger.debug(`Event ${event.id} filtered.`);
|
|
14
|
+
logger.debug(`Event ${event.id} filtered by threshold.`);
|
|
19
15
|
return;
|
|
20
16
|
}
|
|
21
17
|
const message = this.formatMessage(event);
|
|
22
18
|
if (!message)
|
|
23
19
|
return;
|
|
24
|
-
logger.info(`Pushing event ${event.id} to ${this.config.target_groups.length}
|
|
20
|
+
logger.info(`Pushing event ${event.id} to ${this.config.target_groups.length} group(s).`);
|
|
25
21
|
await this.broadcast(message);
|
|
26
22
|
}
|
|
23
|
+
// ---- 过滤逻辑 --------------------------------------------------------
|
|
27
24
|
shouldFilter(event) {
|
|
28
|
-
// Only filter earthquake events
|
|
29
25
|
if (event.disaster_type !== models_1.DisasterType.EARTHQUAKE && event.disaster_type !== models_1.DisasterType.EARTHQUAKE_WARNING) {
|
|
30
|
-
return false;
|
|
26
|
+
return false;
|
|
31
27
|
}
|
|
32
28
|
const data = event.data;
|
|
33
|
-
const {
|
|
34
|
-
|
|
35
|
-
// Always filter out very small earthquakes
|
|
36
|
-
if (data.magnitude !== undefined && data.magnitude < MIN_MAGNITUDE_ABSOLUTE) {
|
|
29
|
+
const { min_magnitude_absolute, min_magnitude_for_push, min_intensity_for_push, min_scale_for_push } = this.config.filter;
|
|
30
|
+
if (data.magnitude !== undefined && data.magnitude < min_magnitude_absolute)
|
|
37
31
|
return true;
|
|
38
|
-
|
|
39
|
-
// Pass if magnitude is significant
|
|
40
|
-
if (data.magnitude !== undefined && data.magnitude >= min_magnitude) {
|
|
32
|
+
if (data.magnitude !== undefined && data.magnitude >= min_magnitude_for_push)
|
|
41
33
|
return false;
|
|
42
|
-
|
|
43
|
-
// Pass if intensity is significant (Chinese sources)
|
|
44
|
-
if (data.intensity !== undefined && data.intensity >= min_intensity) {
|
|
34
|
+
if (data.intensity !== undefined && data.intensity >= min_intensity_for_push)
|
|
45
35
|
return false;
|
|
46
|
-
|
|
47
|
-
// Pass if scale is significant (Japanese sources)
|
|
48
|
-
if (data.scale !== undefined && data.scale >= min_scale) {
|
|
36
|
+
if (data.scale !== undefined && data.scale >= min_scale_for_push)
|
|
49
37
|
return false;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
38
|
+
// M 在 [min_magnitude_absolute, min_magnitude_for_push) 且无显著烈度/震度 → 过滤
|
|
39
|
+
if (data.magnitude !== undefined &&
|
|
40
|
+
data.magnitude >= min_magnitude_absolute &&
|
|
41
|
+
data.magnitude < min_magnitude_for_push &&
|
|
42
|
+
data.intensity === undefined && data.scale === undefined) {
|
|
43
|
+
return true;
|
|
57
44
|
}
|
|
58
45
|
return false;
|
|
59
46
|
}
|
|
47
|
+
// ---- 格式化 ----------------------------------------------------------
|
|
60
48
|
formatMessage(event) {
|
|
61
49
|
switch (event.disaster_type) {
|
|
62
50
|
case models_1.DisasterType.EARTHQUAKE:
|
|
@@ -79,12 +67,10 @@ class MessagePushManager {
|
|
|
79
67
|
msg += `🕐 时间:${this.formatTime(data.shock_time)}\n`;
|
|
80
68
|
msg += `📊 震级:M${data.magnitude?.toFixed(1) || '未知'}\n`;
|
|
81
69
|
msg += `📏 深度:${data.depth !== undefined ? data.depth + 'km' : '未知'}\n`;
|
|
82
|
-
if (data.intensity !== undefined)
|
|
70
|
+
if (data.intensity !== undefined)
|
|
83
71
|
msg += `🔥 最大烈度:${data.intensity.toFixed(1)}\n`;
|
|
84
|
-
|
|
85
|
-
if (data.scale !== undefined) {
|
|
72
|
+
if (data.scale !== undefined)
|
|
86
73
|
msg += `🎚️ 最大震度:${this.formatScale(data.scale)}\n`;
|
|
87
|
-
}
|
|
88
74
|
msg += `📡 数据源:${this.formatSource(data.source)}`;
|
|
89
75
|
return msg;
|
|
90
76
|
}
|
|
@@ -92,7 +78,7 @@ class MessagePushManager {
|
|
|
92
78
|
let msg = `🌊 【海啸预警】${data.title}\n`;
|
|
93
79
|
msg += `⚠️ 级别:${data.level}\n`;
|
|
94
80
|
msg += `🏛️ 发布单位:${data.org_unit}\n`;
|
|
95
|
-
if (data.forecasts
|
|
81
|
+
if (data.forecasts?.length) {
|
|
96
82
|
msg += `📍 预报区域:\n`;
|
|
97
83
|
data.forecasts.slice(0, 5).forEach((f) => {
|
|
98
84
|
msg += ` • ${f.name || f.areaName}: ${f.grade || f.level}\n`;
|
|
@@ -109,6 +95,7 @@ class MessagePushManager {
|
|
|
109
95
|
msg += `📝 详情:${data.description}\n`;
|
|
110
96
|
return msg;
|
|
111
97
|
}
|
|
98
|
+
// ---- 工具 ------------------------------------------------------------
|
|
112
99
|
formatTime(isoStr) {
|
|
113
100
|
try {
|
|
114
101
|
return new Date(isoStr).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
|
@@ -118,14 +105,14 @@ class MessagePushManager {
|
|
|
118
105
|
}
|
|
119
106
|
}
|
|
120
107
|
formatScale(scale) {
|
|
121
|
-
const
|
|
108
|
+
const map = {
|
|
122
109
|
1.0: '1', 2.0: '2', 3.0: '3', 4.0: '4',
|
|
123
110
|
4.5: '5弱', 5.0: '5强', 5.5: '6弱', 6.0: '6强', 7.0: '7'
|
|
124
111
|
};
|
|
125
|
-
return
|
|
112
|
+
return map[scale] ?? scale.toString();
|
|
126
113
|
}
|
|
127
114
|
formatSource(source) {
|
|
128
|
-
const
|
|
115
|
+
const map = {
|
|
129
116
|
'fan_studio_cea': '中国地震预警网',
|
|
130
117
|
'fan_studio_cwa': '台湾中央气象署',
|
|
131
118
|
'fan_studio_cenc': '中国地震台网',
|
|
@@ -143,16 +130,34 @@ class MessagePushManager {
|
|
|
143
130
|
'wolfx_cenc_eq': 'Wolfx-CENC',
|
|
144
131
|
'global_quake': 'GlobalQuake'
|
|
145
132
|
};
|
|
146
|
-
return
|
|
133
|
+
return map[source] ?? source;
|
|
147
134
|
}
|
|
135
|
+
// ---- 推送 ------------------------------------------------------------
|
|
136
|
+
/**
|
|
137
|
+
* 直接向每个群号广播。
|
|
138
|
+
* target_groups 里存的是纯群号(如 "123456789"),
|
|
139
|
+
* 通过 ctx.bots 遍历所有在线 Bot 发送,这样无需硬编码平台前缀。
|
|
140
|
+
*/
|
|
148
141
|
async broadcast(message) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
142
|
+
if (!this.config.target_groups.length)
|
|
143
|
+
return;
|
|
144
|
+
for (const gid of this.config.target_groups) {
|
|
145
|
+
const groupId = String(gid).trim();
|
|
146
|
+
if (!groupId)
|
|
147
|
+
continue;
|
|
148
|
+
let sent = false;
|
|
149
|
+
for (const bot of this.ctx.bots) {
|
|
150
|
+
try {
|
|
151
|
+
await bot.sendMessage(groupId, message);
|
|
152
|
+
sent = true;
|
|
153
|
+
break; // 同一群由第一个能发送的 bot 处理即可
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// 该 bot 不在此群,继续尝试下一个
|
|
157
|
+
}
|
|
153
158
|
}
|
|
154
|
-
|
|
155
|
-
logger.
|
|
159
|
+
if (!sent) {
|
|
160
|
+
logger.warn(`Could not send to group ${groupId}: no available bot.`);
|
|
156
161
|
}
|
|
157
162
|
}
|
|
158
163
|
}
|
package/lib/service.d.ts
CHANGED
|
@@ -2,22 +2,31 @@ import { Context } from 'koishi';
|
|
|
2
2
|
import { Config } from './index';
|
|
3
3
|
export declare class DisasterWarningService {
|
|
4
4
|
private config;
|
|
5
|
-
private connections;
|
|
6
|
-
private reconnectTimers;
|
|
7
|
-
private pusher;
|
|
8
5
|
private ctx;
|
|
9
6
|
private stopped;
|
|
7
|
+
private pusher;
|
|
8
|
+
private deduplicator;
|
|
10
9
|
private handlers;
|
|
11
|
-
private
|
|
10
|
+
private wolfxHandler;
|
|
11
|
+
private connections;
|
|
12
|
+
private wolfxHttpTimer;
|
|
12
13
|
constructor(ctx: Context, config: Config);
|
|
13
14
|
start(): Promise<void>;
|
|
14
15
|
stop(): Promise<void>;
|
|
15
|
-
private
|
|
16
|
-
private
|
|
16
|
+
private connectAll;
|
|
17
|
+
private openConnection;
|
|
18
|
+
private doConnect;
|
|
19
|
+
private startWolfxHttpPoller;
|
|
17
20
|
private handleEvent;
|
|
18
21
|
private shouldPushEvent;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
getStatus(): Record<string, {
|
|
23
|
+
connected: boolean;
|
|
24
|
+
retryCount: number;
|
|
25
|
+
url: string;
|
|
26
|
+
}>;
|
|
27
|
+
/** 给 commands 用:拿到 wolfxHandler 最新的 eqlist 缓存 */
|
|
28
|
+
getEqListCache(): {
|
|
29
|
+
cenc: Record<string, any>;
|
|
30
|
+
jma: Record<string, any>;
|
|
31
|
+
};
|
|
23
32
|
}
|
package/lib/service.js
CHANGED
|
@@ -7,12 +7,20 @@ const models_1 = require("./models");
|
|
|
7
7
|
const handlers_1 = require("./handlers");
|
|
8
8
|
const pusher_1 = require("./pusher");
|
|
9
9
|
const logger = new koishi_1.Logger('disaster-warning');
|
|
10
|
+
// Wolfx HTTP 列表获取间隔(5 分钟)
|
|
11
|
+
const WOLFX_HTTP_INTERVAL_MS = 5 * 60 * 1000;
|
|
12
|
+
// FanStudio 备用服务器
|
|
13
|
+
const FAN_STUDIO_PRIMARY = 'wss://ws.fanstudio.tech/all';
|
|
14
|
+
const FAN_STUDIO_BACKUP = 'wss://ws.fanstudio.hk/all';
|
|
15
|
+
// WebSocket 重连延迟(秒),超过 MAX_RETRY 次后切备用服务器
|
|
16
|
+
const RECONNECT_DELAY_MS = 10000;
|
|
17
|
+
const FALLBACK_RETRY_THRESHOLD = 5;
|
|
10
18
|
class DisasterWarningService {
|
|
11
19
|
constructor(ctx, config) {
|
|
20
|
+
this.stopped = false;
|
|
21
|
+
this.deduplicator = new models_1.EventDeduplicator();
|
|
12
22
|
this.connections = {};
|
|
13
|
-
this.
|
|
14
|
-
this.stopped = false; // Flag to prevent reconnection after stop
|
|
15
|
-
this.wolfxHandlers = {};
|
|
23
|
+
this.wolfxHttpTimer = null;
|
|
16
24
|
this.ctx = ctx;
|
|
17
25
|
this.config = config;
|
|
18
26
|
this.pusher = new pusher_1.MessagePushManager(ctx, config);
|
|
@@ -21,135 +29,163 @@ class DisasterWarningService {
|
|
|
21
29
|
p2p: new handlers_1.P2PHandler(),
|
|
22
30
|
globalQuake: new handlers_1.GlobalQuakeHandler()
|
|
23
31
|
};
|
|
24
|
-
|
|
25
|
-
const wolfxKeys = ['jma_eew', 'cenc_eew', 'cwa_eew', 'jma_eqlist', 'cenc_eqlist'];
|
|
26
|
-
for (const key of wolfxKeys) {
|
|
27
|
-
this.wolfxHandlers[key] = new handlers_1.WolfxHandler(`wolfx_${key}`);
|
|
28
|
-
}
|
|
32
|
+
this.wolfxHandler = new handlers_1.WolfxHandler('wolfx_all');
|
|
29
33
|
}
|
|
30
34
|
async start() {
|
|
31
35
|
if (!this.config.enabled)
|
|
32
36
|
return;
|
|
33
|
-
this.stopped = false;
|
|
37
|
+
this.stopped = false;
|
|
34
38
|
logger.info('Disaster Warning Service starting...');
|
|
35
|
-
this.
|
|
39
|
+
this.connectAll();
|
|
40
|
+
this.startWolfxHttpPoller();
|
|
36
41
|
}
|
|
37
42
|
async stop() {
|
|
38
43
|
logger.info('Disaster Warning Service stopping...');
|
|
39
|
-
this.stopped = true;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
delete this.reconnectTimers[key];
|
|
44
|
+
this.stopped = true;
|
|
45
|
+
if (this.wolfxHttpTimer) {
|
|
46
|
+
clearInterval(this.wolfxHttpTimer);
|
|
47
|
+
this.wolfxHttpTimer = null;
|
|
44
48
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
ws
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
for (const name in this.connections) {
|
|
50
|
+
const entry = this.connections[name];
|
|
51
|
+
if (entry.reconnectTimer)
|
|
52
|
+
clearTimeout(entry.reconnectTimer);
|
|
53
|
+
if (entry.ws) {
|
|
54
|
+
entry.ws.removeAllListeners('close');
|
|
55
|
+
entry.ws.close();
|
|
56
|
+
}
|
|
53
57
|
}
|
|
58
|
+
this.connections = {};
|
|
54
59
|
logger.info('Disaster Warning Service stopped.');
|
|
55
60
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (needsChina && data_types.earthquake_warning) {
|
|
70
|
-
this.connectWolfxSource('cenc_eew', 'wss://ws-api.wolfx.jp/cenc_eew');
|
|
71
|
-
}
|
|
72
|
-
if (needsTaiwan && data_types.earthquake_warning) {
|
|
73
|
-
this.connectWolfxSource('cwa_eew', 'wss://ws-api.wolfx.jp/cwa_eew');
|
|
74
|
-
}
|
|
75
|
-
if (needsJapan && data_types.earthquake_info) {
|
|
76
|
-
this.connectWolfxSource('jma_eqlist', 'wss://ws-api.wolfx.jp/jma_eqlist');
|
|
77
|
-
}
|
|
78
|
-
if (needsChina && data_types.earthquake_info) {
|
|
79
|
-
this.connectWolfxSource('cenc_eqlist', 'wss://ws-api.wolfx.jp/cenc_eqlist');
|
|
61
|
+
// ---- 连接调度 --------------------------------------------------------
|
|
62
|
+
connectAll() {
|
|
63
|
+
const { regions, data_types, data_sources } = this.config;
|
|
64
|
+
// FAN Studio — 单连接 /all,覆盖中国/台湾/USGS/日本/气象/海啸
|
|
65
|
+
if (data_sources.fan_studio) {
|
|
66
|
+
const needFanStudio = (regions.china && (data_types.earthquake_warning || data_types.earthquake_info || data_types.weather_alarm || data_types.tsunami_warning)) ||
|
|
67
|
+
(regions.taiwan && (data_types.earthquake_warning || data_types.earthquake_info)) ||
|
|
68
|
+
(regions.japan && (data_types.earthquake_warning || data_types.earthquake_info)) ||
|
|
69
|
+
(regions.global && data_types.earthquake_info);
|
|
70
|
+
if (needFanStudio) {
|
|
71
|
+
this.openConnection('fan_studio', FAN_STUDIO_PRIMARY, FAN_STUDIO_BACKUP, (data) => {
|
|
72
|
+
this.handleEvent(this.handlers.fanStudio.parseMessage(data));
|
|
73
|
+
});
|
|
80
74
|
}
|
|
81
75
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
76
|
+
// P2P — 日本 EEW / 地震情报 / 海啸
|
|
77
|
+
if (data_sources.p2p && regions.japan &&
|
|
78
|
+
(data_types.earthquake_warning || data_types.earthquake_info || data_types.tsunami_warning)) {
|
|
79
|
+
this.openConnection('p2p', 'wss://api.p2pquake.net/v2/ws', undefined, (data) => {
|
|
80
|
+
this.handleEvent(this.handlers.p2p.parseMessage(data));
|
|
81
|
+
});
|
|
89
82
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
this.
|
|
97
|
-
}
|
|
83
|
+
// Wolfx — /all_eew 合并端点,接收中国/台湾/日本 EEW
|
|
84
|
+
// eqlist 改为 HTTP 轮询(见 startWolfxHttpPoller)
|
|
85
|
+
if (data_sources.wolfx &&
|
|
86
|
+
(data_types.earthquake_warning) &&
|
|
87
|
+
(regions.china || regions.taiwan || regions.japan)) {
|
|
88
|
+
this.openConnection('wolfx_eew', 'wss://ws-api.wolfx.jp/all_eew', undefined, (data) => {
|
|
89
|
+
this.handleEvent(this.wolfxHandler.parseMessage(data));
|
|
90
|
+
});
|
|
98
91
|
}
|
|
99
|
-
//
|
|
100
|
-
if (
|
|
101
|
-
this.
|
|
92
|
+
// GlobalQuake — 全球实时预警
|
|
93
|
+
if (data_sources.global_quake && regions.global && data_types.earthquake_warning) {
|
|
94
|
+
this.openConnection('global_quake', 'wss://gqm.aloys233.top/ws', undefined, (data) => {
|
|
95
|
+
this.handleEvent(this.handlers.globalQuake.parseMessage(data));
|
|
96
|
+
});
|
|
102
97
|
}
|
|
103
98
|
}
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
// ---- WebSocket 生命周期 ----------------------------------------------
|
|
100
|
+
openConnection(name, url, backupUrl, onMessage) {
|
|
106
101
|
if (this.stopped)
|
|
107
102
|
return;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
103
|
+
const entry = {
|
|
104
|
+
url,
|
|
105
|
+
backupUrl,
|
|
106
|
+
retryCount: 0,
|
|
107
|
+
ws: null,
|
|
108
|
+
reconnectTimer: null
|
|
109
|
+
};
|
|
110
|
+
this.connections[name] = entry;
|
|
111
|
+
this.doConnect(name, onMessage);
|
|
112
|
+
}
|
|
113
|
+
doConnect(name, onMessage) {
|
|
114
|
+
if (this.stopped)
|
|
115
|
+
return;
|
|
116
|
+
const entry = this.connections[name];
|
|
117
|
+
if (!entry)
|
|
118
|
+
return;
|
|
119
|
+
// 超过阈值切换到备用服务器
|
|
120
|
+
const useBackup = entry.backupUrl && entry.retryCount >= FALLBACK_RETRY_THRESHOLD;
|
|
121
|
+
const url = useBackup ? entry.backupUrl : entry.url;
|
|
122
|
+
logger.info(`[${name}] Connecting to ${url}${useBackup ? ' (backup)' : ''}...`);
|
|
113
123
|
const ws = new ws_1.WebSocket(url);
|
|
124
|
+
entry.ws = ws;
|
|
114
125
|
ws.on('open', () => {
|
|
115
|
-
logger.info(`
|
|
126
|
+
logger.info(`[${name}] Connected`);
|
|
127
|
+
entry.retryCount = 0;
|
|
116
128
|
});
|
|
117
|
-
ws.on('message', (
|
|
118
|
-
if (this.stopped)
|
|
119
|
-
return;
|
|
129
|
+
ws.on('message', (raw) => {
|
|
120
130
|
try {
|
|
121
|
-
|
|
122
|
-
onMessage(parsed);
|
|
131
|
+
onMessage(JSON.parse(raw.toString()));
|
|
123
132
|
}
|
|
124
133
|
catch (e) {
|
|
125
|
-
logger.warn(`
|
|
134
|
+
logger.warn(`[${name}] Parse error:`, e);
|
|
126
135
|
}
|
|
127
136
|
});
|
|
128
137
|
ws.on('close', () => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
this.connectWebSocket(name, url, onMessage);
|
|
135
|
-
}, 10000);
|
|
136
|
-
}
|
|
138
|
+
if (this.stopped)
|
|
139
|
+
return;
|
|
140
|
+
entry.retryCount++;
|
|
141
|
+
logger.warn(`[${name}] Disconnected (retry #${entry.retryCount}), reconnecting in ${RECONNECT_DELAY_MS / 1000}s...`);
|
|
142
|
+
entry.reconnectTimer = setTimeout(() => this.doConnect(name, onMessage), RECONNECT_DELAY_MS);
|
|
137
143
|
});
|
|
138
144
|
ws.on('error', (err) => {
|
|
139
|
-
logger.error(`
|
|
145
|
+
logger.error(`[${name}] Error:`, err);
|
|
140
146
|
});
|
|
141
|
-
this.connections[name] = ws;
|
|
142
147
|
}
|
|
148
|
+
// ---- Wolfx HTTP 轮询(地震列表) -------------------------------------
|
|
149
|
+
startWolfxHttpPoller() {
|
|
150
|
+
if (!this.config.data_sources.wolfx || !this.config.data_types.earthquake_info)
|
|
151
|
+
return;
|
|
152
|
+
const poll = async () => {
|
|
153
|
+
if (this.stopped)
|
|
154
|
+
return;
|
|
155
|
+
try {
|
|
156
|
+
if (this.config.regions.china) {
|
|
157
|
+
const data = await this.ctx.http.get('https://api.wolfx.jp/cenc_eqlist.json');
|
|
158
|
+
if (data)
|
|
159
|
+
this.handleEvent(this.wolfxHandler.parseEqList(data, 'cenc'));
|
|
160
|
+
}
|
|
161
|
+
if (this.config.regions.japan) {
|
|
162
|
+
const data = await this.ctx.http.get('https://api.wolfx.jp/jma_eqlist.json');
|
|
163
|
+
if (data)
|
|
164
|
+
this.handleEvent(this.wolfxHandler.parseEqList(data, 'jma'));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
logger.warn('[wolfx_http] Fetch failed:', e);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
// 立即执行一次,然后每 5 分钟轮询
|
|
172
|
+
poll();
|
|
173
|
+
this.wolfxHttpTimer = setInterval(poll, WOLFX_HTTP_INTERVAL_MS);
|
|
174
|
+
}
|
|
175
|
+
// ---- 事件处理 --------------------------------------------------------
|
|
143
176
|
async handleEvent(event) {
|
|
144
177
|
if (!event)
|
|
145
178
|
return;
|
|
146
179
|
if (!this.shouldPushEvent(event))
|
|
147
180
|
return;
|
|
181
|
+
if (this.deduplicator.isDuplicate(event)) {
|
|
182
|
+
logger.debug(`[dedup] Skipping duplicate event: ${event.id}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
148
185
|
await this.pusher.pushEvent(event);
|
|
149
186
|
}
|
|
150
187
|
shouldPushEvent(event) {
|
|
151
188
|
const { data_types, regions } = this.config;
|
|
152
|
-
// Check disaster type
|
|
153
189
|
const isEarthquakeWarning = event.disaster_type === models_1.DisasterType.EARTHQUAKE_WARNING;
|
|
154
190
|
const isEarthquakeInfo = event.disaster_type === models_1.DisasterType.EARTHQUAKE;
|
|
155
191
|
const isTsunami = event.disaster_type === models_1.DisasterType.TSUNAMI;
|
|
@@ -162,59 +198,48 @@ class DisasterWarningService {
|
|
|
162
198
|
return false;
|
|
163
199
|
if (isWeather && !data_types.weather_alarm)
|
|
164
200
|
return false;
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
const isJapanSource = [
|
|
201
|
+
const src = event.source;
|
|
202
|
+
const japanSources = [
|
|
168
203
|
models_1.DataSource.P2P_EEW, models_1.DataSource.P2P_EARTHQUAKE, models_1.DataSource.P2P_TSUNAMI,
|
|
169
204
|
models_1.DataSource.WOLFX_JMA_EEW, models_1.DataSource.WOLFX_JMA_EQ, models_1.DataSource.FAN_STUDIO_JMA
|
|
170
|
-
]
|
|
171
|
-
const
|
|
205
|
+
];
|
|
206
|
+
const chinaSources = [
|
|
172
207
|
models_1.DataSource.FAN_STUDIO_CEA, models_1.DataSource.FAN_STUDIO_CENC, models_1.DataSource.FAN_STUDIO_WEATHER,
|
|
173
208
|
models_1.DataSource.FAN_STUDIO_TSUNAMI, models_1.DataSource.WOLFX_CENC_EEW, models_1.DataSource.WOLFX_CENC_EQ
|
|
174
|
-
]
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const isGlobalSource = [
|
|
179
|
-
models_1.DataSource.FAN_STUDIO_USGS, models_1.DataSource.GLOBAL_QUAKE
|
|
180
|
-
].includes(source);
|
|
181
|
-
if (isJapanSource && !regions.japan)
|
|
209
|
+
];
|
|
210
|
+
const taiwanSources = [models_1.DataSource.FAN_STUDIO_CWA, models_1.DataSource.WOLFX_CWA_EEW];
|
|
211
|
+
const globalSources = [models_1.DataSource.FAN_STUDIO_USGS, models_1.DataSource.GLOBAL_QUAKE];
|
|
212
|
+
if (japanSources.includes(src) && !regions.japan)
|
|
182
213
|
return false;
|
|
183
|
-
if (
|
|
214
|
+
if (chinaSources.includes(src) && !regions.china)
|
|
184
215
|
return false;
|
|
185
|
-
if (
|
|
216
|
+
if (taiwanSources.includes(src) && !regions.taiwan)
|
|
186
217
|
return false;
|
|
187
|
-
if (
|
|
218
|
+
if (globalSources.includes(src) && !regions.global)
|
|
188
219
|
return false;
|
|
189
220
|
return true;
|
|
190
221
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const event = handler.parseMessage(data);
|
|
209
|
-
this.handleEvent(event);
|
|
210
|
-
});
|
|
222
|
+
// ---- 状态查询(供 commands 使用)------------------------------------
|
|
223
|
+
getStatus() {
|
|
224
|
+
const result = {};
|
|
225
|
+
for (const [name, entry] of Object.entries(this.connections)) {
|
|
226
|
+
result[name] = {
|
|
227
|
+
connected: entry.ws?.readyState === ws_1.WebSocket.OPEN,
|
|
228
|
+
retryCount: entry.retryCount,
|
|
229
|
+
url: entry.url
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
// Wolfx HTTP poller 状态
|
|
233
|
+
result['wolfx_http_poller'] = {
|
|
234
|
+
connected: this.wolfxHttpTimer !== null,
|
|
235
|
+
retryCount: 0,
|
|
236
|
+
url: 'https://api.wolfx.jp/{cenc,jma}_eqlist.json'
|
|
237
|
+
};
|
|
238
|
+
return result;
|
|
211
239
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
this.
|
|
215
|
-
const event = this.handlers.globalQuake.parseMessage(data);
|
|
216
|
-
this.handleEvent(event);
|
|
217
|
-
});
|
|
240
|
+
/** 给 commands 用:拿到 wolfxHandler 最新的 eqlist 缓存 */
|
|
241
|
+
getEqListCache() {
|
|
242
|
+
return this.wolfxHandler.getEqListCache();
|
|
218
243
|
}
|
|
219
244
|
}
|
|
220
245
|
exports.DisasterWarningService = DisasterWarningService;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-disaster-warning",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Koishi 灾害预警插件,支持多数据源(地震、海啸、气象预警)",
|
|
5
5
|
"contributors": [
|
|
6
6
|
"lumia.wang <fenglian19980510@gmail.com>"
|
|
@@ -34,14 +34,12 @@
|
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"koishi": "^4.18.0"
|
|
36
36
|
},
|
|
37
|
-
"dependencies": {
|
|
38
|
-
"ws": "^8.18.0"
|
|
39
|
-
},
|
|
40
37
|
"devDependencies": {
|
|
41
38
|
"@types/node": "^20.0.0",
|
|
42
39
|
"@types/ws": "^8.5.10",
|
|
43
40
|
"koishi": "^4.18.0",
|
|
44
|
-
"typescript": "^5.0.0"
|
|
41
|
+
"typescript": "^5.0.0",
|
|
42
|
+
"ws": "^8.18.0"
|
|
45
43
|
},
|
|
46
44
|
"koishi": {
|
|
47
45
|
"description": {
|