@sayue_ltr/fleq 1.51.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/README.md +1 -1
- package/dist/config.js +2 -2
- package/dist/dmdata/telegram-parser.js +78 -5
- package/dist/engine/cli/cli-run.js +19 -4
- package/dist/engine/eew/eew-tracker.js +41 -15
- package/dist/engine/monitor/monitor.js +1 -1
- package/dist/engine/notification/notifier.js +5 -3
- package/dist/engine/notification/sound-player.js +232 -35
- package/dist/engine/presentation/processors/process-eew.js +15 -0
- package/dist/engine/presentation/processors/process-message.js +2 -2
- package/dist/engine/startup/update-checker.js +23 -2
- package/dist/engine/template/compiler.js +4 -1
- package/dist/engine/template/field-accessor.js +10 -4
- package/dist/engine/template/filters.js +14 -8
- package/dist/engine/template/parser.js +10 -15
- package/dist/types.js +1 -1
- package/dist/ui/earthquake-formatter.js +5 -0
- package/dist/ui/formatter.js +49 -0
- package/dist/ui/repl-handlers/command-definitions.js +11 -4
- package/dist/ui/repl-handlers/info-handlers.js +97 -41
- package/dist/ui/repl-handlers/settings-handlers.js +19 -15
- package/dist/ui/statistics-formatter.js +65 -15
- package/dist/ui/status-line.js +11 -0
- package/dist/ui/test-samples.js +6 -0
- package/dist/ui/theme.js +9 -0
- 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 +137 -6
- package/package.json +1 -1
|
@@ -36,9 +36,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.SOUND_LEVELS = void 0;
|
|
37
37
|
exports.isSoundLevel = isSoundLevel;
|
|
38
38
|
exports.clearCustomSoundCache = clearCustomSoundCache;
|
|
39
|
+
exports._setUptimeProviderForTest = _setUptimeProviderForTest;
|
|
40
|
+
exports._nowMsForTest = _nowMsForTest;
|
|
39
41
|
exports.playSound = playSound;
|
|
40
42
|
exports.dispose = dispose;
|
|
41
43
|
exports.resetSoundPlayer = resetSoundPlayer;
|
|
44
|
+
exports.checkSoundBackend = checkSoundBackend;
|
|
42
45
|
const child_process_1 = require("child_process");
|
|
43
46
|
const fs = __importStar(require("fs"));
|
|
44
47
|
const path = __importStar(require("path"));
|
|
@@ -112,6 +115,26 @@ function clearCustomSoundCache() {
|
|
|
112
115
|
customSoundCache.clear();
|
|
113
116
|
windowsSoundCache.clear();
|
|
114
117
|
}
|
|
118
|
+
// ── 起動ウィンドウ用の単調時計 ──
|
|
119
|
+
/** 起動後リトライのウィンドウ (ms) */
|
|
120
|
+
const STARTUP_WINDOW_MS = 60_000;
|
|
121
|
+
/** 失敗時のリトライ待機 (ms) */
|
|
122
|
+
const RETRY_DELAY_MS = 20_000;
|
|
123
|
+
/** テスト用に差し替え可能な uptime プロバイダ (秒) */
|
|
124
|
+
let uptimeProvider = null;
|
|
125
|
+
/** プロセス起動からの経過ミリ秒 (Date.now の非単調性を避ける) */
|
|
126
|
+
function nowMs() {
|
|
127
|
+
const seconds = uptimeProvider != null ? uptimeProvider() : process.uptime();
|
|
128
|
+
return Math.floor(seconds * 1000);
|
|
129
|
+
}
|
|
130
|
+
/** テスト用: uptime プロバイダを差し替える (null で本物に戻す) */
|
|
131
|
+
function _setUptimeProviderForTest(fn) {
|
|
132
|
+
uptimeProvider = fn;
|
|
133
|
+
}
|
|
134
|
+
/** テスト用: 現在の nowMs 値を返す */
|
|
135
|
+
function _nowMsForTest() {
|
|
136
|
+
return nowMs();
|
|
137
|
+
}
|
|
115
138
|
// ── 有界キュー ──
|
|
116
139
|
/** 同時再生の最大数 */
|
|
117
140
|
const MAX_CONCURRENT = 1;
|
|
@@ -121,12 +144,19 @@ const MAX_QUEUE_SIZE = 3;
|
|
|
121
144
|
const PLAY_TIMEOUT_MS = 10_000;
|
|
122
145
|
/** 再生中のプロセス数 */
|
|
123
146
|
let activeCount = 0;
|
|
124
|
-
/** 再生待ちキュー */
|
|
147
|
+
/** 再生待ちキュー (level と isRetry フラグを保持) */
|
|
125
148
|
const playQueue = [];
|
|
126
149
|
/** 現在再生中のプロセス (タイムアウト kill 用) */
|
|
127
150
|
let activeProcess = null;
|
|
128
151
|
/** タイムアウトタイマー */
|
|
129
152
|
let activeTimer = null;
|
|
153
|
+
/** 起動後リトライのタイマーハンドル集合 (dispose でクリア) */
|
|
154
|
+
const retryTimers = new Set();
|
|
155
|
+
/**
|
|
156
|
+
* 再生世代カウンタ。dispose/resetSoundPlayer で増やすことで、
|
|
157
|
+
* 旧世代の子プロセスコールバックが戻ってきても completion を拒否する。
|
|
158
|
+
*/
|
|
159
|
+
let runGeneration = 0;
|
|
130
160
|
/** dispose 済みフラグ */
|
|
131
161
|
let disposed = false;
|
|
132
162
|
/**
|
|
@@ -143,48 +173,95 @@ function onPlayFinished() {
|
|
|
143
173
|
return;
|
|
144
174
|
if (playQueue.length > 0) {
|
|
145
175
|
const next = playQueue.shift();
|
|
146
|
-
runPlay(next);
|
|
176
|
+
runPlay(next.level, { isRetry: next.isRetry });
|
|
147
177
|
}
|
|
148
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* 起動ウィンドウ内で失敗した場合に、指定秒後の再試行をスケジュールする。
|
|
181
|
+
* - リトライ由来の失敗 (opts.isRetry) は再リトライしない
|
|
182
|
+
* - 起動ウィンドウ判定は runPlay 開始時に捕捉済みの値を渡すこと
|
|
183
|
+
*/
|
|
184
|
+
function scheduleRetryIfNeeded(level, opts) {
|
|
185
|
+
if (opts?.isRetry)
|
|
186
|
+
return;
|
|
187
|
+
if (disposed)
|
|
188
|
+
return;
|
|
189
|
+
log.warn(`通知音を ${RETRY_DELAY_MS / 1000} 秒後に再試行します (${level})`);
|
|
190
|
+
const timer = setTimeout(() => {
|
|
191
|
+
retryTimers.delete(timer);
|
|
192
|
+
if (!disposed)
|
|
193
|
+
playSound(level, { isRetry: true });
|
|
194
|
+
}, RETRY_DELAY_MS);
|
|
195
|
+
retryTimers.add(timer);
|
|
196
|
+
}
|
|
149
197
|
/**
|
|
150
198
|
* プロセスを起動して再生を実行する。
|
|
151
|
-
*
|
|
199
|
+
* タイムアウトと launch 経路のコールバックが競合した場合、先に claim() した
|
|
200
|
+
* 側だけが log/bell/キュー進行を実行する (二重完了ガード)。
|
|
201
|
+
* dispose/resetSoundPlayer で runGeneration が変わると旧 runPlay の完了は拒否される。
|
|
152
202
|
*/
|
|
153
|
-
function runPlay(level) {
|
|
203
|
+
function runPlay(level, opts) {
|
|
154
204
|
activeCount++;
|
|
205
|
+
let finished = false;
|
|
206
|
+
const myGeneration = runGeneration;
|
|
207
|
+
// リトライ対象判定は runPlay 開始時の uptime で確定させる。
|
|
208
|
+
// 10 秒 timeout などで失敗確定時点でウィンドウ外でも、
|
|
209
|
+
// 起動直後に始まった再生はリトライ対象とする。
|
|
210
|
+
const startedInStartupWindow = nowMs() < STARTUP_WINDOW_MS;
|
|
211
|
+
const handle = {
|
|
212
|
+
claim: () => {
|
|
213
|
+
if (finished)
|
|
214
|
+
return false;
|
|
215
|
+
// dispose/reset された旧世代の runPlay は完了処理を行わない
|
|
216
|
+
if (myGeneration !== runGeneration)
|
|
217
|
+
return false;
|
|
218
|
+
finished = true;
|
|
219
|
+
return true;
|
|
220
|
+
},
|
|
221
|
+
done: (failed) => {
|
|
222
|
+
if (failed === true && startedInStartupWindow) {
|
|
223
|
+
scheduleRetryIfNeeded(level, opts);
|
|
224
|
+
}
|
|
225
|
+
onPlayFinished();
|
|
226
|
+
},
|
|
227
|
+
};
|
|
155
228
|
try {
|
|
156
229
|
const customPath = findCustomSound(level);
|
|
157
230
|
const platform = process.platform;
|
|
158
231
|
if (customPath) {
|
|
159
|
-
activeProcess = launchCustomSound(customPath, platform,
|
|
232
|
+
activeProcess = launchCustomSound(customPath, platform, handle);
|
|
160
233
|
}
|
|
161
234
|
else if (platform === "win32") {
|
|
162
|
-
activeProcess = launchSystemSoundWindows(level,
|
|
235
|
+
activeProcess = launchSystemSoundWindows(level, handle);
|
|
163
236
|
}
|
|
164
237
|
else if (platform === "darwin") {
|
|
165
|
-
activeProcess = launchSystemSoundMacOS(level,
|
|
238
|
+
activeProcess = launchSystemSoundMacOS(level, handle);
|
|
166
239
|
}
|
|
167
240
|
else {
|
|
168
|
-
activeProcess = launchSystemSoundLinux(level,
|
|
241
|
+
activeProcess = launchSystemSoundLinux(level, handle);
|
|
169
242
|
}
|
|
170
243
|
if (activeProcess != null) {
|
|
171
244
|
activeTimer = setTimeout(() => {
|
|
172
|
-
|
|
245
|
+
if (!handle.claim())
|
|
246
|
+
return;
|
|
247
|
+
log.warn(`通知音の再生がタイムアウトしました (${PLAY_TIMEOUT_MS}ms)`);
|
|
173
248
|
try {
|
|
174
249
|
activeProcess?.kill();
|
|
175
250
|
}
|
|
176
251
|
catch {
|
|
177
252
|
// ignore
|
|
178
253
|
}
|
|
179
|
-
|
|
254
|
+
handle.done(true);
|
|
180
255
|
}, PLAY_TIMEOUT_MS);
|
|
181
256
|
}
|
|
182
257
|
}
|
|
183
258
|
catch (err) {
|
|
259
|
+
if (!handle.claim())
|
|
260
|
+
return;
|
|
184
261
|
if (err instanceof Error) {
|
|
185
|
-
log.
|
|
262
|
+
log.warn(`通知音の再生に失敗しました: ${err.message}`);
|
|
186
263
|
}
|
|
187
|
-
|
|
264
|
+
handle.done(true);
|
|
188
265
|
}
|
|
189
266
|
}
|
|
190
267
|
/**
|
|
@@ -194,18 +271,32 @@ function runPlay(level) {
|
|
|
194
271
|
* カスタム効果音ファイルがあればそちらを優先、なければ OS システムサウンドにフォールバック。
|
|
195
272
|
* 再生失敗はログに記録するのみで例外は投げない。
|
|
196
273
|
*/
|
|
197
|
-
function playSound(level) {
|
|
274
|
+
function playSound(level, opts) {
|
|
198
275
|
if (disposed)
|
|
199
276
|
return;
|
|
200
277
|
if (activeCount < MAX_CONCURRENT) {
|
|
201
|
-
runPlay(level);
|
|
278
|
+
runPlay(level, opts);
|
|
202
279
|
}
|
|
203
280
|
else {
|
|
204
281
|
if (playQueue.length >= MAX_QUEUE_SIZE) {
|
|
205
|
-
|
|
206
|
-
|
|
282
|
+
// critical は警報相当のため drop 候補から除外。それ以外の最古を落とす。
|
|
283
|
+
const dropIdx = playQueue.findIndex((e) => e.level !== "critical");
|
|
284
|
+
if (dropIdx >= 0) {
|
|
285
|
+
const [dropped] = playQueue.splice(dropIdx, 1);
|
|
286
|
+
log.debug(`通知音キューが上限 (${MAX_QUEUE_SIZE}) に達したため破棄しました: ${dropped.level}`);
|
|
287
|
+
}
|
|
288
|
+
else if (level !== "critical") {
|
|
289
|
+
// キューが critical で埋まっている → 非 critical の新規は諦める
|
|
290
|
+
log.debug(`通知音キューが critical で飽和しているため新規再生を破棄しました: ${level}`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// 新規も critical で、既存も全て critical → 最古の critical を落とす (従来動作)
|
|
295
|
+
const dropped = playQueue.shift();
|
|
296
|
+
log.debug(`通知音キューが上限 (${MAX_QUEUE_SIZE}) に達したため破棄しました: ${dropped?.level}`);
|
|
297
|
+
}
|
|
207
298
|
}
|
|
208
|
-
playQueue.push(level);
|
|
299
|
+
playQueue.push({ level, isRetry: opts?.isRetry === true });
|
|
209
300
|
}
|
|
210
301
|
}
|
|
211
302
|
/**
|
|
@@ -214,11 +305,17 @@ function playSound(level) {
|
|
|
214
305
|
*/
|
|
215
306
|
function dispose() {
|
|
216
307
|
disposed = true;
|
|
308
|
+
// 世代を進めて旧 runPlay の遅延コールバックを無効化する
|
|
309
|
+
runGeneration++;
|
|
217
310
|
playQueue.length = 0;
|
|
218
311
|
if (activeTimer != null) {
|
|
219
312
|
clearTimeout(activeTimer);
|
|
220
313
|
activeTimer = null;
|
|
221
314
|
}
|
|
315
|
+
for (const timer of retryTimers) {
|
|
316
|
+
clearTimeout(timer);
|
|
317
|
+
}
|
|
318
|
+
retryTimers.clear();
|
|
222
319
|
if (activeProcess != null) {
|
|
223
320
|
try {
|
|
224
321
|
activeProcess.kill();
|
|
@@ -235,6 +332,8 @@ function dispose() {
|
|
|
235
332
|
*/
|
|
236
333
|
function resetSoundPlayer() {
|
|
237
334
|
disposed = false;
|
|
335
|
+
// 世代を進めて旧 runPlay の遅延コールバックを無効化する
|
|
336
|
+
runGeneration++;
|
|
238
337
|
activeCount = 0;
|
|
239
338
|
playQueue.length = 0;
|
|
240
339
|
activeProcess = null;
|
|
@@ -242,6 +341,10 @@ function resetSoundPlayer() {
|
|
|
242
341
|
clearTimeout(activeTimer);
|
|
243
342
|
activeTimer = null;
|
|
244
343
|
}
|
|
344
|
+
for (const timer of retryTimers) {
|
|
345
|
+
clearTimeout(timer);
|
|
346
|
+
}
|
|
347
|
+
retryTimers.clear();
|
|
245
348
|
}
|
|
246
349
|
// ── カスタム効果音再生 ──
|
|
247
350
|
function launchCustomSound(filePath, platform, onDone) {
|
|
@@ -268,20 +371,30 @@ function launchCustomSoundWindows(filePath, onDone) {
|
|
|
268
371
|
`[Win32.Mci]::mciSendStringW('close fleqsnd',$null,0,[IntPtr]::Zero)|Out-Null`,
|
|
269
372
|
].join(" ");
|
|
270
373
|
const proc = (0, child_process_1.execFile)("powershell", ["-NoProfile", "-Command", psCommand], (err) => {
|
|
374
|
+
if (!onDone.claim())
|
|
375
|
+
return;
|
|
271
376
|
if (err) {
|
|
272
|
-
log.
|
|
377
|
+
log.warn(`Windows カスタム通知音の再生に失敗しました: ${err.message}`);
|
|
378
|
+
onDone.done(true);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
onDone.done();
|
|
273
382
|
}
|
|
274
|
-
onDone();
|
|
275
383
|
});
|
|
276
384
|
return proc;
|
|
277
385
|
}
|
|
278
386
|
/** macOS: afplay で mp3/wav を再生 */
|
|
279
387
|
function launchCustomSoundMacOS(filePath, onDone) {
|
|
280
388
|
const proc = (0, child_process_1.execFile)("afplay", [filePath], (err) => {
|
|
389
|
+
if (!onDone.claim())
|
|
390
|
+
return;
|
|
281
391
|
if (err) {
|
|
282
|
-
log.
|
|
392
|
+
log.warn(`macOS カスタム通知音の再生に失敗しました: ${err.message}`);
|
|
393
|
+
onDone.done(true);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
onDone.done();
|
|
283
397
|
}
|
|
284
|
-
onDone();
|
|
285
398
|
});
|
|
286
399
|
return proc;
|
|
287
400
|
}
|
|
@@ -291,11 +404,16 @@ function launchCustomSoundLinux(filePath, onDone) {
|
|
|
291
404
|
if (ext === ".mp3") {
|
|
292
405
|
// mp3 は ffplay で再生
|
|
293
406
|
const proc = (0, child_process_1.execFile)("ffplay", ["-nodisp", "-autoexit", "-loglevel", "quiet", filePath], (err) => {
|
|
407
|
+
if (!onDone.claim())
|
|
408
|
+
return;
|
|
294
409
|
if (err) {
|
|
295
|
-
log.
|
|
410
|
+
log.warn(`Linux ffplay での再生に失敗しました: ${err.message}`);
|
|
296
411
|
printBell();
|
|
412
|
+
onDone.done(true);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
onDone.done();
|
|
297
416
|
}
|
|
298
|
-
onDone();
|
|
299
417
|
});
|
|
300
418
|
return proc;
|
|
301
419
|
}
|
|
@@ -304,15 +422,22 @@ function launchCustomSoundLinux(filePath, onDone) {
|
|
|
304
422
|
const proc = (0, child_process_1.execFile)("paplay", [filePath], (err) => {
|
|
305
423
|
if (err) {
|
|
306
424
|
(0, child_process_1.execFile)("aplay", ["-q", filePath], (err2) => {
|
|
425
|
+
if (!onDone.claim())
|
|
426
|
+
return;
|
|
307
427
|
if (err2) {
|
|
308
|
-
log.
|
|
428
|
+
log.warn(`Linux 通知音の再生に失敗しました: ${err2.message}`);
|
|
309
429
|
printBell();
|
|
430
|
+
onDone.done(true);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
onDone.done();
|
|
310
434
|
}
|
|
311
|
-
onDone();
|
|
312
435
|
});
|
|
313
436
|
}
|
|
314
437
|
else {
|
|
315
|
-
onDone()
|
|
438
|
+
if (!onDone.claim())
|
|
439
|
+
return;
|
|
440
|
+
onDone.done();
|
|
316
441
|
}
|
|
317
442
|
});
|
|
318
443
|
return proc;
|
|
@@ -344,17 +469,24 @@ function findWindowsSystemSound(level) {
|
|
|
344
469
|
function launchSystemSoundWindows(level, onDone) {
|
|
345
470
|
const soundPath = findWindowsSystemSound(level);
|
|
346
471
|
if (soundPath == null) {
|
|
347
|
-
|
|
472
|
+
if (!onDone.claim())
|
|
473
|
+
return null;
|
|
474
|
+
log.warn(`Windows 通知音が見つかりません。bell にフォールバックします: ${WINDOWS_SOUNDS[level]}`);
|
|
348
475
|
printBell();
|
|
349
|
-
onDone();
|
|
476
|
+
onDone.done(true);
|
|
350
477
|
return null;
|
|
351
478
|
}
|
|
352
479
|
const psCommand = `(New-Object System.Media.SoundPlayer '${soundPath}').PlaySync()`;
|
|
353
480
|
const proc = (0, child_process_1.execFile)("powershell", ["-NoProfile", "-Command", psCommand], (err) => {
|
|
481
|
+
if (!onDone.claim())
|
|
482
|
+
return;
|
|
354
483
|
if (err) {
|
|
355
|
-
log.
|
|
484
|
+
log.warn(`Windows 通知音の再生に失敗しました: ${err.message}`);
|
|
485
|
+
onDone.done(true);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
onDone.done();
|
|
356
489
|
}
|
|
357
|
-
onDone();
|
|
358
490
|
});
|
|
359
491
|
return proc;
|
|
360
492
|
}
|
|
@@ -362,26 +494,38 @@ function launchSystemSoundMacOS(level, onDone) {
|
|
|
362
494
|
const soundFile = MACOS_SOUNDS[level];
|
|
363
495
|
const soundPath = `/System/Library/Sounds/${soundFile}`;
|
|
364
496
|
const proc = (0, child_process_1.execFile)("afplay", [soundPath], (err) => {
|
|
497
|
+
if (!onDone.claim())
|
|
498
|
+
return;
|
|
365
499
|
if (err) {
|
|
366
|
-
log.
|
|
500
|
+
log.warn(`macOS 通知音の再生に失敗しました: ${err.message}`);
|
|
501
|
+
onDone.done(true);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
onDone.done();
|
|
367
505
|
}
|
|
368
|
-
onDone();
|
|
369
506
|
});
|
|
370
507
|
return proc;
|
|
371
508
|
}
|
|
372
509
|
function launchSystemSoundLinux(level, onDone) {
|
|
373
510
|
const eventName = LINUX_CANBERRA_EVENTS[level];
|
|
374
511
|
if (eventName === "bell") {
|
|
512
|
+
if (!onDone.claim())
|
|
513
|
+
return null;
|
|
375
514
|
printBell();
|
|
376
|
-
onDone();
|
|
515
|
+
onDone.done();
|
|
377
516
|
return null;
|
|
378
517
|
}
|
|
379
518
|
const proc = (0, child_process_1.execFile)("canberra-gtk-play", ["-i", eventName], (err) => {
|
|
519
|
+
if (!onDone.claim())
|
|
520
|
+
return;
|
|
380
521
|
if (err) {
|
|
381
|
-
log.
|
|
522
|
+
log.warn(`canberra-gtk-play 失敗、bell にフォールバック: ${err.message}`);
|
|
382
523
|
printBell();
|
|
524
|
+
onDone.done(true);
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
onDone.done();
|
|
383
528
|
}
|
|
384
|
-
onDone();
|
|
385
529
|
});
|
|
386
530
|
return proc;
|
|
387
531
|
}
|
|
@@ -393,3 +537,56 @@ function printBell() {
|
|
|
393
537
|
// ignore
|
|
394
538
|
}
|
|
395
539
|
}
|
|
540
|
+
// ── バックエンド健康チェック ──
|
|
541
|
+
/** バックエンドプローブの timeout (ms) */
|
|
542
|
+
const BACKEND_PROBE_TIMEOUT_MS = 2_000;
|
|
543
|
+
/**
|
|
544
|
+
* 音声バックエンドが利用可能かを判定する。
|
|
545
|
+
* Linux: ffplay で 0.1 秒の無音サンプルを再生し、終了コードで判定する。
|
|
546
|
+
* PATH 上に ffplay があっても音声デバイスが使えない場合は ok=false を返す。
|
|
547
|
+
* Windows / macOS: 即座に ok=true を返す (ビルトインの再生経路が常に利用可能)。
|
|
548
|
+
*/
|
|
549
|
+
async function checkSoundBackend() {
|
|
550
|
+
const platform = process.platform;
|
|
551
|
+
if (platform === "win32")
|
|
552
|
+
return { ok: true, label: "winmm" };
|
|
553
|
+
if (platform === "darwin")
|
|
554
|
+
return { ok: true, label: "afplay" };
|
|
555
|
+
return await new Promise((resolve) => {
|
|
556
|
+
let settled = false;
|
|
557
|
+
let timer = null;
|
|
558
|
+
const proc = (0, child_process_1.execFile)("ffplay", ["-f", "lavfi", "-i", "anullsrc=d=0.1", "-nodisp", "-autoexit", "-loglevel", "quiet"], (err) => {
|
|
559
|
+
if (settled)
|
|
560
|
+
return;
|
|
561
|
+
settled = true;
|
|
562
|
+
if (timer != null)
|
|
563
|
+
clearTimeout(timer);
|
|
564
|
+
if (err) {
|
|
565
|
+
const e = err;
|
|
566
|
+
const reason = e.code === "ENOENT"
|
|
567
|
+
? "ffplay not found in PATH"
|
|
568
|
+
: `ffplay probe failed: ${e.message}`;
|
|
569
|
+
resolve({ ok: false, label: "ffplay", reason });
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
resolve({ ok: true, label: "ffplay" });
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
// モック環境などで execFile のコールバックが同期的に呼ばれ settled=true に
|
|
576
|
+
// なっている場合、ここで timer を作らないようガードする。
|
|
577
|
+
if (settled)
|
|
578
|
+
return;
|
|
579
|
+
timer = setTimeout(() => {
|
|
580
|
+
if (settled)
|
|
581
|
+
return;
|
|
582
|
+
settled = true;
|
|
583
|
+
try {
|
|
584
|
+
proc?.kill();
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
// ignore
|
|
588
|
+
}
|
|
589
|
+
resolve({ ok: false, label: "ffplay", reason: "probe timeout" });
|
|
590
|
+
}, BACKEND_PROBE_TIMEOUT_MS);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
@@ -52,6 +52,21 @@ function processEew(msg, eewTracker, eewLogger) {
|
|
|
52
52
|
log.debug(`EEW 重複報スキップ: EventID=${eewInfo.eventId} 第${eewInfo.serial}報`);
|
|
53
53
|
return { kind: "duplicate" };
|
|
54
54
|
}
|
|
55
|
+
// VXSE44 は VXSE45 と重複するため常時抑制 (VXSE44 は配信終了予定)
|
|
56
|
+
const isSuppressed = result.isSuppressed || msg.head.type === "VXSE44";
|
|
57
|
+
if (isSuppressed) {
|
|
58
|
+
log.debug(`EEW 抑制 (${msg.head.type === "VXSE44" ? "VXSE44常時抑制" : "VXSE45優先"}): type=${eewInfo.type} EventID=${eewInfo.eventId} 第${eewInfo.serial}報`);
|
|
59
|
+
eewLogger.logReport(eewInfo, result);
|
|
60
|
+
// 抑制されても終端処理は実行する
|
|
61
|
+
if (result.isCancelled && eewInfo.eventId) {
|
|
62
|
+
eewLogger.closeEvent(eewInfo.eventId, "取消");
|
|
63
|
+
}
|
|
64
|
+
if (eewInfo.nextAdvisory && eewInfo.eventId && !result.isCancelled) {
|
|
65
|
+
eewLogger.closeEvent(eewInfo.eventId, "最終報");
|
|
66
|
+
eewTracker.finalizeEvent(eewInfo.eventId);
|
|
67
|
+
}
|
|
68
|
+
return { kind: "suppressed" };
|
|
69
|
+
}
|
|
55
70
|
// ログ記録
|
|
56
71
|
eewLogger.logReport(eewInfo, result);
|
|
57
72
|
if (result.isCancelled && eewInfo.eventId) {
|
|
@@ -23,8 +23,8 @@ function processMessage(msg, route, deps) {
|
|
|
23
23
|
const eewResult = (0, process_eew_1.processEew)(msg, deps.eewTracker, deps.eewLogger);
|
|
24
24
|
if (eewResult.kind === "ok")
|
|
25
25
|
return eewResult.outcome;
|
|
26
|
-
if (eewResult.kind === "duplicate")
|
|
27
|
-
return null; //
|
|
26
|
+
if (eewResult.kind === "duplicate" || eewResult.kind === "suppressed")
|
|
27
|
+
return null; // 重複・抑制 → 表示・統計なし
|
|
28
28
|
// parse-failed → raw 表示するが統計には含めない(旧 router と同じ動作)
|
|
29
29
|
const raw = (0, process_raw_1.processRaw)(msg, category);
|
|
30
30
|
raw.stats.shouldRecord = false;
|
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.isUpdateCheckDisabled = isUpdateCheckDisabled;
|
|
40
|
+
exports.isPersonalBuild = isPersonalBuild;
|
|
40
41
|
exports.isNewerVersion = isNewerVersion;
|
|
41
42
|
exports.checkForUpdates = checkForUpdates;
|
|
42
43
|
const https = __importStar(require("https"));
|
|
@@ -61,6 +62,15 @@ function isUpdateCheckDisabled(env = process.env) {
|
|
|
61
62
|
return false;
|
|
62
63
|
return ["1", "true", "yes", "on"].includes(raw.toLowerCase());
|
|
63
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* personal overlay 版のビルドかどうかを判定する。
|
|
67
|
+
* `package.json.version` に `-personal` を含むビルドは公開 npm 版と差分があり
|
|
68
|
+
* 「最新公開版にアップデートしろ」という通知は意味を持たないため、チェック自体を skip する。
|
|
69
|
+
* 詳細: docs/specs/personal-branch-overlay-pattern.md / Knowledge/Dev の同名ノート参照。
|
|
70
|
+
*/
|
|
71
|
+
function isPersonalBuild(version) {
|
|
72
|
+
return /-personal\b/i.test(version);
|
|
73
|
+
}
|
|
64
74
|
/** キャッシュを読み込む。無効ならnullを返す */
|
|
65
75
|
function readCache() {
|
|
66
76
|
try {
|
|
@@ -134,7 +144,11 @@ function fetchJson(url, extractVersion) {
|
|
|
134
144
|
}
|
|
135
145
|
/** npm registry から最新バージョンを取得する */
|
|
136
146
|
function fetchFromNpm(packageName) {
|
|
137
|
-
|
|
147
|
+
// scoped package (`@scope/name`) は npm registry API 仕様で `/` を `%2F` に encode する必要がある。
|
|
148
|
+
// registry.npmjs.org は生 `/` も受け付けるが、厳密なミラー/プロキシでは弾かれうるため
|
|
149
|
+
// 明示 encode して移植性を確保する。`@` は encode 不要(仕様上どちらでも可)。
|
|
150
|
+
const encodedName = packageName.replace("/", "%2F");
|
|
151
|
+
return fetchJson(`https://registry.npmjs.org/${encodedName}/latest`, (data) => {
|
|
138
152
|
if (typeof data === "object" &&
|
|
139
153
|
data != null &&
|
|
140
154
|
"version" in data &&
|
|
@@ -202,6 +216,10 @@ function checkForUpdates(packageName, currentVersion) {
|
|
|
202
216
|
log.debug("update check skipped by FLEQ_NO_UPDATE_CHECK");
|
|
203
217
|
return;
|
|
204
218
|
}
|
|
219
|
+
if (isPersonalBuild(currentVersion)) {
|
|
220
|
+
log.debug(`update check skipped: personal build (${currentVersion})`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
205
223
|
// キャッシュが有効なら registry にアクセスしない
|
|
206
224
|
const cache = readCache();
|
|
207
225
|
if (cache != null && Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {
|
|
@@ -224,6 +242,9 @@ function checkForUpdates(packageName, currentVersion) {
|
|
|
224
242
|
}
|
|
225
243
|
/** 更新通知を表示する */
|
|
226
244
|
function printUpdateNotice(current, latest, packageName) {
|
|
227
|
-
|
|
245
|
+
// GitHub Releases の tag_name は "v2.0.0" 形式なので、表示前に先頭の v を strip する
|
|
246
|
+
const currentDisplay = current.replace(/^v/, "");
|
|
247
|
+
const latestDisplay = latest.replace(/^v/, "");
|
|
248
|
+
log.warn(`Update available: v${currentDisplay} → v${latestDisplay} ` +
|
|
228
249
|
chalk_1.default.gray(`npm install -g ${packageName}@latest`));
|
|
229
250
|
}
|
|
@@ -96,7 +96,10 @@ function isTruthy(val) {
|
|
|
96
96
|
function stringify(value) {
|
|
97
97
|
if (value == null)
|
|
98
98
|
return "";
|
|
99
|
+
// 表示専用ポリシー対応: 配列は改行で結合する。
|
|
100
|
+
// ", " 区切りで 1 行に並べる「機械可読 1 行出力」を作れないようにし、
|
|
101
|
+
// shell pipe 連携での再配信足場化を防ぐ。
|
|
99
102
|
if (Array.isArray(value))
|
|
100
|
-
return value.join("
|
|
103
|
+
return value.join("\n");
|
|
101
104
|
return String(value);
|
|
102
105
|
}
|
|
@@ -2,14 +2,20 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getFieldValue = getFieldValue;
|
|
4
4
|
/**
|
|
5
|
-
* PresentationEvent
|
|
5
|
+
* PresentationEvent からドットパスで値を取得する。
|
|
6
|
+
*
|
|
7
|
+
* 表示専用ポリシー (dmdata.jp 再配信ポリシー対応) のため、配列インデックス参照
|
|
8
|
+
* (`[N]`) は parser 側で禁止済み。本関数では二重防御として、念のため `raw`
|
|
9
|
+
* フィールドへのアクセスも拒否する。
|
|
6
10
|
*
|
|
7
11
|
* segments 例:
|
|
8
|
-
* - ["title"]
|
|
9
|
-
* - ["
|
|
10
|
-
* - ["areaItems", 0, "name"] → event.areaItems[0].name
|
|
12
|
+
* - ["title"] → event.title
|
|
13
|
+
* - ["earthquake", "magnitude"] → event.earthquake.magnitude
|
|
11
14
|
*/
|
|
12
15
|
function getFieldValue(event, segments) {
|
|
16
|
+
// 二重防御: parser を経由せず直接呼ばれた場合にも raw への参照を拒否
|
|
17
|
+
if (segments[0] === "raw")
|
|
18
|
+
return undefined;
|
|
13
19
|
let current = event;
|
|
14
20
|
for (const seg of segments) {
|
|
15
21
|
if (current == null)
|
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
// ── Template filter functions ──
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.applyFilter = applyFilter;
|
|
5
|
+
/**
|
|
6
|
+
* フィルタ内部で値を文字列化する共通関数。
|
|
7
|
+
*
|
|
8
|
+
* 表示専用ポリシー対応: 配列は改行区切りで文字列化する。
|
|
9
|
+
* `String([...])` が "," 連結するため、`|upper` や `|replace` といった
|
|
10
|
+
* 文字列系フィルタ経由で配列を 1 行機械可読出力に整形できてしまうのを防ぐ。
|
|
11
|
+
*/
|
|
5
12
|
function toString(value) {
|
|
6
13
|
if (value == null)
|
|
7
14
|
return "";
|
|
15
|
+
if (Array.isArray(value))
|
|
16
|
+
return value.join("\n");
|
|
8
17
|
return String(value);
|
|
9
18
|
}
|
|
10
19
|
function filterDefault(value, args) {
|
|
@@ -27,12 +36,6 @@ function filterPad(value, args) {
|
|
|
27
36
|
return str;
|
|
28
37
|
return str.padEnd(width);
|
|
29
38
|
}
|
|
30
|
-
function filterJoin(value, args) {
|
|
31
|
-
if (!Array.isArray(value))
|
|
32
|
-
return toString(value);
|
|
33
|
-
const separator = args[0] != null ? String(args[0]) : ",";
|
|
34
|
-
return value.join(separator);
|
|
35
|
-
}
|
|
36
39
|
function filterDate(value, args) {
|
|
37
40
|
const format = args[0] != null ? String(args[0]) : "HH:mm";
|
|
38
41
|
const date = value instanceof Date ? value : new Date(String(value));
|
|
@@ -58,6 +61,11 @@ function filterReplace(value, args) {
|
|
|
58
61
|
const str = toString(value);
|
|
59
62
|
const search = args[0] != null ? String(args[0]) : "";
|
|
60
63
|
const replacement = args[1] != null ? String(args[1]) : "";
|
|
64
|
+
// 表示専用ポリシー対応: search/replacement に改行文字を含めると、
|
|
65
|
+
// 配列の改行 join を 1 行化する経路(\n → "," 等の置換)を作れてしまうため禁止。
|
|
66
|
+
if (/[\r\n]/.test(search) || /[\r\n]/.test(replacement)) {
|
|
67
|
+
throw new Error("テンプレート実行エラー: replace フィルタの引数に改行文字 (\\n / \\r) を含めることはできません。表示専用制限です。");
|
|
68
|
+
}
|
|
61
69
|
return str.split(search).join(replacement);
|
|
62
70
|
}
|
|
63
71
|
function filterUpper(value) {
|
|
@@ -78,8 +86,6 @@ function applyFilter(name, value, args) {
|
|
|
78
86
|
return filterTruncate(value, args);
|
|
79
87
|
case "pad":
|
|
80
88
|
return filterPad(value, args);
|
|
81
|
-
case "join":
|
|
82
|
-
return filterJoin(value, args);
|
|
83
89
|
case "date":
|
|
84
90
|
return filterDate(value, args);
|
|
85
91
|
case "replace":
|
|
@@ -144,9 +144,12 @@ class TemplateParser {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
/**
|
|
147
|
-
*
|
|
148
|
-
* 例: "
|
|
149
|
-
*
|
|
147
|
+
* ドットパスを segments 配列に分割する。
|
|
148
|
+
* 例: "foo.bar.baz" → ["foo", "bar", "baz"]
|
|
149
|
+
*
|
|
150
|
+
* 表示専用ポリシー (dmdata.jp 再配信ポリシー対応) のため、以下を禁止する:
|
|
151
|
+
* - ブラケット記法 `[N]` (配列インデックス参照)
|
|
152
|
+
* - 先頭セグメントが `raw` のパス (生 XML データへの直接参照)
|
|
150
153
|
*/
|
|
151
154
|
function parsePathSegments(path) {
|
|
152
155
|
const segments = [];
|
|
@@ -157,18 +160,7 @@ function parsePathSegments(path) {
|
|
|
157
160
|
continue;
|
|
158
161
|
}
|
|
159
162
|
if (path[i] === "[") {
|
|
160
|
-
|
|
161
|
-
i++; // [
|
|
162
|
-
const start = i;
|
|
163
|
-
while (i < path.length && path[i] !== "]")
|
|
164
|
-
i++;
|
|
165
|
-
const inner = path.slice(start, i);
|
|
166
|
-
if (i < path.length)
|
|
167
|
-
i++; // ]
|
|
168
|
-
// 数値ならnumber、そうでなければstring
|
|
169
|
-
const num = Number(inner);
|
|
170
|
-
segments.push(Number.isNaN(num) ? inner : num);
|
|
171
|
-
continue;
|
|
163
|
+
throw new Error(`テンプレート構文エラー: 配列インデックス参照 [N] は無効です (path: "${path}")。表示専用制限により、要素を1行に並べる機械可読出力を防いでいます。`);
|
|
172
164
|
}
|
|
173
165
|
// 識別子: 次の . または [ まで
|
|
174
166
|
const start = i;
|
|
@@ -176,6 +168,9 @@ function parsePathSegments(path) {
|
|
|
176
168
|
i++;
|
|
177
169
|
segments.push(path.slice(start, i));
|
|
178
170
|
}
|
|
171
|
+
if (segments[0] === "raw") {
|
|
172
|
+
throw new Error(`テンプレート構文エラー: raw フィールド参照は無効です (path: "${path}")。表示専用制限により、生 XML データへの直接アクセスを禁止しています。`);
|
|
173
|
+
}
|
|
179
174
|
return segments;
|
|
180
175
|
}
|
|
181
176
|
/** 文字列リテラル内のエスケープシーケンスを復元する */
|