@sayue_ltr/fleq 1.50.0 → 1.51.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 +103 -0
- package/README.md +47 -5
- package/dist/config.js +35 -2
- package/dist/dmdata/rest-client.js +58 -3
- package/dist/dmdata/telegram-parser.js +37 -59
- package/dist/dmdata/ws-client.js +49 -18
- package/dist/engine/cli/cli-run.js +71 -1
- package/dist/engine/cli/cli.js +12 -0
- 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 +51 -3
- package/dist/engine/monitor/shutdown.js +1 -0
- package/dist/engine/notification/notifier.js +16 -1
- package/dist/engine/notification/sound-player.js +193 -28
- 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 +90 -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 +102 -0
- package/dist/engine/template/field-accessor.js +25 -0
- package/dist/engine/template/filters.js +94 -0
- package/dist/engine/template/index.js +5 -0
- package/dist/engine/template/parser.js +190 -0
- package/dist/engine/template/tokenizer.js +96 -0
- package/dist/engine/template/types.js +2 -0
- package/dist/types.js +2 -1
- package/dist/ui/display-adapter.js +60 -0
- package/dist/ui/earthquake-formatter.js +17 -5
- package/dist/ui/eew-formatter.js +25 -10
- package/dist/ui/formatter.js +67 -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 +320 -0
- package/dist/ui/repl-handlers/index.js +11 -0
- package/dist/ui/repl-handlers/info-handlers.js +577 -0
- package/dist/ui/repl-handlers/operation-handlers.js +233 -0
- package/dist/ui/repl-handlers/settings-handlers.js +923 -0
- package/dist/ui/repl-handlers/types.js +10 -0
- package/dist/ui/repl.js +81 -1752
- package/dist/ui/statistics-formatter.js +208 -0
- package/dist/ui/status-line.js +69 -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/theme.js +34 -5
- package/dist/ui/tip-shuffler.js +81 -0
- package/dist/ui/volcano-formatter.js +15 -13
- package/dist/ui/waiting-tips.js +289 -249
- package/package.json +1 -1
|
@@ -47,14 +47,21 @@ const formatter_1 = require("../../ui/formatter");
|
|
|
47
47
|
const repl_coordinator_1 = require("./repl-coordinator");
|
|
48
48
|
const shutdown_1 = require("./shutdown");
|
|
49
49
|
const log = __importStar(require("../../logger"));
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const summary_interval_formatter_1 = require("../../ui/summary-interval-formatter");
|
|
51
|
+
const summary_tracker_1 = require("../messages/summary-tracker");
|
|
52
|
+
async function startMonitor(config, pipelineController) {
|
|
53
|
+
// display adapter は遅延ロードで ui 依存を monitor 側に限定する
|
|
54
|
+
const { createDisplayAdapter } = await Promise.resolve().then(() => __importStar(require("../../ui/display-adapter")));
|
|
55
|
+
const display = createDisplayAdapter();
|
|
56
|
+
const pipeline = pipelineController?.getPipeline();
|
|
57
|
+
const { handler: routeMessage, eewLogger, notifier, tsunamiState, volcanoState, stats, summaryTracker, flushAndDisposeVolcanoBuffer } = (0, message_router_1.createMessageHandler)({ pipeline: pipeline ?? undefined, display });
|
|
52
58
|
// EEW ログ設定を反映
|
|
53
59
|
eewLogger.setEnabled(config.eewLog);
|
|
54
60
|
eewLogger.setFields(config.eewLogFields);
|
|
55
61
|
let disconnectedAt = null;
|
|
56
62
|
let isFirstConnection = true;
|
|
57
63
|
let replHandler = null;
|
|
64
|
+
let summaryTimerControl = null;
|
|
58
65
|
const manager = new multi_connection_manager_1.MultiConnectionManager(config, {
|
|
59
66
|
onData: (msg) => {
|
|
60
67
|
(0, repl_coordinator_1.withReplDisplay)(replHandler, () => routeMessage(msg));
|
|
@@ -88,11 +95,15 @@ async function startMonitor(config) {
|
|
|
88
95
|
getReplHandler: () => replHandler,
|
|
89
96
|
resetTerminalTitle: cli_run_1.resetTerminalTitle,
|
|
90
97
|
flushAndDisposeVolcanoBuffer,
|
|
98
|
+
stopSummaryTimer: () => summaryTimerControl?.stop(),
|
|
91
99
|
});
|
|
92
100
|
// REPL ハンドラ (遅延ロード)
|
|
93
101
|
const { ReplHandler } = await Promise.resolve().then(() => __importStar(require("../../ui/repl")));
|
|
94
|
-
replHandler = new ReplHandler(config, manager, notifier, eewLogger, shutdown, [tsunamiState, volcanoState], [tsunamiState, volcanoState]);
|
|
102
|
+
replHandler = new ReplHandler(config, manager, notifier, eewLogger, shutdown, stats, [tsunamiState, volcanoState], [tsunamiState, volcanoState], pipelineController, summaryTracker);
|
|
95
103
|
(0, shutdown_1.registerShutdownSignals)(shutdown);
|
|
104
|
+
// 定期要約タイマー
|
|
105
|
+
summaryTimerControl = createSummaryTimerControl(config, summaryTracker, () => replHandler);
|
|
106
|
+
replHandler.setSummaryTimerControl(summaryTimerControl);
|
|
96
107
|
// REPL を先に起動 (接続中もコマンド入力可能にする)
|
|
97
108
|
replHandler.start();
|
|
98
109
|
// 起動時: 最新の津波・火山警報状態を復元 (WebSocket 接続前に実行)
|
|
@@ -116,3 +127,40 @@ async function startMonitor(config) {
|
|
|
116
127
|
log.info("retry コマンドで再接続を試みることができます。");
|
|
117
128
|
}
|
|
118
129
|
}
|
|
130
|
+
/** 定期要約タイマーの制御オブジェクトを生成する。初期値が設定済みなら自動起動する。 */
|
|
131
|
+
function createSummaryTimerControl(config, tracker, getReplHandler) {
|
|
132
|
+
let timer = null;
|
|
133
|
+
function showOutput(intervalMinutes) {
|
|
134
|
+
const snapshot = tracker.getSnapshot();
|
|
135
|
+
const output = (0, summary_interval_formatter_1.formatSummaryInterval)(snapshot, intervalMinutes, true);
|
|
136
|
+
(0, repl_coordinator_1.withReplDisplay)(getReplHandler(), () => {
|
|
137
|
+
console.log(output);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const control = {
|
|
141
|
+
start(intervalMinutes) {
|
|
142
|
+
// 既存タイマーを停止してから再起動
|
|
143
|
+
control.stop();
|
|
144
|
+
const intervalMs = intervalMinutes * 60_000;
|
|
145
|
+
timer = setInterval(() => showOutput(intervalMinutes), intervalMs);
|
|
146
|
+
timer.unref();
|
|
147
|
+
},
|
|
148
|
+
stop() {
|
|
149
|
+
if (timer != null) {
|
|
150
|
+
clearInterval(timer);
|
|
151
|
+
timer = null;
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
isRunning() {
|
|
155
|
+
return timer != null;
|
|
156
|
+
},
|
|
157
|
+
showNow() {
|
|
158
|
+
showOutput(summary_tracker_1.WINDOW_MINUTES);
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
// 初期値が設定されていれば自動起動
|
|
162
|
+
if (config.summaryInterval != null) {
|
|
163
|
+
control.start(config.summaryInterval);
|
|
164
|
+
}
|
|
165
|
+
return control;
|
|
166
|
+
}
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.Notifier = exports.NOTIFY_CATEGORY_LABELS = void 0;
|
|
37
|
+
exports.clearIconPathCache = clearIconPathCache;
|
|
37
38
|
exports.resolveIconPath = resolveIconPath;
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const fs = __importStar(require("fs"));
|
|
@@ -55,12 +56,24 @@ const CATEGORY_ICON_PREFIX = {
|
|
|
55
56
|
lgObservation: "lg-observation",
|
|
56
57
|
volcano: "volcano",
|
|
57
58
|
};
|
|
59
|
+
/** resolveIconPath の結果キャッシュ。キー: "{category}:{level|''}" */
|
|
60
|
+
const iconPathCache = new Map();
|
|
61
|
+
/**
|
|
62
|
+
* resolveIconPath のキャッシュをクリアする (テスト用)。
|
|
63
|
+
*/
|
|
64
|
+
function clearIconPathCache() {
|
|
65
|
+
iconPathCache.clear();
|
|
66
|
+
}
|
|
58
67
|
/**
|
|
59
68
|
* カテゴリとレベルからアイコンパスを解決する。
|
|
60
69
|
* 3段フォールバック: {prefix}-{level}.png → {prefix}.png → default.png
|
|
61
|
-
* いずれも見つからなければ undefined
|
|
70
|
+
* いずれも見つからなければ undefined を返す。結果はキャッシュして再利用する。
|
|
62
71
|
*/
|
|
63
72
|
function resolveIconPath(category, level) {
|
|
73
|
+
const cacheKey = `${category}:${level ?? ""}`;
|
|
74
|
+
if (iconPathCache.has(cacheKey)) {
|
|
75
|
+
return iconPathCache.get(cacheKey);
|
|
76
|
+
}
|
|
64
77
|
const prefix = CATEGORY_ICON_PREFIX[category];
|
|
65
78
|
const candidates = [];
|
|
66
79
|
if (level) {
|
|
@@ -70,9 +83,11 @@ function resolveIconPath(category, level) {
|
|
|
70
83
|
candidates.push(path.join(ICONS_DIR, "default.png"));
|
|
71
84
|
for (const candidate of candidates) {
|
|
72
85
|
if (fs.existsSync(candidate)) {
|
|
86
|
+
iconPathCache.set(cacheKey, candidate);
|
|
73
87
|
return candidate;
|
|
74
88
|
}
|
|
75
89
|
}
|
|
90
|
+
iconPathCache.set(cacheKey, undefined);
|
|
76
91
|
return undefined;
|
|
77
92
|
}
|
|
78
93
|
/** 通知アプリ名 */
|
|
@@ -35,7 +35,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.SOUND_LEVELS = void 0;
|
|
37
37
|
exports.isSoundLevel = isSoundLevel;
|
|
38
|
+
exports.clearCustomSoundCache = clearCustomSoundCache;
|
|
38
39
|
exports.playSound = playSound;
|
|
40
|
+
exports.dispose = dispose;
|
|
41
|
+
exports.resetSoundPlayer = resetSoundPlayer;
|
|
39
42
|
const child_process_1 = require("child_process");
|
|
40
43
|
const fs = __importStar(require("fs"));
|
|
41
44
|
const path = __importStar(require("path"));
|
|
@@ -82,143 +85,305 @@ const LINUX_CANBERRA_EVENTS = {
|
|
|
82
85
|
info: "dialog-information",
|
|
83
86
|
cancel: "bell",
|
|
84
87
|
};
|
|
88
|
+
// ── findCustomSound キャッシュ ──
|
|
89
|
+
/** findCustomSound の結果キャッシュ (null = 存在しない, undefined = 未検索) */
|
|
90
|
+
const customSoundCache = new Map();
|
|
85
91
|
/**
|
|
86
92
|
* カスタム効果音ファイルのパスを返す。見つからなければ null。
|
|
87
|
-
* mp3 → wav
|
|
93
|
+
* mp3 → wav の順で探索する。結果はキャッシュして再利用する。
|
|
88
94
|
*/
|
|
89
95
|
function findCustomSound(level) {
|
|
96
|
+
if (customSoundCache.has(level)) {
|
|
97
|
+
return customSoundCache.get(level);
|
|
98
|
+
}
|
|
90
99
|
const baseName = CUSTOM_SOUND_FILES[level];
|
|
91
100
|
for (const ext of SUPPORTED_EXTENSIONS) {
|
|
92
101
|
const filePath = path.join(CUSTOM_SOUNDS_DIR, baseName + ext);
|
|
93
102
|
if (fs.existsSync(filePath)) {
|
|
103
|
+
customSoundCache.set(level, filePath);
|
|
94
104
|
return filePath;
|
|
95
105
|
}
|
|
96
106
|
}
|
|
107
|
+
customSoundCache.set(level, null);
|
|
97
108
|
return null;
|
|
98
109
|
}
|
|
110
|
+
/** サウンドキャッシュをクリアする (テスト用) */
|
|
111
|
+
function clearCustomSoundCache() {
|
|
112
|
+
customSoundCache.clear();
|
|
113
|
+
windowsSoundCache.clear();
|
|
114
|
+
}
|
|
115
|
+
// ── 有界キュー ──
|
|
116
|
+
/** 同時再生の最大数 */
|
|
117
|
+
const MAX_CONCURRENT = 1;
|
|
118
|
+
/** キューの最大サイズ (超えたら古いエントリを破棄) */
|
|
119
|
+
const MAX_QUEUE_SIZE = 3;
|
|
120
|
+
/** 再生プロセスのタイムアウト (ms) */
|
|
121
|
+
const PLAY_TIMEOUT_MS = 10_000;
|
|
122
|
+
/** 再生中のプロセス数 */
|
|
123
|
+
let activeCount = 0;
|
|
124
|
+
/** 再生待ちキュー */
|
|
125
|
+
const playQueue = [];
|
|
126
|
+
/** 現在再生中のプロセス (タイムアウト kill 用) */
|
|
127
|
+
let activeProcess = null;
|
|
128
|
+
/** タイムアウトタイマー */
|
|
129
|
+
let activeTimer = null;
|
|
130
|
+
/** dispose 済みフラグ */
|
|
131
|
+
let disposed = false;
|
|
99
132
|
/**
|
|
100
|
-
*
|
|
101
|
-
* カスタム効果音ファイルがあればそちらを優先、なければ OS システムサウンドにフォールバック。
|
|
102
|
-
* 再生失敗はログに記録するのみで例外は投げない。
|
|
133
|
+
* 再生完了後にキューから次のエントリを取り出して再生する。
|
|
103
134
|
*/
|
|
104
|
-
function
|
|
135
|
+
function onPlayFinished() {
|
|
136
|
+
activeCount--;
|
|
137
|
+
activeProcess = null;
|
|
138
|
+
if (activeTimer != null) {
|
|
139
|
+
clearTimeout(activeTimer);
|
|
140
|
+
activeTimer = null;
|
|
141
|
+
}
|
|
142
|
+
if (disposed)
|
|
143
|
+
return;
|
|
144
|
+
if (playQueue.length > 0) {
|
|
145
|
+
const next = playQueue.shift();
|
|
146
|
+
runPlay(next);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* プロセスを起動して再生を実行する。
|
|
151
|
+
* タイムアウトを設定し、完了時に onPlayFinished を呼ぶ。
|
|
152
|
+
*/
|
|
153
|
+
function runPlay(level) {
|
|
154
|
+
activeCount++;
|
|
105
155
|
try {
|
|
106
156
|
const customPath = findCustomSound(level);
|
|
107
157
|
const platform = process.platform;
|
|
108
158
|
if (customPath) {
|
|
109
|
-
|
|
159
|
+
activeProcess = launchCustomSound(customPath, platform, onPlayFinished);
|
|
110
160
|
}
|
|
111
161
|
else if (platform === "win32") {
|
|
112
|
-
|
|
162
|
+
activeProcess = launchSystemSoundWindows(level, onPlayFinished);
|
|
113
163
|
}
|
|
114
164
|
else if (platform === "darwin") {
|
|
115
|
-
|
|
165
|
+
activeProcess = launchSystemSoundMacOS(level, onPlayFinished);
|
|
116
166
|
}
|
|
117
167
|
else {
|
|
118
|
-
|
|
168
|
+
activeProcess = launchSystemSoundLinux(level, onPlayFinished);
|
|
169
|
+
}
|
|
170
|
+
if (activeProcess != null) {
|
|
171
|
+
activeTimer = setTimeout(() => {
|
|
172
|
+
log.debug(`通知音の再生がタイムアウトしました (${PLAY_TIMEOUT_MS}ms)`);
|
|
173
|
+
try {
|
|
174
|
+
activeProcess?.kill();
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// ignore
|
|
178
|
+
}
|
|
179
|
+
onPlayFinished();
|
|
180
|
+
}, PLAY_TIMEOUT_MS);
|
|
119
181
|
}
|
|
120
182
|
}
|
|
121
183
|
catch (err) {
|
|
122
184
|
if (err instanceof Error) {
|
|
123
185
|
log.debug(`通知音の再生に失敗しました: ${err.message}`);
|
|
124
186
|
}
|
|
187
|
+
onPlayFinished();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 通知音を再生する (fire-and-forget)。
|
|
192
|
+
* 同時再生は MAX_CONCURRENT 件に制限し、超えた場合はキューに積む。
|
|
193
|
+
* キューが MAX_QUEUE_SIZE を超えた場合は先頭 (最古) を破棄する。
|
|
194
|
+
* カスタム効果音ファイルがあればそちらを優先、なければ OS システムサウンドにフォールバック。
|
|
195
|
+
* 再生失敗はログに記録するのみで例外は投げない。
|
|
196
|
+
*/
|
|
197
|
+
function playSound(level) {
|
|
198
|
+
if (disposed)
|
|
199
|
+
return;
|
|
200
|
+
if (activeCount < MAX_CONCURRENT) {
|
|
201
|
+
runPlay(level);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
if (playQueue.length >= MAX_QUEUE_SIZE) {
|
|
205
|
+
const dropped = playQueue.shift();
|
|
206
|
+
log.debug(`通知音キューが上限 (${MAX_QUEUE_SIZE}) に達したため破棄しました: ${dropped}`);
|
|
207
|
+
}
|
|
208
|
+
playQueue.push(level);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 進行中の再生を停止し、キューをクリアする。
|
|
213
|
+
* アプリ終了時に呼ぶことでプロセスリークを防ぐ。
|
|
214
|
+
*/
|
|
215
|
+
function dispose() {
|
|
216
|
+
disposed = true;
|
|
217
|
+
playQueue.length = 0;
|
|
218
|
+
if (activeTimer != null) {
|
|
219
|
+
clearTimeout(activeTimer);
|
|
220
|
+
activeTimer = null;
|
|
221
|
+
}
|
|
222
|
+
if (activeProcess != null) {
|
|
223
|
+
try {
|
|
224
|
+
activeProcess.kill();
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// ignore
|
|
228
|
+
}
|
|
229
|
+
activeProcess = null;
|
|
230
|
+
}
|
|
231
|
+
activeCount = 0;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* dispose 後に再利用できるよう内部状態をリセットする (テスト用)。
|
|
235
|
+
*/
|
|
236
|
+
function resetSoundPlayer() {
|
|
237
|
+
disposed = false;
|
|
238
|
+
activeCount = 0;
|
|
239
|
+
playQueue.length = 0;
|
|
240
|
+
activeProcess = null;
|
|
241
|
+
if (activeTimer != null) {
|
|
242
|
+
clearTimeout(activeTimer);
|
|
243
|
+
activeTimer = null;
|
|
125
244
|
}
|
|
126
245
|
}
|
|
127
246
|
// ── カスタム効果音再生 ──
|
|
128
|
-
function
|
|
247
|
+
function launchCustomSound(filePath, platform, onDone) {
|
|
129
248
|
if (platform === "win32") {
|
|
130
|
-
|
|
249
|
+
return launchCustomSoundWindows(filePath, onDone);
|
|
131
250
|
}
|
|
132
251
|
else if (platform === "darwin") {
|
|
133
|
-
|
|
252
|
+
return launchCustomSoundMacOS(filePath, onDone);
|
|
134
253
|
}
|
|
135
254
|
else {
|
|
136
|
-
|
|
255
|
+
return launchCustomSoundLinux(filePath, onDone);
|
|
137
256
|
}
|
|
138
257
|
}
|
|
139
258
|
/** Windows: winmm.dll mciSendString で mp3/wav を同期再生 */
|
|
140
|
-
function
|
|
259
|
+
function launchCustomSoundWindows(filePath, onDone) {
|
|
141
260
|
// MediaPlayer (WPF) は非同期ロードのためタイミング問題が起きやすい。
|
|
142
261
|
// mciSendString は同期 (wait) で確実に再生できる。
|
|
262
|
+
// execFile を使い cmd.exe のダブルクォーテーション解釈を回避する。
|
|
143
263
|
const escaped = filePath.replace(/'/g, "''");
|
|
144
264
|
const psCommand = [
|
|
145
|
-
`Add-Type -Namespace Win32 -Name Mci -MemberDefinition '[DllImport("winmm.dll",CharSet=
|
|
265
|
+
`Add-Type -Namespace Win32 -Name Mci -MemberDefinition '[DllImport("winmm.dll",CharSet=CharSet.Unicode)]public static extern int mciSendStringW(string cmd,System.Text.StringBuilder ret,int retLen,System.IntPtr hwnd);';`,
|
|
146
266
|
`[Win32.Mci]::mciSendStringW('open "' + '${escaped}' + '" type mpegvideo alias fleqsnd',$null,0,[IntPtr]::Zero)|Out-Null;`,
|
|
147
267
|
`[Win32.Mci]::mciSendStringW('play fleqsnd wait',$null,0,[IntPtr]::Zero)|Out-Null;`,
|
|
148
268
|
`[Win32.Mci]::mciSendStringW('close fleqsnd',$null,0,[IntPtr]::Zero)|Out-Null`,
|
|
149
269
|
].join(" ");
|
|
150
|
-
(0, child_process_1.
|
|
270
|
+
const proc = (0, child_process_1.execFile)("powershell", ["-NoProfile", "-Command", psCommand], (err) => {
|
|
151
271
|
if (err) {
|
|
152
272
|
log.debug(`Windows カスタム通知音の再生に失敗しました: ${err.message}`);
|
|
153
273
|
}
|
|
274
|
+
onDone();
|
|
154
275
|
});
|
|
276
|
+
return proc;
|
|
155
277
|
}
|
|
156
278
|
/** macOS: afplay で mp3/wav を再生 */
|
|
157
|
-
function
|
|
158
|
-
(0, child_process_1.execFile)("afplay", [filePath], (err) => {
|
|
279
|
+
function launchCustomSoundMacOS(filePath, onDone) {
|
|
280
|
+
const proc = (0, child_process_1.execFile)("afplay", [filePath], (err) => {
|
|
159
281
|
if (err) {
|
|
160
282
|
log.debug(`macOS カスタム通知音の再生に失敗しました: ${err.message}`);
|
|
161
283
|
}
|
|
284
|
+
onDone();
|
|
162
285
|
});
|
|
286
|
+
return proc;
|
|
163
287
|
}
|
|
164
288
|
/** Linux: ffplay → paplay → aplay のフォールバック */
|
|
165
|
-
function
|
|
289
|
+
function launchCustomSoundLinux(filePath, onDone) {
|
|
166
290
|
const ext = path.extname(filePath).toLowerCase();
|
|
167
291
|
if (ext === ".mp3") {
|
|
168
292
|
// mp3 は ffplay で再生
|
|
169
|
-
(0, child_process_1.execFile)("ffplay", ["-nodisp", "-autoexit", "-loglevel", "quiet", filePath], (err) => {
|
|
293
|
+
const proc = (0, child_process_1.execFile)("ffplay", ["-nodisp", "-autoexit", "-loglevel", "quiet", filePath], (err) => {
|
|
170
294
|
if (err) {
|
|
171
295
|
log.debug(`Linux ffplay での再生に失敗しました: ${err.message}`);
|
|
172
296
|
printBell();
|
|
173
297
|
}
|
|
298
|
+
onDone();
|
|
174
299
|
});
|
|
300
|
+
return proc;
|
|
175
301
|
}
|
|
176
302
|
else {
|
|
177
303
|
// wav は paplay → aplay のフォールバック
|
|
178
|
-
(0, child_process_1.execFile)("paplay", [filePath], (err) => {
|
|
304
|
+
const proc = (0, child_process_1.execFile)("paplay", [filePath], (err) => {
|
|
179
305
|
if (err) {
|
|
180
306
|
(0, child_process_1.execFile)("aplay", ["-q", filePath], (err2) => {
|
|
181
307
|
if (err2) {
|
|
182
308
|
log.debug(`Linux 通知音の再生に失敗しました: ${err2.message}`);
|
|
183
309
|
printBell();
|
|
184
310
|
}
|
|
311
|
+
onDone();
|
|
185
312
|
});
|
|
186
313
|
}
|
|
314
|
+
else {
|
|
315
|
+
onDone();
|
|
316
|
+
}
|
|
187
317
|
});
|
|
318
|
+
return proc;
|
|
188
319
|
}
|
|
189
320
|
}
|
|
190
321
|
// ── システムサウンドフォールバック ──
|
|
191
|
-
|
|
322
|
+
/** Windows システムサウンドの存在確認キャッシュ (null = 存在しない, undefined = 未検索) */
|
|
323
|
+
const windowsSoundCache = new Map();
|
|
324
|
+
/** Windows サウンドファイルの確認済みパスを返す。存在しなければ null。 */
|
|
325
|
+
function findWindowsSystemSound(level) {
|
|
326
|
+
if (windowsSoundCache.has(level)) {
|
|
327
|
+
return windowsSoundCache.get(level);
|
|
328
|
+
}
|
|
192
329
|
const soundFile = WINDOWS_SOUNDS[level];
|
|
193
330
|
const soundPath = path.join(process.env.SYSTEMROOT || "C:\\Windows", "Media", soundFile);
|
|
331
|
+
if (fs.existsSync(soundPath)) {
|
|
332
|
+
windowsSoundCache.set(level, soundPath);
|
|
333
|
+
return soundPath;
|
|
334
|
+
}
|
|
335
|
+
// フォールバック: Windows Default.wav
|
|
336
|
+
const defaultPath = path.join(process.env.SYSTEMROOT || "C:\\Windows", "Media", "Windows Default.wav");
|
|
337
|
+
if (fs.existsSync(defaultPath)) {
|
|
338
|
+
windowsSoundCache.set(level, defaultPath);
|
|
339
|
+
return defaultPath;
|
|
340
|
+
}
|
|
341
|
+
windowsSoundCache.set(level, null);
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
function launchSystemSoundWindows(level, onDone) {
|
|
345
|
+
const soundPath = findWindowsSystemSound(level);
|
|
346
|
+
if (soundPath == null) {
|
|
347
|
+
log.debug(`Windows 通知音が見つかりません。bell にフォールバックします: ${WINDOWS_SOUNDS[level]}`);
|
|
348
|
+
printBell();
|
|
349
|
+
onDone();
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
194
352
|
const psCommand = `(New-Object System.Media.SoundPlayer '${soundPath}').PlaySync()`;
|
|
195
|
-
(0, child_process_1.
|
|
353
|
+
const proc = (0, child_process_1.execFile)("powershell", ["-NoProfile", "-Command", psCommand], (err) => {
|
|
196
354
|
if (err) {
|
|
197
355
|
log.debug(`Windows 通知音の再生に失敗しました: ${err.message}`);
|
|
198
356
|
}
|
|
357
|
+
onDone();
|
|
199
358
|
});
|
|
359
|
+
return proc;
|
|
200
360
|
}
|
|
201
|
-
function
|
|
361
|
+
function launchSystemSoundMacOS(level, onDone) {
|
|
202
362
|
const soundFile = MACOS_SOUNDS[level];
|
|
203
363
|
const soundPath = `/System/Library/Sounds/${soundFile}`;
|
|
204
|
-
(0, child_process_1.execFile)("afplay", [soundPath], (err) => {
|
|
364
|
+
const proc = (0, child_process_1.execFile)("afplay", [soundPath], (err) => {
|
|
205
365
|
if (err) {
|
|
206
366
|
log.debug(`macOS 通知音の再生に失敗しました: ${err.message}`);
|
|
207
367
|
}
|
|
368
|
+
onDone();
|
|
208
369
|
});
|
|
370
|
+
return proc;
|
|
209
371
|
}
|
|
210
|
-
function
|
|
372
|
+
function launchSystemSoundLinux(level, onDone) {
|
|
211
373
|
const eventName = LINUX_CANBERRA_EVENTS[level];
|
|
212
374
|
if (eventName === "bell") {
|
|
213
375
|
printBell();
|
|
214
|
-
|
|
376
|
+
onDone();
|
|
377
|
+
return null;
|
|
215
378
|
}
|
|
216
|
-
(0, child_process_1.execFile)("canberra-gtk-play", ["-i", eventName], (err) => {
|
|
379
|
+
const proc = (0, child_process_1.execFile)("canberra-gtk-play", ["-i", eventName], (err) => {
|
|
217
380
|
if (err) {
|
|
218
381
|
log.debug(`canberra-gtk-play 失敗、bell にフォールバック: ${err.message}`);
|
|
219
382
|
printBell();
|
|
220
383
|
}
|
|
384
|
+
onDone();
|
|
221
385
|
});
|
|
386
|
+
return proc;
|
|
222
387
|
}
|
|
223
388
|
function printBell() {
|
|
224
389
|
try {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PresentationDiffStore = void 0;
|
|
4
|
+
/** TTL のデフォルト値 (30分) */
|
|
5
|
+
const DEFAULT_TTL_MS = 30 * 60 * 1000;
|
|
6
|
+
/** プルーニング実行間隔 (apply 呼び出し回数) */
|
|
7
|
+
const PRUNE_INTERVAL = 50;
|
|
8
|
+
/**
|
|
9
|
+
* PresentationEvent の差分を検出・保持するストア。
|
|
10
|
+
*
|
|
11
|
+
* apply() は PresentationEvent を受け取り、同一 diffKey を持つ直前のイベントとの
|
|
12
|
+
* 差分を検出して diff プロパティを付与して返す。
|
|
13
|
+
*
|
|
14
|
+
* TTL ベースの自動クリーンアップにより、長時間稼働時のメモリ蓄積を防止する。
|
|
15
|
+
*/
|
|
16
|
+
class PresentationDiffStore {
|
|
17
|
+
previous = new Map();
|
|
18
|
+
ttlMs;
|
|
19
|
+
applyCount = 0;
|
|
20
|
+
constructor(ttlMs) {
|
|
21
|
+
this.ttlMs = ttlMs ?? DEFAULT_TTL_MS;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* イベントを受け取り、差分を検出する。
|
|
25
|
+
* - 初回 (同一 diffKey で初めて) → diff なし
|
|
26
|
+
* - 2回目以降 → diff あり (changed フラグ + summary + fields)
|
|
27
|
+
* - diffKey が解決できないドメイン → diff なし (対象外)
|
|
28
|
+
*/
|
|
29
|
+
apply(event) {
|
|
30
|
+
this.applyCount++;
|
|
31
|
+
if (this.applyCount % PRUNE_INTERVAL === 0) {
|
|
32
|
+
this.prune();
|
|
33
|
+
}
|
|
34
|
+
const diffKey = this.resolveDiffKey(event);
|
|
35
|
+
if (diffKey == null)
|
|
36
|
+
return event;
|
|
37
|
+
const entry = this.previous.get(diffKey);
|
|
38
|
+
const diff = entry ? this.computeDiff(entry.event, event) : undefined;
|
|
39
|
+
this.previous.set(diffKey, { event, updatedAt: Date.now() });
|
|
40
|
+
return diff ? { ...event, diff } : event;
|
|
41
|
+
}
|
|
42
|
+
/** 指定した diffKey のエントリを削除する */
|
|
43
|
+
remove(diffKey) {
|
|
44
|
+
this.previous.delete(diffKey);
|
|
45
|
+
}
|
|
46
|
+
/** テスト用: ストアをクリアする */
|
|
47
|
+
clear() {
|
|
48
|
+
this.previous.clear();
|
|
49
|
+
}
|
|
50
|
+
/** TTL を超過した古いエントリを削除する */
|
|
51
|
+
prune() {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
for (const [key, entry] of this.previous) {
|
|
54
|
+
if (now - entry.updatedAt > this.ttlMs) {
|
|
55
|
+
this.previous.delete(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ── diffKey 解決 ──
|
|
60
|
+
resolveDiffKey(event) {
|
|
61
|
+
switch (event.domain) {
|
|
62
|
+
case "eew":
|
|
63
|
+
return event.eventId ? `eew:${event.eventId}` : null;
|
|
64
|
+
case "tsunami":
|
|
65
|
+
return event.type === "VTSE41" ? "tsunami:vtse41" : null;
|
|
66
|
+
case "volcano":
|
|
67
|
+
return event.type === "VFVO50" && event.volcanoCode
|
|
68
|
+
? `volcano:${event.volcanoCode}`
|
|
69
|
+
: null;
|
|
70
|
+
default:
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// ── diff 算出 ──
|
|
75
|
+
computeDiff(prev, curr) {
|
|
76
|
+
const fields = [];
|
|
77
|
+
const summary = [];
|
|
78
|
+
switch (curr.domain) {
|
|
79
|
+
case "eew":
|
|
80
|
+
this.compareEew(prev, curr, fields, summary);
|
|
81
|
+
break;
|
|
82
|
+
case "tsunami":
|
|
83
|
+
this.compareTsunami(prev, curr, fields, summary);
|
|
84
|
+
break;
|
|
85
|
+
case "volcano":
|
|
86
|
+
this.compareVolcano(prev, curr, fields, summary);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
changed: fields.length > 0,
|
|
91
|
+
summary,
|
|
92
|
+
fields,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
compareEew(prev, curr, fields, summary) {
|
|
96
|
+
// magnitude
|
|
97
|
+
if (prev.magnitude !== curr.magnitude) {
|
|
98
|
+
fields.push({
|
|
99
|
+
key: "magnitude",
|
|
100
|
+
previous: prev.magnitude ?? null,
|
|
101
|
+
current: curr.magnitude ?? null,
|
|
102
|
+
significance: "major",
|
|
103
|
+
});
|
|
104
|
+
if (prev.magnitude != null && curr.magnitude != null) {
|
|
105
|
+
summary.push(`M${prev.magnitude}→${curr.magnitude}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// maxInt (forecastMaxInt for EEW)
|
|
109
|
+
const prevInt = prev.forecastMaxInt ?? prev.maxInt;
|
|
110
|
+
const currInt = curr.forecastMaxInt ?? curr.maxInt;
|
|
111
|
+
if (prevInt !== currInt) {
|
|
112
|
+
fields.push({
|
|
113
|
+
key: "maxInt",
|
|
114
|
+
previous: prevInt ?? null,
|
|
115
|
+
current: currInt ?? null,
|
|
116
|
+
significance: "major",
|
|
117
|
+
});
|
|
118
|
+
if (prevInt != null && currInt != null) {
|
|
119
|
+
summary.push(`${prevInt}→${currInt}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// hypocenterName
|
|
123
|
+
if (prev.hypocenterName !== curr.hypocenterName) {
|
|
124
|
+
fields.push({
|
|
125
|
+
key: "hypocenterName",
|
|
126
|
+
previous: prev.hypocenterName ?? null,
|
|
127
|
+
current: curr.hypocenterName ?? null,
|
|
128
|
+
significance: "minor",
|
|
129
|
+
});
|
|
130
|
+
summary.push("震源変更");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
compareTsunami(prev, curr, fields, summary) {
|
|
134
|
+
if (prev.areaCount !== curr.areaCount) {
|
|
135
|
+
fields.push({
|
|
136
|
+
key: "areaCount",
|
|
137
|
+
previous: prev.areaCount,
|
|
138
|
+
current: curr.areaCount,
|
|
139
|
+
significance: "major",
|
|
140
|
+
});
|
|
141
|
+
summary.push(`${prev.areaCount}区域→${curr.areaCount}区域`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
compareVolcano(prev, curr, fields, summary) {
|
|
145
|
+
if (prev.alertLevel !== curr.alertLevel) {
|
|
146
|
+
fields.push({
|
|
147
|
+
key: "alertLevel",
|
|
148
|
+
previous: prev.alertLevel ?? null,
|
|
149
|
+
current: curr.alertLevel ?? null,
|
|
150
|
+
significance: "major",
|
|
151
|
+
});
|
|
152
|
+
if (prev.alertLevel != null && curr.alertLevel != null) {
|
|
153
|
+
summary.push(`Lv${prev.alertLevel}→${curr.alertLevel}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
exports.PresentationDiffStore = PresentationDiffStore;
|