@sayue_ltr/fleq 1.50.1 → 2.0.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/CHANGELOG.md +174 -0
- package/README.md +42 -6
- package/dist/config.js +37 -4
- package/dist/dmdata/rest-client.js +58 -3
- package/dist/dmdata/telegram-parser.js +115 -64
- package/dist/dmdata/ws-client.js +49 -18
- package/dist/engine/cli/cli-run.js +88 -3
- package/dist/engine/cli/cli.js +12 -0
- package/dist/engine/eew/eew-tracker.js +41 -15
- package/dist/engine/filter/compile-filter.js +21 -0
- package/dist/engine/filter/compiler.js +188 -0
- package/dist/engine/filter/errors.js +41 -0
- package/dist/engine/filter/field-registry.js +78 -0
- package/dist/engine/filter/index.js +15 -0
- package/dist/engine/filter/parser.js +137 -0
- package/dist/engine/filter/rank-maps.js +34 -0
- package/dist/engine/filter/tokenizer.js +121 -0
- package/dist/engine/filter/type-checker.js +104 -0
- package/dist/engine/filter/types.js +2 -0
- package/dist/engine/filter-template/pipeline-controller.js +73 -0
- package/dist/engine/filter-template/pipeline.js +16 -0
- package/dist/engine/messages/display-callbacks.js +7 -0
- package/dist/engine/messages/message-router.js +114 -182
- package/dist/engine/messages/summary-tracker.js +106 -0
- package/dist/engine/messages/telegram-stats.js +103 -0
- package/dist/engine/messages/volcano-route-handler.js +122 -0
- package/dist/engine/monitor/monitor.js +52 -4
- package/dist/engine/monitor/shutdown.js +1 -0
- package/dist/engine/notification/notifier.js +21 -4
- package/dist/engine/notification/sound-player.js +398 -36
- package/dist/engine/presentation/diff-store.js +158 -0
- package/dist/engine/presentation/diff-types.js +2 -0
- package/dist/engine/presentation/events/from-earthquake.js +53 -0
- package/dist/engine/presentation/events/from-eew.js +72 -0
- package/dist/engine/presentation/events/from-lg-observation.js +58 -0
- package/dist/engine/presentation/events/from-nankai-trough.js +39 -0
- package/dist/engine/presentation/events/from-raw.js +35 -0
- package/dist/engine/presentation/events/from-seismic-text.js +37 -0
- package/dist/engine/presentation/events/from-tsunami.js +51 -0
- package/dist/engine/presentation/events/from-volcano.js +88 -0
- package/dist/engine/presentation/events/to-presentation-event.js +32 -0
- package/dist/engine/presentation/level-helpers.js +118 -0
- package/dist/engine/presentation/processors/process-earthquake.js +36 -0
- package/dist/engine/presentation/processors/process-eew.js +105 -0
- package/dist/engine/presentation/processors/process-lg-observation.js +30 -0
- package/dist/engine/presentation/processors/process-message.js +53 -0
- package/dist/engine/presentation/processors/process-nankai-trough.js +30 -0
- package/dist/engine/presentation/processors/process-raw.js +22 -0
- package/dist/engine/presentation/processors/process-seismic-text.js +30 -0
- package/dist/engine/presentation/processors/process-tsunami.js +42 -0
- package/dist/engine/presentation/processors/process-volcano.js +41 -0
- package/dist/engine/presentation/types.js +2 -0
- package/dist/engine/startup/config-resolver.js +2 -0
- package/dist/engine/template/compile-template.js +18 -0
- package/dist/engine/template/compiler.js +105 -0
- package/dist/engine/template/field-accessor.js +31 -0
- package/dist/engine/template/filters.js +100 -0
- package/dist/engine/template/index.js +5 -0
- package/dist/engine/template/parser.js +185 -0
- package/dist/engine/template/tokenizer.js +96 -0
- package/dist/engine/template/types.js +2 -0
- package/dist/types.js +3 -2
- package/dist/ui/display-adapter.js +60 -0
- package/dist/ui/earthquake-formatter.js +22 -5
- package/dist/ui/eew-formatter.js +25 -10
- package/dist/ui/formatter.js +116 -32
- package/dist/ui/minimap/grid-layout.js +91 -0
- package/dist/ui/minimap/index.js +16 -0
- package/dist/ui/minimap/minimap-renderer.js +277 -0
- package/dist/ui/minimap/pref-mapping.js +82 -0
- package/dist/ui/minimap/types.js +2 -0
- package/dist/ui/night-overlay.js +56 -0
- package/dist/ui/repl-handlers/command-definitions.js +327 -0
- package/dist/ui/repl-handlers/index.js +11 -0
- package/dist/ui/repl-handlers/info-handlers.js +633 -0
- package/dist/ui/repl-handlers/operation-handlers.js +233 -0
- package/dist/ui/repl-handlers/settings-handlers.js +927 -0
- package/dist/ui/repl-handlers/types.js +10 -0
- package/dist/ui/repl.js +81 -1752
- package/dist/ui/statistics-formatter.js +258 -0
- package/dist/ui/status-line.js +80 -0
- package/dist/ui/summary/index.js +5 -0
- package/dist/ui/summary/summary-line.js +18 -0
- package/dist/ui/summary/summary-model.js +31 -0
- package/dist/ui/summary/token-builders.js +317 -0
- package/dist/ui/summary/types.js +2 -0
- package/dist/ui/summary/width-fit.js +41 -0
- package/dist/ui/summary-interval-formatter.js +72 -0
- package/dist/ui/test-samples.js +6 -0
- package/dist/ui/theme.js +43 -5
- package/dist/ui/tip-shuffler.js +81 -0
- package/dist/ui/volcano-formatter.js +15 -13
- package/dist/ui/waiting-tips-eew.js +63 -0
- package/dist/ui/waiting-tips-info-systems.js +81 -0
- package/dist/ui/waiting-tips-seismology.js +97 -0
- package/dist/ui/waiting-tips-tsunami.js +72 -0
- package/dist/ui/waiting-tips-weather.js +189 -0
- package/dist/ui/waiting-tips.js +420 -249
- package/package.json +1 -1
|
@@ -41,6 +41,7 @@ exports.parseXml = parseXml;
|
|
|
41
41
|
exports.dig = dig;
|
|
42
42
|
exports.str = str;
|
|
43
43
|
exports.first = first;
|
|
44
|
+
exports.extractBaseReport = extractBaseReport;
|
|
44
45
|
exports.parseEarthquakeTelegram = parseEarthquakeTelegram;
|
|
45
46
|
exports.parseEewTelegram = parseEewTelegram;
|
|
46
47
|
exports.parseTsunamiTelegram = parseTsunamiTelegram;
|
|
@@ -151,10 +152,15 @@ function extractEarthquake(earthquake) {
|
|
|
151
152
|
const area = first(dig(hypo, "Area"));
|
|
152
153
|
const name = str(dig(area, "Name"));
|
|
153
154
|
// 座標パース: "+35.7+139.8-10000/" 形式
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
// VXSE61 等では jmx_eb:Coordinate が複数 (十進度 + 度分) 存在し配列になる。
|
|
156
|
+
// type="震源位置(度分)" を除外して十進度を優先選択する。
|
|
157
|
+
const rawCoord = dig(area, "jmx_eb:Coordinate") || dig(area, "Coordinate");
|
|
158
|
+
const coordNode = Array.isArray(rawCoord)
|
|
159
|
+
? rawCoord.find((c) => str(dig(c, "@_type")) !== "震源位置(度分)") ?? rawCoord[0]
|
|
160
|
+
: rawCoord;
|
|
161
|
+
const coordStr = str(coordNode != null && typeof coordNode === "object"
|
|
162
|
+
? dig(coordNode, "#text")
|
|
163
|
+
: coordNode);
|
|
158
164
|
const { lat, lon, depth } = parseCoordinate(coordStr);
|
|
159
165
|
const magRaw = str(dig(earthquake, "jmx_eb:Magnitude", "#text") ||
|
|
160
166
|
dig(earthquake, "Magnitude", "#text") ||
|
|
@@ -237,6 +243,54 @@ function extractTsunami(body) {
|
|
|
237
243
|
return { text };
|
|
238
244
|
}
|
|
239
245
|
// ── EEW ヘルパー ──
|
|
246
|
+
/** Headline Information の Kind Code に警報コード (31) が含まれるか */
|
|
247
|
+
function hasWarningHeadlineCode(head) {
|
|
248
|
+
const headline = dig(head, "Headline");
|
|
249
|
+
const informations = dig(headline, "Information");
|
|
250
|
+
const infoList = Array.isArray(informations) ? informations : informations ? [informations] : [];
|
|
251
|
+
for (const info of infoList) {
|
|
252
|
+
const items = dig(info, "Item");
|
|
253
|
+
const itemList = Array.isArray(items) ? items : items ? [items] : [];
|
|
254
|
+
for (const item of itemList) {
|
|
255
|
+
const kinds = dig(item, "Kind");
|
|
256
|
+
const kindList = Array.isArray(kinds) ? kinds : kinds ? [kinds] : [];
|
|
257
|
+
for (const kind of kindList) {
|
|
258
|
+
const code = parseInt(str(dig(kind, "Code")), 10);
|
|
259
|
+
if (code === 31)
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
/** 予測地域の Category Kind Code に警報コード (10-19) が含まれるか */
|
|
267
|
+
function hasWarningAreaKind(body) {
|
|
268
|
+
const forecast = dig(body, "Intensity", "Forecast");
|
|
269
|
+
if (!forecast)
|
|
270
|
+
return false;
|
|
271
|
+
const prefs = dig(forecast, "Pref");
|
|
272
|
+
if (!Array.isArray(prefs))
|
|
273
|
+
return false;
|
|
274
|
+
for (const pref of prefs) {
|
|
275
|
+
const areas = dig(pref, "Area");
|
|
276
|
+
if (!Array.isArray(areas))
|
|
277
|
+
continue;
|
|
278
|
+
for (const area of areas) {
|
|
279
|
+
const categories = dig(area, "Category");
|
|
280
|
+
const catList = Array.isArray(categories) ? categories : categories ? [categories] : [];
|
|
281
|
+
for (const cat of catList) {
|
|
282
|
+
const kinds = dig(cat, "Kind");
|
|
283
|
+
const kindList = Array.isArray(kinds) ? kinds : kinds ? [kinds] : [];
|
|
284
|
+
for (const kind of kindList) {
|
|
285
|
+
const code = parseInt(str(dig(kind, "Code")), 10);
|
|
286
|
+
if (code >= 10 && code <= 19)
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
240
294
|
function parseMaxIntChangeReason(body) {
|
|
241
295
|
const raw = str(dig(body, "Intensity", "Forecast", "Appendix", "MaxIntChangeReason"));
|
|
242
296
|
if (!raw)
|
|
@@ -384,22 +438,32 @@ function extractLgObservationDetails(body) {
|
|
|
384
438
|
}
|
|
385
439
|
return result;
|
|
386
440
|
}
|
|
441
|
+
// ── 共通前処理 ──
|
|
442
|
+
/** decodeBody → parseXml → Report/Head/Body を抽出する共通前処理 */
|
|
443
|
+
function extractBaseReport(msg) {
|
|
444
|
+
const xmlStr = decodeBody(msg);
|
|
445
|
+
const parsed = parseXml(xmlStr);
|
|
446
|
+
const report = dig(parsed, "Report") ||
|
|
447
|
+
dig(parsed, "jmx:Report") ||
|
|
448
|
+
dig(parsed, "jmx_seis:Report");
|
|
449
|
+
if (!report) {
|
|
450
|
+
log.debug("Report ノードが見つかりません");
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
report,
|
|
455
|
+
head: dig(report, "Head"),
|
|
456
|
+
body: dig(report, "Body"),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
387
459
|
// ── 公開API ──
|
|
388
460
|
/** 地震関連電文(VXSE51/52/53等)をパース */
|
|
389
461
|
function parseEarthquakeTelegram(msg) {
|
|
390
462
|
try {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
// Report > Body を探す
|
|
394
|
-
const report = dig(parsed, "Report") ||
|
|
395
|
-
dig(parsed, "jmx:Report") ||
|
|
396
|
-
dig(parsed, "jmx_seis:Report");
|
|
397
|
-
if (!report) {
|
|
398
|
-
log.debug("Report ノードが見つかりません");
|
|
463
|
+
const base = extractBaseReport(msg);
|
|
464
|
+
if (!base)
|
|
399
465
|
return null;
|
|
400
|
-
}
|
|
401
|
-
const body = dig(report, "Body");
|
|
402
|
-
const head = dig(report, "Head");
|
|
466
|
+
const { head, body } = base;
|
|
403
467
|
const info = {
|
|
404
468
|
type: msg.head.type,
|
|
405
469
|
infoType: str(dig(head, "InfoType")),
|
|
@@ -407,6 +471,7 @@ function parseEarthquakeTelegram(msg) {
|
|
|
407
471
|
reportDateTime: str(dig(head, "ReportDateTime")),
|
|
408
472
|
headline: str(dig(head, "Headline", "Text")) || null,
|
|
409
473
|
publishingOffice: msg.xmlReport?.control?.publishingOffice || "",
|
|
474
|
+
eventId: str(dig(head, "EventID")) || null,
|
|
410
475
|
isTest: msg.head.test,
|
|
411
476
|
};
|
|
412
477
|
// 震源
|
|
@@ -432,15 +497,10 @@ function parseEarthquakeTelegram(msg) {
|
|
|
432
497
|
/** EEW電文をパース */
|
|
433
498
|
function parseEewTelegram(msg) {
|
|
434
499
|
try {
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
const report = dig(parsed, "Report") ||
|
|
438
|
-
dig(parsed, "jmx:Report") ||
|
|
439
|
-
dig(parsed, "jmx_seis:Report");
|
|
440
|
-
if (!report)
|
|
500
|
+
const base = extractBaseReport(msg);
|
|
501
|
+
if (!base)
|
|
441
502
|
return null;
|
|
442
|
-
const head =
|
|
443
|
-
const body = dig(report, "Body");
|
|
503
|
+
const { head, body } = base;
|
|
444
504
|
// 仮定震源要素の検出
|
|
445
505
|
const earthquake = dig(body, "Earthquake");
|
|
446
506
|
const earthquakeCondition = str(dig(earthquake, "Condition"));
|
|
@@ -455,7 +515,7 @@ function parseEewTelegram(msg) {
|
|
|
455
515
|
serial: str(dig(head, "Serial")) || null,
|
|
456
516
|
eventId: str(dig(head, "EventID")) || null,
|
|
457
517
|
isTest: msg.head.test,
|
|
458
|
-
isWarning:
|
|
518
|
+
isWarning: false, // 仮値 — 後で XML から判定
|
|
459
519
|
isAssumedHypocenter: false,
|
|
460
520
|
};
|
|
461
521
|
info.maxIntChangeReason = parseMaxIntChangeReason(body);
|
|
@@ -474,6 +534,25 @@ function parseEewTelegram(msg) {
|
|
|
474
534
|
(info.maxIntChangeReason === 9 || hasPlumArea);
|
|
475
535
|
info.isAssumedHypocenter =
|
|
476
536
|
assumedHypocenterByCondition || assumedHypocenterByFallback;
|
|
537
|
+
// isWarning: XML ベース主判定 + classification を安全側フォールバック
|
|
538
|
+
// xmlWarning / classWarning を先に変数化し、判定と観測ログで同じ値を共用する。
|
|
539
|
+
const xmlWarning = msg.head.type === "VXSE43" ||
|
|
540
|
+
hasWarningAreaKind(body) ||
|
|
541
|
+
hasWarningHeadlineCode(head);
|
|
542
|
+
const classWarning = msg.classification === "eew.warning";
|
|
543
|
+
info.isWarning = xmlWarning || classWarning;
|
|
544
|
+
// 観測ログ (仕様不整合の検知用):
|
|
545
|
+
// (1) classification=eew.warning だが XML ベース判定で警報条件を検出できない
|
|
546
|
+
// (2) VXSE43 電文なのに classification が eew.warning ではない(契約差分・仕様変更の兆候)
|
|
547
|
+
// 逆方向の一般形 (xmlWarning && !classWarning) は VXSE44/VXSE45 警報相当の正常パターンなのでログしない。
|
|
548
|
+
if (classWarning && !xmlWarning) {
|
|
549
|
+
log.warn(`EEW classification=eew.warning だが XML ベース判定で警報条件を検出できず: ` +
|
|
550
|
+
`type=${msg.head.type} EventID=${str(dig(head, "EventID"))}`);
|
|
551
|
+
}
|
|
552
|
+
else if (msg.head.type === "VXSE43" && !classWarning) {
|
|
553
|
+
log.warn(`EEW VXSE43 電文だが classification=${msg.classification} (eew.warning ではない): ` +
|
|
554
|
+
`EventID=${str(dig(head, "EventID"))}`);
|
|
555
|
+
}
|
|
477
556
|
// NextAdvisory (最終報)
|
|
478
557
|
const nextAdvisory = str(dig(body, "NextAdvisory"));
|
|
479
558
|
if (nextAdvisory) {
|
|
@@ -489,17 +568,10 @@ function parseEewTelegram(msg) {
|
|
|
489
568
|
/** 津波電文(VTSE41/51/52)をパース */
|
|
490
569
|
function parseTsunamiTelegram(msg) {
|
|
491
570
|
try {
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
const report = dig(parsed, "Report") ||
|
|
495
|
-
dig(parsed, "jmx:Report") ||
|
|
496
|
-
dig(parsed, "jmx_seis:Report");
|
|
497
|
-
if (!report) {
|
|
498
|
-
log.debug("Report ノードが見つかりません");
|
|
571
|
+
const base = extractBaseReport(msg);
|
|
572
|
+
if (!base)
|
|
499
573
|
return null;
|
|
500
|
-
}
|
|
501
|
-
const head = dig(report, "Head");
|
|
502
|
-
const body = dig(report, "Body");
|
|
574
|
+
const { head, body } = base;
|
|
503
575
|
const warningComment = dig(body, "Comments", "WarningComment");
|
|
504
576
|
const warningCommentText = Array.isArray(warningComment)
|
|
505
577
|
? str(dig(warningComment[0], "Text"))
|
|
@@ -566,17 +638,10 @@ function parseTsunamiTelegram(msg) {
|
|
|
566
638
|
/** 地震活動テキスト電文(VXSE56/VXSE60/VZSE40)をパース */
|
|
567
639
|
function parseSeismicTextTelegram(msg) {
|
|
568
640
|
try {
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
const report = dig(parsed, "Report") ||
|
|
572
|
-
dig(parsed, "jmx:Report") ||
|
|
573
|
-
dig(parsed, "jmx_seis:Report");
|
|
574
|
-
if (!report) {
|
|
575
|
-
log.debug("Report ノードが見つかりません");
|
|
641
|
+
const base = extractBaseReport(msg);
|
|
642
|
+
if (!base)
|
|
576
643
|
return null;
|
|
577
|
-
}
|
|
578
|
-
const head = dig(report, "Head");
|
|
579
|
-
const body = dig(report, "Body");
|
|
644
|
+
const { head, body } = base;
|
|
580
645
|
const info = {
|
|
581
646
|
type: msg.head.type,
|
|
582
647
|
infoType: str(dig(head, "InfoType")),
|
|
@@ -597,17 +662,10 @@ function parseSeismicTextTelegram(msg) {
|
|
|
597
662
|
/** 南海トラフ関連電文(VYSE50/51/52/VYSE60)をパース */
|
|
598
663
|
function parseNankaiTroughTelegram(msg) {
|
|
599
664
|
try {
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
const report = dig(parsed, "Report") ||
|
|
603
|
-
dig(parsed, "jmx:Report") ||
|
|
604
|
-
dig(parsed, "jmx_seis:Report");
|
|
605
|
-
if (!report) {
|
|
606
|
-
log.debug("Report ノードが見つかりません");
|
|
665
|
+
const base = extractBaseReport(msg);
|
|
666
|
+
if (!base)
|
|
607
667
|
return null;
|
|
608
|
-
}
|
|
609
|
-
const head = dig(report, "Head");
|
|
610
|
-
const body = dig(report, "Body");
|
|
668
|
+
const { head, body } = base;
|
|
611
669
|
const info = {
|
|
612
670
|
type: msg.head.type,
|
|
613
671
|
infoType: str(dig(head, "InfoType")),
|
|
@@ -651,17 +709,10 @@ function parseNankaiTroughTelegram(msg) {
|
|
|
651
709
|
/** 長周期地震動観測情報(VXSE62)をパース */
|
|
652
710
|
function parseLgObservationTelegram(msg) {
|
|
653
711
|
try {
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
const report = dig(parsed, "Report") ||
|
|
657
|
-
dig(parsed, "jmx:Report") ||
|
|
658
|
-
dig(parsed, "jmx_seis:Report");
|
|
659
|
-
if (!report) {
|
|
660
|
-
log.debug("Report ノードが見つかりません");
|
|
712
|
+
const base = extractBaseReport(msg);
|
|
713
|
+
if (!base)
|
|
661
714
|
return null;
|
|
662
|
-
}
|
|
663
|
-
const head = dig(report, "Head");
|
|
664
|
-
const body = dig(report, "Body");
|
|
715
|
+
const { head, body } = base;
|
|
665
716
|
const info = {
|
|
666
717
|
type: msg.head.type,
|
|
667
718
|
infoType: str(dig(head, "InfoType")),
|
package/dist/dmdata/ws-client.js
CHANGED
|
@@ -90,6 +90,8 @@ class WebSocketManager {
|
|
|
90
90
|
/** 接続を開始する */
|
|
91
91
|
async connect() {
|
|
92
92
|
this.shouldRun = true;
|
|
93
|
+
// 既存の再接続タイマーと CONNECTING 中のソケットを中止してから新規接続する
|
|
94
|
+
this.cancelInflight();
|
|
93
95
|
const seq = ++this.connectSeq;
|
|
94
96
|
await this.doConnect(seq);
|
|
95
97
|
}
|
|
@@ -123,13 +125,46 @@ class WebSocketManager {
|
|
|
123
125
|
this.heartbeatTimer = null;
|
|
124
126
|
}
|
|
125
127
|
}
|
|
128
|
+
/** 再接続タイマーと CONNECTING 中のソケットを中止する */
|
|
129
|
+
cancelInflight() {
|
|
130
|
+
if (this.reconnectTimer) {
|
|
131
|
+
clearTimeout(this.reconnectTimer);
|
|
132
|
+
this.reconnectTimer = null;
|
|
133
|
+
}
|
|
134
|
+
// CONNECTING 中のソケットがあれば閉じて孤立を防止
|
|
135
|
+
if (this.ws && this.ws.readyState !== ws_1.default.OPEN) {
|
|
136
|
+
try {
|
|
137
|
+
this.ws.close();
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// close() 自体の失敗は無視
|
|
141
|
+
}
|
|
142
|
+
this.ws = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** close/error 共通の切断後処理 */
|
|
146
|
+
onDisconnect(reason) {
|
|
147
|
+
this.clearTimers();
|
|
148
|
+
this.ws = null;
|
|
149
|
+
this.previousSocketId = this.socketId;
|
|
150
|
+
this.socketId = null;
|
|
151
|
+
this.heartbeatDeadlineAt = null;
|
|
152
|
+
this.endpointSelector.recordDisconnected();
|
|
153
|
+
this.events.onDisconnected(reason);
|
|
154
|
+
this.scheduleReconnect();
|
|
155
|
+
}
|
|
126
156
|
async doConnect(seq) {
|
|
127
157
|
try {
|
|
158
|
+
// REST API 呼び出し前に世代チェック — 古い再接続タイマーやシャットダウン後の呼び出しを弾く
|
|
159
|
+
if (!this.shouldRun || seq !== this.connectSeq) {
|
|
160
|
+
log.debug("接続中断(pre-API): shouldRun=false または世代不一致");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
128
163
|
log.info("Socket Start を実行中...");
|
|
129
164
|
const startRes = await (0, rest_client_1.prepareAndStartSocket)(this.config, this.previousSocketId ?? undefined);
|
|
130
|
-
// close()
|
|
165
|
+
// REST API 完了後に再度世代チェック — API 呼び出し中に close() や新しい connect() が来た場合を検出
|
|
131
166
|
if (!this.shouldRun || seq !== this.connectSeq) {
|
|
132
|
-
log.debug("
|
|
167
|
+
log.debug("接続中断(post-API): close() または新しい connect() が呼ばれたため新しいソケットを作成しません");
|
|
133
168
|
return;
|
|
134
169
|
}
|
|
135
170
|
if (!startRes.websocket) {
|
|
@@ -137,6 +172,11 @@ class WebSocketManager {
|
|
|
137
172
|
}
|
|
138
173
|
const wsUrl = this.endpointSelector.resolveUrl(startRes.websocket.url);
|
|
139
174
|
log.info(`WebSocket に接続中: ${wsUrl.replace(/ticket=.*/, "ticket=***")}`);
|
|
175
|
+
// WebSocket 作成前に最終チェック
|
|
176
|
+
if (!this.shouldRun || seq !== this.connectSeq) {
|
|
177
|
+
log.debug("接続中断(pre-WS): close() または新しい connect() が呼ばれたため WebSocket を作成しません");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
140
180
|
const socket = new ws_1.default(wsUrl, ["dmdata.v2"]);
|
|
141
181
|
this.ws = socket;
|
|
142
182
|
socket.on("open", () => {
|
|
@@ -161,14 +201,7 @@ class WebSocketManager {
|
|
|
161
201
|
return;
|
|
162
202
|
const reasonStr = reason.toString() || `code=${code}`;
|
|
163
203
|
log.warn(`WebSocket 切断: ${reasonStr}`);
|
|
164
|
-
this.
|
|
165
|
-
this.ws = null;
|
|
166
|
-
this.previousSocketId = this.socketId;
|
|
167
|
-
this.socketId = null;
|
|
168
|
-
this.heartbeatDeadlineAt = null;
|
|
169
|
-
this.endpointSelector.recordDisconnected();
|
|
170
|
-
this.events.onDisconnected(reasonStr);
|
|
171
|
-
this.scheduleReconnect();
|
|
204
|
+
this.onDisconnect(reasonStr);
|
|
172
205
|
});
|
|
173
206
|
socket.on("error", (err) => {
|
|
174
207
|
log.error(`WebSocket エラー: ${err.message}`);
|
|
@@ -181,14 +214,7 @@ class WebSocketManager {
|
|
|
181
214
|
catch {
|
|
182
215
|
// close() 自体の失敗は無視
|
|
183
216
|
}
|
|
184
|
-
this.
|
|
185
|
-
this.ws = null;
|
|
186
|
-
this.previousSocketId = this.socketId;
|
|
187
|
-
this.socketId = null;
|
|
188
|
-
this.heartbeatDeadlineAt = null;
|
|
189
|
-
this.endpointSelector.recordDisconnected();
|
|
190
|
-
this.events.onDisconnected(`error: ${err.message}`);
|
|
191
|
-
this.scheduleReconnect();
|
|
217
|
+
this.onDisconnect(`error: ${err.message}`);
|
|
192
218
|
});
|
|
193
219
|
}
|
|
194
220
|
catch (err) {
|
|
@@ -329,6 +355,11 @@ class WebSocketManager {
|
|
|
329
355
|
const currentSeq = this.connectSeq;
|
|
330
356
|
this.reconnectTimer = setTimeout(async () => {
|
|
331
357
|
this.reconnectTimer = null;
|
|
358
|
+
// タイマー発火時に世代が変わっていたら(手動 retry や close() があった)何もしない
|
|
359
|
+
if (!this.shouldRun || currentSeq !== this.connectSeq) {
|
|
360
|
+
log.debug("再接続タイマー発火をスキップ: shouldRun=false または世代不一致");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
332
363
|
await this.doConnect(currentSeq);
|
|
333
364
|
}, delay);
|
|
334
365
|
}
|
|
@@ -39,6 +39,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.runMonitor = runMonitor;
|
|
40
40
|
exports.resetTerminalTitle = resetTerminalTitle;
|
|
41
41
|
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
42
44
|
const rest_client_1 = require("../../dmdata/rest-client");
|
|
43
45
|
const monitor_1 = require("../monitor/monitor");
|
|
44
46
|
const formatter_1 = require("../../ui/formatter");
|
|
@@ -46,6 +48,7 @@ const theme_1 = require("../../ui/theme");
|
|
|
46
48
|
const config_resolver_1 = require("../startup/config-resolver");
|
|
47
49
|
const updateChecker = __importStar(require("../startup/update-checker"));
|
|
48
50
|
const log = __importStar(require("../../logger"));
|
|
51
|
+
const pipeline_controller_1 = require("../filter-template/pipeline-controller");
|
|
49
52
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
50
53
|
const { version: VERSION } = require("../../../package.json");
|
|
51
54
|
async function runMonitor(opts) {
|
|
@@ -84,6 +87,11 @@ async function runMonitor(opts) {
|
|
|
84
87
|
for (const w of themeWarnings) {
|
|
85
88
|
log.warn(w);
|
|
86
89
|
}
|
|
90
|
+
// ナイトモード (resolveConfig で解決済み: CLI --night > Config > デフォルト)
|
|
91
|
+
if (config.nightMode) {
|
|
92
|
+
(0, theme_1.setNightMode)(true);
|
|
93
|
+
log.info("ナイトモード: ON");
|
|
94
|
+
}
|
|
87
95
|
// formatter キャッシュ初期化
|
|
88
96
|
if (config.tableWidth != null) {
|
|
89
97
|
(0, formatter_1.setFrameWidth)(config.tableWidth);
|
|
@@ -92,9 +100,71 @@ async function runMonitor(opts) {
|
|
|
92
100
|
(0, formatter_1.setDisplayMode)(config.displayMode);
|
|
93
101
|
(0, formatter_1.setMaxObservations)(config.maxObservations);
|
|
94
102
|
(0, formatter_1.setTruncation)(config.truncation);
|
|
95
|
-
|
|
103
|
+
// Filter / Template コンパイル
|
|
104
|
+
const pipelineController = new pipeline_controller_1.PipelineController();
|
|
105
|
+
if (opts.filter && opts.filter.length > 0) {
|
|
106
|
+
try {
|
|
107
|
+
// 複数フィルタは括弧付きで AND 結合
|
|
108
|
+
const combined = opts.filter.map((e) => `(${e})`).join(" and ");
|
|
109
|
+
pipelineController.setFilter(combined);
|
|
110
|
+
log.info(`フィルタ: ${opts.filter.join(" AND ")}`);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (err instanceof Error) {
|
|
114
|
+
log.error(`フィルタのコンパイルに失敗しました:\n${err.message}`);
|
|
115
|
+
}
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (opts.template) {
|
|
120
|
+
try {
|
|
121
|
+
let tplSource = opts.template;
|
|
122
|
+
if (tplSource.startsWith("@")) {
|
|
123
|
+
const filePath = tplSource.slice(1).replace(/^~/, os.homedir());
|
|
124
|
+
const MAX_TEMPLATE_SIZE = 1024 * 1024; // 1MB
|
|
125
|
+
const stat = fs.statSync(filePath);
|
|
126
|
+
if (stat.size > MAX_TEMPLATE_SIZE) {
|
|
127
|
+
log.error(`テンプレートファイルが大きすぎます (${stat.size} bytes, 上限 ${MAX_TEMPLATE_SIZE} bytes): ${filePath}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
tplSource = fs.readFileSync(filePath, "utf-8").trim();
|
|
131
|
+
}
|
|
132
|
+
pipelineController.setTemplate(tplSource);
|
|
133
|
+
log.info("テンプレート: カスタム");
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
if (err instanceof Error) {
|
|
137
|
+
log.warn(`テンプレートのコンパイルに失敗しました:\n${err.message}`);
|
|
138
|
+
}
|
|
139
|
+
// template エラーは警告のみ — 通常表示にフォールバック
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (opts.focus) {
|
|
143
|
+
try {
|
|
144
|
+
pipelineController.setFocus(opts.focus);
|
|
145
|
+
log.info(`フォーカス: ${opts.focus}`);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
if (err instanceof Error) {
|
|
149
|
+
log.error(`フォーカスのコンパイルに失敗しました:\n${err.message}`);
|
|
150
|
+
}
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// summaryInterval (CLI > Config > デフォルト, 0 = 無効化)
|
|
155
|
+
if (opts.summaryInterval != null) {
|
|
156
|
+
if (opts.summaryInterval === 0) {
|
|
157
|
+
config.summaryInterval = null;
|
|
158
|
+
log.info("定期要約: 無効");
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
config.summaryInterval = opts.summaryInterval;
|
|
162
|
+
log.info(`定期要約: ${opts.summaryInterval}分間隔`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
await printBanner(config);
|
|
96
166
|
updateChecker.checkForUpdates("fleq", VERSION);
|
|
97
|
-
await (0, monitor_1.startMonitor)(config);
|
|
167
|
+
await (0, monitor_1.startMonitor)(config, pipelineController);
|
|
98
168
|
}
|
|
99
169
|
/** ターミナルタイトルを設定する (ANSI OSC sequence) */
|
|
100
170
|
function setTerminalTitle(title) {
|
|
@@ -110,12 +180,27 @@ function resetTerminalTitle() {
|
|
|
110
180
|
}
|
|
111
181
|
}
|
|
112
182
|
/** 起動バナー表示 */
|
|
113
|
-
function printBanner(config) {
|
|
183
|
+
async function printBanner(config) {
|
|
114
184
|
log.info(`受信区分: ${config.classifications.join(", ")}`);
|
|
115
185
|
log.info(`テストモード: ${config.testMode}`);
|
|
116
186
|
if (config.displayMode !== "normal") {
|
|
117
187
|
log.info(`表示モード: ${config.displayMode}`);
|
|
118
188
|
}
|
|
189
|
+
// 音声バックエンド状態 (起動バナー末尾、Linux は実再生プローブ)
|
|
190
|
+
try {
|
|
191
|
+
const { checkSoundBackend } = await Promise.resolve().then(() => __importStar(require("../notification/sound-player")));
|
|
192
|
+
const result = await checkSoundBackend();
|
|
193
|
+
// CUD パレット: blueGreen = RGB(0, 158, 115), vermillion = RGB(213, 94, 0)
|
|
194
|
+
const line = result.ok
|
|
195
|
+
? chalk_1.default.rgb(0, 158, 115)(`音声: ${result.label} OK`)
|
|
196
|
+
: chalk_1.default.rgb(213, 94, 0)(`音声: ${result.label} NG${result.reason ? ` (${result.reason})` : ""}`);
|
|
197
|
+
console.log(line);
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
if (err instanceof Error) {
|
|
201
|
+
log.warn(`音声バックエンド確認エラー: ${err.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
119
204
|
log.info("接続を開始します...");
|
|
120
205
|
console.log();
|
|
121
206
|
}
|
package/dist/engine/cli/cli.js
CHANGED
|
@@ -51,6 +51,18 @@ function buildProgram() {
|
|
|
51
51
|
.option("--keep-existing", "既存のWebSocket接続を維持します(互換オプション。現在はこちらがデフォルトです)")
|
|
52
52
|
.option("--close-others", "同一APIキーの既存 open socket を閉じてから接続します")
|
|
53
53
|
.option("--mode <mode>", '表示モードを指定します: "normal" | "compact"')
|
|
54
|
+
.option("--filter <expr>", "条件式で電文を絞り込みます (複数指定で AND 結合)", (val, prev) => [...prev, val], [])
|
|
55
|
+
.option("--template <template>", "電文の1行要約テンプレートを指定します (@ でファイル読込)")
|
|
56
|
+
.option("--focus <expr>", "条件に一致しない電文を dim 表示に落とします")
|
|
57
|
+
.option("--summary-interval [minutes]", "N分ごとに受信要約を表示 (デフォルト10分, 0で無効化)", (val) => {
|
|
58
|
+
if (val === undefined || val === "true")
|
|
59
|
+
return 10;
|
|
60
|
+
const n = parseInt(val, 10);
|
|
61
|
+
if (n === 0)
|
|
62
|
+
return 0;
|
|
63
|
+
return Number.isFinite(n) && n > 0 ? n : 10;
|
|
64
|
+
})
|
|
65
|
+
.option("--night", "ナイトモードを有効にします")
|
|
54
66
|
.option("--debug", "デバッグログを表示します", false)
|
|
55
67
|
.action(async (opts) => {
|
|
56
68
|
const { runMonitor } = await Promise.resolve().then(() => __importStar(require("./cli-run")));
|
|
@@ -118,6 +118,8 @@ class EewTracker {
|
|
|
118
118
|
isNew: true,
|
|
119
119
|
isDuplicate: false,
|
|
120
120
|
isCancelled: info.infoType === "取消",
|
|
121
|
+
isSuppressed: false,
|
|
122
|
+
isUpgradeToWarning: false,
|
|
121
123
|
activeCount: this.getActiveCount(),
|
|
122
124
|
colorIndex: 0,
|
|
123
125
|
};
|
|
@@ -125,56 +127,80 @@ class EewTracker {
|
|
|
125
127
|
const serialRaw = parseInt(info.serial || "", 10);
|
|
126
128
|
const serial = Number.isFinite(serialRaw) ? serialRaw : null;
|
|
127
129
|
const isCancelled = info.infoType === "取消";
|
|
130
|
+
const headType = info.type;
|
|
128
131
|
const existing = this.events.get(eventId);
|
|
129
132
|
if (existing) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
const typeState = existing.byType.get(headType);
|
|
134
|
+
// 同一 type 内の重複判定
|
|
135
|
+
if (!isCancelled && serial != null && serial > 0 && typeState && serial <= typeState.lastSerial) {
|
|
133
136
|
return {
|
|
134
137
|
isNew: false,
|
|
135
138
|
isDuplicate: true,
|
|
136
139
|
isCancelled: false,
|
|
140
|
+
isSuppressed: false,
|
|
141
|
+
isUpgradeToWarning: false,
|
|
137
142
|
activeCount: this.getActiveCount(),
|
|
138
143
|
colorIndex: existing.colorIndex,
|
|
139
144
|
};
|
|
140
145
|
}
|
|
141
|
-
//
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
existing.lastSerial
|
|
146
|
+
// 抑制判定: VXSE45 受信済みなら VXSE43/44 は抑制
|
|
147
|
+
const isSuppressed = existing.hasSeen45 && (headType === "VXSE43" || headType === "VXSE44");
|
|
148
|
+
// type 状態の更新 (抑制されても serial・lastUpdate は更新する)
|
|
149
|
+
const previousInfo = typeState?.previousInfo;
|
|
150
|
+
if (!typeState) {
|
|
151
|
+
existing.byType.set(headType, { lastSerial: serial ?? 0, previousInfo: info });
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
if (serial != null) {
|
|
155
|
+
typeState.lastSerial = Math.max(typeState.lastSerial, serial);
|
|
156
|
+
}
|
|
157
|
+
typeState.previousInfo = info;
|
|
158
|
+
}
|
|
159
|
+
// hasSeen45 更新
|
|
160
|
+
if (headType === "VXSE45") {
|
|
161
|
+
existing.hasSeen45 = true;
|
|
162
|
+
}
|
|
163
|
+
// 差分計算: 同一 type 内の連続更新でのみ (初めての type では diff なし)
|
|
164
|
+
const diff = previousInfo ? computeDiff(previousInfo, info) : undefined;
|
|
165
|
+
// 警報昇格判定 (イベント単位)
|
|
166
|
+
const isUpgradeToWarning = !isSuppressed && !existing.hasWarningIssued && info.isWarning;
|
|
167
|
+
if (!isSuppressed) {
|
|
168
|
+
existing.hasWarningIssued = existing.hasWarningIssued || info.isWarning;
|
|
147
169
|
}
|
|
148
|
-
existing.isWarning = existing.isWarning || info.isWarning;
|
|
149
170
|
existing.isCancelled = isCancelled;
|
|
150
171
|
existing.lastUpdate = new Date();
|
|
151
|
-
existing.previousInfo = info;
|
|
152
172
|
return {
|
|
153
173
|
isNew: false,
|
|
154
174
|
isDuplicate: false,
|
|
155
175
|
isCancelled,
|
|
176
|
+
isSuppressed,
|
|
177
|
+
isUpgradeToWarning,
|
|
156
178
|
activeCount: this.getActiveCount(),
|
|
157
|
-
diff,
|
|
179
|
+
diff: isSuppressed ? undefined : diff,
|
|
158
180
|
previousInfo,
|
|
159
181
|
colorIndex: existing.colorIndex,
|
|
160
182
|
};
|
|
161
183
|
}
|
|
162
184
|
// 新規イベント
|
|
163
185
|
const colorIndex = this.nextColorIndex();
|
|
186
|
+
const byType = new Map();
|
|
187
|
+
byType.set(headType, { lastSerial: serial ?? 0, previousInfo: info });
|
|
164
188
|
this.events.set(eventId, {
|
|
165
189
|
eventId,
|
|
166
|
-
|
|
167
|
-
|
|
190
|
+
byType,
|
|
191
|
+
hasSeen45: headType === "VXSE45",
|
|
192
|
+
hasWarningIssued: info.isWarning,
|
|
168
193
|
isCancelled,
|
|
169
194
|
isFinalized: false,
|
|
170
195
|
lastUpdate: new Date(),
|
|
171
|
-
previousInfo: info,
|
|
172
196
|
colorIndex,
|
|
173
197
|
});
|
|
174
198
|
return {
|
|
175
199
|
isNew: true,
|
|
176
200
|
isDuplicate: false,
|
|
177
201
|
isCancelled,
|
|
202
|
+
isSuppressed: false,
|
|
203
|
+
isUpgradeToWarning: false,
|
|
178
204
|
activeCount: this.getActiveCount(),
|
|
179
205
|
colorIndex,
|
|
180
206
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.compileFilter = compileFilter;
|
|
4
|
+
const tokenizer_1 = require("./tokenizer");
|
|
5
|
+
const parser_1 = require("./parser");
|
|
6
|
+
const type_checker_1 = require("./type-checker");
|
|
7
|
+
const compiler_1 = require("./compiler");
|
|
8
|
+
/**
|
|
9
|
+
* フィルタ式文字列を受け取り、tokenize → parse → typeCheck → compile の
|
|
10
|
+
* パイプラインを通して FilterPredicate を返す公開 API。
|
|
11
|
+
*
|
|
12
|
+
* @throws FilterSyntaxError — 構文エラー
|
|
13
|
+
* @throws FilterFieldError — 未知フィールド
|
|
14
|
+
* @throws FilterTypeError — 型不整合
|
|
15
|
+
*/
|
|
16
|
+
function compileFilter(expr) {
|
|
17
|
+
const tokens = (0, tokenizer_1.tokenize)(expr);
|
|
18
|
+
const ast = (0, parser_1.parse)(tokens, expr);
|
|
19
|
+
(0, type_checker_1.typeCheck)(ast, expr);
|
|
20
|
+
return (0, compiler_1.compile)(ast);
|
|
21
|
+
}
|