@zhin.js/plugin-earthquake 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +115 -2
  2. package/commands//345/234/260/351/234/207/index.js +25 -0
  3. package/commands//345/234/260/351/234/207/manage-permission.js +20 -0
  4. package/commands//345/234/260/351/234/207//350/256/242/351/230/205/index.js +51 -0
  5. package/commands//345/234/260/351/234/207//350/256/242/351/230/205/347/212/266/346/200/201/index.js +41 -0
  6. package/commands//345/234/260/351/234/207//351/200/200/350/256/242/index.js +19 -0
  7. package/lib/config.d.ts +58 -0
  8. package/lib/config.d.ts.map +1 -0
  9. package/lib/config.js +138 -0
  10. package/lib/config.js.map +1 -0
  11. package/lib/conversation.d.ts +14 -0
  12. package/lib/conversation.d.ts.map +1 -0
  13. package/lib/conversation.js +54 -0
  14. package/lib/conversation.js.map +1 -0
  15. package/lib/dedupe.d.ts +60 -0
  16. package/lib/dedupe.d.ts.map +1 -0
  17. package/lib/dedupe.js +122 -0
  18. package/lib/dedupe.js.map +1 -0
  19. package/lib/filters.d.ts +26 -0
  20. package/lib/filters.d.ts.map +1 -0
  21. package/lib/filters.js +47 -0
  22. package/lib/filters.js.map +1 -0
  23. package/lib/format.d.ts +14 -0
  24. package/lib/format.d.ts.map +1 -0
  25. package/lib/format.js +66 -0
  26. package/lib/format.js.map +1 -0
  27. package/lib/http.d.ts +71 -0
  28. package/lib/http.d.ts.map +1 -0
  29. package/lib/http.js +138 -0
  30. package/lib/http.js.map +1 -0
  31. package/lib/index.d.ts +22 -0
  32. package/lib/index.d.ts.map +1 -0
  33. package/lib/index.js +14 -0
  34. package/lib/index.js.map +1 -0
  35. package/lib/query-text.d.ts +11 -0
  36. package/lib/query-text.d.ts.map +1 -0
  37. package/lib/query-text.js +37 -0
  38. package/lib/query-text.js.map +1 -0
  39. package/lib/rate-limit.d.ts +17 -0
  40. package/lib/rate-limit.d.ts.map +1 -0
  41. package/lib/rate-limit.js +41 -0
  42. package/lib/rate-limit.js.map +1 -0
  43. package/lib/runtime.d.ts +4 -0
  44. package/lib/runtime.d.ts.map +1 -0
  45. package/lib/runtime.js +4 -0
  46. package/lib/runtime.js.map +1 -0
  47. package/lib/service.d.ts +77 -0
  48. package/lib/service.d.ts.map +1 -0
  49. package/lib/service.js +229 -0
  50. package/lib/service.js.map +1 -0
  51. package/lib/sources/cenc.d.ts +37 -0
  52. package/lib/sources/cenc.d.ts.map +1 -0
  53. package/lib/sources/cenc.js +110 -0
  54. package/lib/sources/cenc.js.map +1 -0
  55. package/lib/sources/feed-source.d.ts +51 -0
  56. package/lib/sources/feed-source.d.ts.map +1 -0
  57. package/lib/sources/feed-source.js +115 -0
  58. package/lib/sources/feed-source.js.map +1 -0
  59. package/lib/sources/parse-helpers.d.ts +10 -0
  60. package/lib/sources/parse-helpers.d.ts.map +1 -0
  61. package/lib/sources/parse-helpers.js +36 -0
  62. package/lib/sources/parse-helpers.js.map +1 -0
  63. package/lib/sources/usgs.d.ts +14 -0
  64. package/lib/sources/usgs.d.ts.map +1 -0
  65. package/lib/sources/usgs.js +81 -0
  66. package/lib/sources/usgs.js.map +1 -0
  67. package/lib/subscribe-args.d.ts +9 -0
  68. package/lib/subscribe-args.d.ts.map +1 -0
  69. package/lib/subscribe-args.js +40 -0
  70. package/lib/subscribe-args.js.map +1 -0
  71. package/lib/subscriptions.d.ts +36 -0
  72. package/lib/subscriptions.d.ts.map +1 -0
  73. package/lib/subscriptions.js +122 -0
  74. package/lib/subscriptions.js.map +1 -0
  75. package/lib/types.d.ts +36 -0
  76. package/lib/types.d.ts.map +1 -0
  77. package/lib/types.js +2 -0
  78. package/lib/types.js.map +1 -0
  79. package/package.json +105 -3
  80. package/plugin.js +71 -0
  81. package/schema.json +153 -0
  82. package/skills/earthquake/SKILL.md +37 -0
  83. package/skills/earthquake/tools/recent_earthquakes/index.js +19 -0
@@ -0,0 +1,37 @@
1
+ /** Shared query rendering for the `地震` command and the Agent tool. */
2
+ import { formatEventList, formatSourceWarnings } from './format.js';
3
+ import { EarthquakeUnavailableError } from './service.js';
4
+ export const DEFAULT_QUERY_HOURS = 24;
5
+ export const MAX_QUERY_HOURS = 168;
6
+ export async function renderRecentEarthquakes(service, options) {
7
+ const config = service.config;
8
+ const limit = Math.min(20, Math.max(1, Math.round(options.limit ?? config.queryLimit)));
9
+ const minMagnitude = options.minMagnitude ?? config.queryMinMagnitude;
10
+ const hours = Math.min(MAX_QUERY_HOURS, Math.max(1, Math.round(options.hours ?? DEFAULT_QUERY_HOURS)));
11
+ const region = options.region?.trim();
12
+ const regions = region ? [region] : [];
13
+ try {
14
+ const result = await service.queryRecent({ limit, minMagnitude, regions, hours });
15
+ const scope = region ? `「${region}」` : '';
16
+ const header = `最近 ${hours} 小时${scope} M${minMagnitude.toFixed(1)} 以上地震`;
17
+ const warning = formatSourceWarnings(result.outcomes);
18
+ const staleNote = result.stale ? '⚠️ 数据源暂不可用,以下为上次成功获取的数据' : undefined;
19
+ if (result.events.length === 0) {
20
+ return [`${header}:暂无记录`, staleNote, warning].filter(Boolean).join('\n');
21
+ }
22
+ return formatEventList({
23
+ title: `${header}(共 ${result.total} 条):`,
24
+ events: result.events,
25
+ timezone: config.timezone,
26
+ hiddenCount: result.total - result.events.length,
27
+ footer: [staleNote, warning],
28
+ });
29
+ }
30
+ catch (error) {
31
+ if (error instanceof EarthquakeUnavailableError) {
32
+ return `暂时无法获取地震数据,请稍后再试。\n${error.message}`;
33
+ }
34
+ throw error;
35
+ }
36
+ }
37
+ //# sourceMappingURL=query-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-text.js","sourceRoot":"","sources":["../src/query-text.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEpE,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAS1D,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AACtC,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC;AAEnC,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,OAA0B,EAAE,OAAyB;IACjG,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,IAAI,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACxF,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,MAAM,CAAC,iBAAiB,CAAC;IACtE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,IAAI,mBAAmB,CAAC,CAAC,CAAC,CAAC;IACvG,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC;IACtC,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAClF,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1C,MAAM,MAAM,GAAG,MAAM,KAAK,MAAM,KAAK,KAAK,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;QACzE,MAAM,OAAO,GAAG,oBAAoB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACtD,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,SAAS,CAAC;QACvE,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,OAAO,CAAC,GAAG,MAAM,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,eAAe,CAAC;YACrB,KAAK,EAAE,GAAG,MAAM,MAAM,MAAM,CAAC,KAAK,MAAM;YACxC,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,WAAW,EAAE,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM;YAChD,MAAM,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC;SAC7B,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,0BAA0B,EAAE,CAAC;YAChD,OAAO,sBAAsB,KAAK,CAAC,OAAO,EAAE,CAAC;QAC/C,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Sliding-window limiter for outbound pushes, keyed by conversation.
3
+ * State lives in memory only: a restart starts every window fresh.
4
+ */
5
+ export declare class SlidingWindowRateLimiter {
6
+ #private;
7
+ constructor(options: {
8
+ readonly limit: number;
9
+ readonly windowMs: number;
10
+ readonly now?: () => number;
11
+ });
12
+ /** Consumes one slot when available. */
13
+ tryAcquire(key: string): boolean;
14
+ /** Slots left in the current window without consuming one. */
15
+ remaining(key: string): number;
16
+ }
17
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,wBAAwB;;gBAMvB,OAAO,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;KAAE;IAiBvG,wCAAwC;IACxC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAShC,8DAA8D;IAC9D,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;CAG/B"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Sliding-window limiter for outbound pushes, keyed by conversation.
3
+ * State lives in memory only: a restart starts every window fresh.
4
+ */
5
+ export class SlidingWindowRateLimiter {
6
+ #limit;
7
+ #windowMs;
8
+ #now;
9
+ #history = new Map();
10
+ constructor(options) {
11
+ this.#limit = options.limit;
12
+ this.#windowMs = options.windowMs;
13
+ this.#now = options.now ?? Date.now;
14
+ }
15
+ #recent(key, now) {
16
+ const since = now - this.#windowMs;
17
+ const kept = (this.#history.get(key) ?? []).filter((timestamp) => timestamp > since);
18
+ if (kept.length === 0) {
19
+ this.#history.delete(key);
20
+ }
21
+ else {
22
+ this.#history.set(key, kept);
23
+ }
24
+ return kept;
25
+ }
26
+ /** Consumes one slot when available. */
27
+ tryAcquire(key) {
28
+ const now = this.#now();
29
+ const recent = this.#recent(key, now);
30
+ if (recent.length >= this.#limit)
31
+ return false;
32
+ recent.push(now);
33
+ this.#history.set(key, recent);
34
+ return true;
35
+ }
36
+ /** Slots left in the current window without consuming one. */
37
+ remaining(key) {
38
+ return Math.max(0, this.#limit - this.#recent(key, this.#now()).length);
39
+ }
40
+ }
41
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,OAAO,wBAAwB;IAC1B,MAAM,CAAS;IACf,SAAS,CAAS;IAClB,IAAI,CAAe;IACnB,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAC;IAEhD,YAAY,OAA2F;QACrG,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAC5B,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC;QAClC,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACtC,CAAC;IAED,OAAO,CAAC,GAAW,EAAE,GAAW;QAC9B,MAAM,KAAK,GAAG,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;QACnC,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC;QACrF,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5B,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC/B,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,wCAAwC;IACxC,UAAU,CAAC,GAAW;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACtC,IAAI,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAC/C,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,8DAA8D;IAC9D,SAAS,CAAC,GAAW;QACnB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;IAC1E,CAAC;CACF"}
@@ -0,0 +1,4 @@
1
+ import type { EarthquakeService } from './service.js';
2
+ /** Owner-scoped earthquake service shared by commands, the poller and the Agent tool. */
3
+ export declare const earthquakeServiceToken: import("zhin.js").Token<EarthquakeService>;
4
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,yFAAyF;AACzF,eAAO,MAAM,sBAAsB,4CAGlC,CAAC"}
package/lib/runtime.js ADDED
@@ -0,0 +1,4 @@
1
+ import { createToken } from 'zhin.js';
2
+ /** Owner-scoped earthquake service shared by commands, the poller and the Agent tool. */
3
+ export const earthquakeServiceToken = createToken('zhin.earthquake.service', 'Owner-scoped earthquake query and subscription service');
4
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAGtC,yFAAyF;AACzF,MAAM,CAAC,MAAM,sBAAsB,GAAG,WAAW,CAC/C,yBAAyB,EACzB,wDAAwD,CACzD,CAAC"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Earthquake service: fetches every enabled source, merges reports, answers
3
+ * queries and drives the subscription poller.
4
+ *
5
+ * Poller guarantees:
6
+ * - `poll()` never throws; every failure is logged and reported in the result.
7
+ * - Overlapping ticks are skipped (single flight), so a slow source cannot pile up work.
8
+ * - The first successful fetch of each source is a baseline: its current events
9
+ * are remembered without pushing, so startup or source recovery never floods groups.
10
+ * - Events older than `maxEventAgeMinutes` are never pushed.
11
+ * - Each physical event is pushed at most once (it is marked seen before sending;
12
+ * a failed send is logged and not retried, to avoid duplicate deliveries).
13
+ * - Pushes are rate-limited per conversation; skipped events are summarised as a
14
+ * count in that conversation's next allowed push.
15
+ */
16
+ import type { ResolvedEarthquakeConfig } from './config.js';
17
+ import { type EventFilter } from './filters.js';
18
+ import type { HttpDependencies } from './http.js';
19
+ import { FeedSource, type FeedSourceHealth } from './sources/feed-source.js';
20
+ import { type EarthquakeSubscription, type SubscriptionStore } from './subscriptions.js';
21
+ import type { EarthquakeEvent, SourceOutcome } from './types.js';
22
+ export type PushSender = (subscription: EarthquakeSubscription, content: string) => Promise<void>;
23
+ export interface ServiceLogger {
24
+ warn(message: string): void;
25
+ debug?(message: string): void;
26
+ }
27
+ export interface EarthquakeServiceOptions {
28
+ readonly config: ResolvedEarthquakeConfig;
29
+ readonly store: SubscriptionStore;
30
+ readonly push?: PushSender;
31
+ readonly deps?: HttpDependencies;
32
+ readonly logger?: ServiceLogger;
33
+ /** Replaces the default CENC/USGS sources (tests, custom mirrors). */
34
+ readonly sources?: readonly FeedSource[];
35
+ }
36
+ export interface QueryOptions extends EventFilter {
37
+ readonly limit: number;
38
+ readonly hours: number;
39
+ }
40
+ export interface QueryResult {
41
+ readonly events: readonly EarthquakeEvent[];
42
+ /** Matching events before the limit was applied. */
43
+ readonly total: number;
44
+ readonly outcomes: readonly SourceOutcome[];
45
+ /** True when every source failed and cached data from an earlier round was used. */
46
+ readonly stale: boolean;
47
+ }
48
+ export type PollStatus = 'ok' | 'skipped' | 'failed';
49
+ export interface PollReport {
50
+ readonly status: PollStatus;
51
+ readonly outcomes: readonly SourceOutcome[];
52
+ readonly newEvents: number;
53
+ readonly pushed: number;
54
+ readonly rateLimited: number;
55
+ readonly failedPushes: number;
56
+ readonly error?: string;
57
+ }
58
+ export declare class EarthquakeUnavailableError extends Error {
59
+ readonly outcomes: readonly SourceOutcome[];
60
+ constructor(outcomes: readonly SourceOutcome[]);
61
+ }
62
+ export declare function createDefaultSources(config: ResolvedEarthquakeConfig, deps?: HttpDependencies): FeedSource[];
63
+ export declare class EarthquakeService {
64
+ #private;
65
+ readonly config: ResolvedEarthquakeConfig;
66
+ readonly store: SubscriptionStore;
67
+ constructor(options: EarthquakeServiceOptions);
68
+ get canPush(): boolean;
69
+ health(): readonly FeedSourceHealth[];
70
+ /** Stops future polls; an in-flight poll finishes its current step. */
71
+ stop(): void;
72
+ /** Recent events matching the filter, newest first. Falls back to cached data when every source fails. */
73
+ queryRecent(options: QueryOptions): Promise<QueryResult>;
74
+ /** One poller tick. Never throws. */
75
+ poll(): Promise<PollReport>;
76
+ }
77
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
package/lib/service.js ADDED
@@ -0,0 +1,229 @@
1
+ import { mergeReports, SeenEventLog } from './dedupe.js';
2
+ import { matchesFilter } from './filters.js';
3
+ import { formatEventList, formatSourceWarnings } from './format.js';
4
+ import { SlidingWindowRateLimiter } from './rate-limit.js';
5
+ import { parseCencFeed } from './sources/cenc.js';
6
+ import { FeedSource } from './sources/feed-source.js';
7
+ import { parseUsgsFeed, usgsFeedUrl } from './sources/usgs.js';
8
+ import { subscriptionKey } from './subscriptions.js';
9
+ /** Reports dated further in the future than this are treated as corrupt. */
10
+ const FUTURE_TOLERANCE_MS = 10 * 60_000;
11
+ export class EarthquakeUnavailableError extends Error {
12
+ outcomes;
13
+ constructor(outcomes) {
14
+ super(formatSourceWarnings(outcomes) ?? '所有地震数据源暂时不可用');
15
+ this.name = 'EarthquakeUnavailableError';
16
+ this.outcomes = outcomes;
17
+ }
18
+ }
19
+ const SOURCE_LABELS = {
20
+ cenc: 'CENC 中国地震台网',
21
+ usgs: 'USGS',
22
+ };
23
+ export function createDefaultSources(config, deps) {
24
+ const policy = {
25
+ timeoutMs: config.requestTimeoutMs,
26
+ maxRetries: config.maxRetries,
27
+ baseDelayMs: config.retryBaseDelayMs,
28
+ maxDelayMs: config.retryMaxDelayMs,
29
+ };
30
+ return config.sources.map((id) => {
31
+ if (id === 'cenc') {
32
+ return new FeedSource({ id, label: SOURCE_LABELS.cenc, urls: config.cencUrls, parse: parseCencFeed, policy, deps });
33
+ }
34
+ return new FeedSource({ id, label: SOURCE_LABELS.usgs, urls: [usgsFeedUrl(config.usgsFeed)], parse: parseUsgsFeed, policy, deps });
35
+ });
36
+ }
37
+ function emptyReport(status, outcomes, error) {
38
+ return { status, outcomes, newEvents: 0, pushed: 0, rateLimited: 0, failedPushes: 0, error };
39
+ }
40
+ function errorMessage(error) {
41
+ return error instanceof Error ? error.message : String(error);
42
+ }
43
+ export class EarthquakeService {
44
+ config;
45
+ store;
46
+ #sources;
47
+ #push;
48
+ #logger;
49
+ #now;
50
+ #seen = new SeenEventLog();
51
+ #baselinedSources = new Set();
52
+ #limiter;
53
+ #rateLimitedCounts = new Map();
54
+ #polling = false;
55
+ #stopped = false;
56
+ constructor(options) {
57
+ this.config = options.config;
58
+ this.store = options.store;
59
+ this.#push = options.push;
60
+ this.#logger = options.logger;
61
+ this.#now = options.deps?.now ?? Date.now;
62
+ this.#sources = options.sources ?? createDefaultSources(options.config, options.deps);
63
+ this.#limiter = new SlidingWindowRateLimiter({
64
+ limit: options.config.pushLimit,
65
+ windowMs: options.config.pushWindowMinutes * 60_000,
66
+ now: this.#now,
67
+ });
68
+ }
69
+ get canPush() {
70
+ return this.#push !== undefined;
71
+ }
72
+ health() {
73
+ return this.#sources.map((source) => source.health());
74
+ }
75
+ /** Stops future polls; an in-flight poll finishes its current step. */
76
+ stop() {
77
+ this.#stopped = true;
78
+ }
79
+ async #fetchAll() {
80
+ const reports = new Map();
81
+ const settled = await Promise.all(this.#sources.map(async (source) => {
82
+ if (source.isCoolingDown()) {
83
+ return { source: source.id, ok: false, skipped: true, eventCount: 0 };
84
+ }
85
+ try {
86
+ const events = await source.fetchEvents();
87
+ reports.set(source.id, events);
88
+ return { source: source.id, ok: true, eventCount: events.length };
89
+ }
90
+ catch (error) {
91
+ return { source: source.id, ok: false, error: errorMessage(error), eventCount: 0 };
92
+ }
93
+ }));
94
+ return { reports, outcomes: settled };
95
+ }
96
+ #withinWindow(reports, windowMs) {
97
+ const now = this.#now();
98
+ const oldest = now - windowMs;
99
+ const newest = now + FUTURE_TOLERANCE_MS;
100
+ return reports.filter((report) => report.time >= oldest && report.time <= newest);
101
+ }
102
+ /** Recent events matching the filter, newest first. Falls back to cached data when every source fails. */
103
+ async queryRecent(options) {
104
+ const { reports, outcomes } = await this.#fetchAll();
105
+ let stale = false;
106
+ let collected = [...reports.values()].flat();
107
+ if (reports.size === 0) {
108
+ const cached = this.#sources.flatMap((source) => source.cachedEvents() ?? []);
109
+ if (cached.length === 0) {
110
+ throw new EarthquakeUnavailableError(outcomes);
111
+ }
112
+ stale = true;
113
+ collected = cached;
114
+ }
115
+ const windowed = this.#withinWindow(collected, options.hours * 60 * 60_000);
116
+ const merged = mergeReports(windowed, this.config.sources);
117
+ const matching = merged.filter((event) => matchesFilter(event, options));
118
+ return {
119
+ events: matching.slice(0, options.limit),
120
+ total: matching.length,
121
+ outcomes,
122
+ stale,
123
+ };
124
+ }
125
+ /** One poller tick. Never throws. */
126
+ async poll() {
127
+ if (this.#stopped)
128
+ return emptyReport('skipped', []);
129
+ if (this.#polling)
130
+ return emptyReport('skipped', []);
131
+ this.#polling = true;
132
+ try {
133
+ return await this.#pollOnce();
134
+ }
135
+ catch (error) {
136
+ const message = errorMessage(error);
137
+ this.#logger?.warn(`[earthquake] poll failed: ${message}`);
138
+ return emptyReport('failed', [], message);
139
+ }
140
+ finally {
141
+ this.#polling = false;
142
+ }
143
+ }
144
+ async #pollOnce() {
145
+ const { reports, outcomes } = await this.#fetchAll();
146
+ for (const outcome of outcomes) {
147
+ if (!outcome.ok && !outcome.skipped) {
148
+ this.#logger?.warn(`[earthquake] source failed: ${outcome.error ?? outcome.source}`);
149
+ }
150
+ }
151
+ if (reports.size === 0) {
152
+ return emptyReport('failed', outcomes, '所有数据源本轮均不可用');
153
+ }
154
+ const windowMs = this.config.maxEventAgeMinutes * 60_000;
155
+ const windowed = new Map();
156
+ for (const [source, sourceReports] of reports) {
157
+ windowed.set(source, this.#withinWindow(sourceReports, windowMs));
158
+ }
159
+ // Baseline: remember what a source shows on its first good round, push nothing from it.
160
+ for (const [source, sourceReports] of windowed) {
161
+ if (this.#baselinedSources.has(source))
162
+ continue;
163
+ for (const report of sourceReports) {
164
+ this.#seen.add({ primary: report, reports: [report] });
165
+ }
166
+ this.#baselinedSources.add(source);
167
+ }
168
+ const merged = mergeReports([...windowed.values()].flat(), this.config.sources);
169
+ const fresh = merged.filter((event) => !this.#seen.has(event));
170
+ // Merged events may carry reports that were never seen; record them all.
171
+ for (const event of merged) {
172
+ this.#seen.add(event);
173
+ }
174
+ this.#seen.prune(this.#now());
175
+ if (fresh.length === 0 || !this.#push || this.#stopped) {
176
+ return { ...emptyReport('ok', outcomes), newEvents: fresh.length };
177
+ }
178
+ let subscriptions;
179
+ try {
180
+ subscriptions = await this.store.list();
181
+ }
182
+ catch (error) {
183
+ const message = errorMessage(error);
184
+ this.#logger?.warn(`[earthquake] cannot load subscriptions: ${message}`);
185
+ return { ...emptyReport('failed', outcomes, message), newEvents: fresh.length };
186
+ }
187
+ let pushed = 0;
188
+ let rateLimited = 0;
189
+ let failedPushes = 0;
190
+ for (const subscription of subscriptions) {
191
+ const matching = fresh.filter((event) => matchesFilter(event, subscription));
192
+ if (matching.length === 0)
193
+ continue;
194
+ const key = subscriptionKey(subscription);
195
+ if (!this.#limiter.tryAcquire(key)) {
196
+ rateLimited += matching.length;
197
+ this.#rateLimitedCounts.set(key, (this.#rateLimitedCounts.get(key) ?? 0) + matching.length);
198
+ continue;
199
+ }
200
+ const content = this.#formatPush(matching, this.#rateLimitedCounts.get(key) ?? 0);
201
+ this.#rateLimitedCounts.delete(key);
202
+ try {
203
+ await this.#push(subscription, content);
204
+ pushed += 1;
205
+ }
206
+ catch (error) {
207
+ failedPushes += 1;
208
+ this.#logger?.warn(`[earthquake] push to ${subscription.adapter}/${subscription.conversationKind}/${subscription.conversationId} failed: ${errorMessage(error)}`);
209
+ }
210
+ }
211
+ return { status: 'ok', outcomes, newEvents: fresh.length, pushed, rateLimited, failedPushes };
212
+ }
213
+ #formatPush(events, previouslyRateLimited) {
214
+ const limit = this.config.maxEventsPerMessage;
215
+ const shown = events.slice(0, limit);
216
+ const title = events.length === 1 ? '🌏 地震速报' : `🌏 地震速报(${events.length} 条)`;
217
+ const footer = previouslyRateLimited > 0
218
+ ? `(推送限流期间另有 ${previouslyRateLimited} 条地震未推送,可发送「地震」查看)`
219
+ : undefined;
220
+ return formatEventList({
221
+ title,
222
+ events: shown,
223
+ timezone: this.config.timezone,
224
+ hiddenCount: events.length - shown.length,
225
+ footer: [footer],
226
+ });
227
+ }
228
+ }
229
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * China Earthquake Networks Center (中国地震台网中心, CENC) public catalogue feed.
3
+ *
4
+ * The public site https://www.cenc.ac.cn / https://ceic.ac.cn renders its
5
+ * "最新地震" list from a static JSON array at `https://ceic.ac.cn/data/data.json`:
6
+ *
7
+ * [{ "id": "CC.20261020145206.3", "time": "2026-10-20 14:41:20",
8
+ * "latitude": 37.8, "longitude": 95.65, "depth": 10,
9
+ * "magnitude": 4.6, "location": "青海海西州直辖区" }, ...]
10
+ *
11
+ * The default primary is the keyless public mirror `https://api.wolfx.jp/cenc_eqlist.json`
12
+ * (much smaller, CDN-cached), which republishes the same catalogue as an object:
13
+ *
14
+ * { "No1": { "EventID": "AU.20261020144123.7", "time": "2026-10-20 14:41:20",
15
+ * "location": "青海海西州直辖区", "magnitude": "4.6", "depth": "10",
16
+ * "latitude": "37.80", "longitude": "95.65", ... }, "No2": ... }
17
+ *
18
+ * Both shapes are accepted. Mirrors may assign different ids to one event, so
19
+ * cross-round de-duplication never relies on CENC ids alone (see dedupe.ts).
20
+ *
21
+ * Times are Beijing time (UTC+8) without an offset. The feeds are undocumented, so
22
+ * parsing is deliberately tolerant: numeric strings are accepted, a wrapped
23
+ * `{ data: [...] }` envelope is unwrapped, and invalid rows are skipped one by
24
+ * one instead of failing the whole batch.
25
+ */
26
+ import type { SourceEarthquake } from '../types.js';
27
+ /** Keyless public mirror (default primary). */
28
+ export declare const CENC_MIRROR_URL = "https://api.wolfx.jp/cenc_eqlist.json";
29
+ /** Static JSON behind the official CENC "最新地震" page (default fallback). */
30
+ export declare const CENC_OFFICIAL_URL = "https://ceic.ac.cn/data/data.json";
31
+ export declare const DEFAULT_CENC_URLS: readonly string[];
32
+ export declare const CENC_HOME_URL = "https://www.cenc.ac.cn/";
33
+ /** Parses `YYYY-MM-DD HH:mm:ss` in Beijing time; also accepts ISO strings with an explicit offset. */
34
+ export declare function parseBeijingTime(value: string): number | undefined;
35
+ /** Parses the CENC feed body. Throws only when the body is not JSON or has no list at all. */
36
+ export declare function parseCencFeed(body: string): SourceEarthquake[];
37
+ //# sourceMappingURL=cenc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cenc.d.ts","sourceRoot":"","sources":["../../src/sources/cenc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAUpD,+CAA+C;AAC/C,eAAO,MAAM,eAAe,0CAA0C,CAAC;AACvE,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,sCAAsC,CAAC;AACrE,eAAO,MAAM,iBAAiB,mBAAsD,CAAC;AACrF,eAAO,MAAM,aAAa,4BAA4B,CAAC;AAKvD,sGAAsG;AACtG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAqBlE;AAgDD,8FAA8F;AAC9F,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAqB9D"}
@@ -0,0 +1,110 @@
1
+ import { isPlausibleMagnitude, isRecord, isValidLatitude, isValidLongitude, toFiniteNumber, toTrimmedString, } from './parse-helpers.js';
2
+ /** Keyless public mirror (default primary). */
3
+ export const CENC_MIRROR_URL = 'https://api.wolfx.jp/cenc_eqlist.json';
4
+ /** Static JSON behind the official CENC "最新地震" page (default fallback). */
5
+ export const CENC_OFFICIAL_URL = 'https://ceic.ac.cn/data/data.json';
6
+ export const DEFAULT_CENC_URLS = Object.freeze([CENC_MIRROR_URL, CENC_OFFICIAL_URL]);
7
+ export const CENC_HOME_URL = 'https://www.cenc.ac.cn/';
8
+ const BEIJING_OFFSET_MS = 8 * 60 * 60 * 1_000;
9
+ const BEIJING_TIME_PATTERN = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?$/u;
10
+ /** Parses `YYYY-MM-DD HH:mm:ss` in Beijing time; also accepts ISO strings with an explicit offset. */
11
+ export function parseBeijingTime(value) {
12
+ const match = BEIJING_TIME_PATTERN.exec(value.trim());
13
+ if (!match) {
14
+ const hasExplicitZone = /(?:Z|[+-]\d{2}:?\d{2})$/u.test(value.trim());
15
+ if (!hasExplicitZone)
16
+ return undefined;
17
+ const parsed = Date.parse(value);
18
+ return Number.isNaN(parsed) ? undefined : parsed;
19
+ }
20
+ const year = Number(match[1]);
21
+ const month = Number(match[2]);
22
+ const day = Number(match[3]);
23
+ const hour = Number(match[4]);
24
+ const minute = Number(match[5]);
25
+ const second = Number(match[6]);
26
+ const utc = Date.UTC(year, month - 1, day, hour, minute, second);
27
+ const roundTrip = new Date(utc);
28
+ // Reject impossible dates such as 2026-02-31 instead of silently rolling over.
29
+ if (roundTrip.getUTCDate() !== day || roundTrip.getUTCMonth() !== month - 1) {
30
+ return undefined;
31
+ }
32
+ return utc - BEIJING_OFFSET_MS;
33
+ }
34
+ const MIRROR_ROW_KEY = /^No\d+$/u;
35
+ function isMirrorObject(payload) {
36
+ return Object.keys(payload).some((key) => MIRROR_ROW_KEY.test(key));
37
+ }
38
+ /** Returns the row list, or undefined when the payload has no recognisable list. */
39
+ function extractRows(payload) {
40
+ if (Array.isArray(payload))
41
+ return payload;
42
+ if (!isRecord(payload))
43
+ return undefined;
44
+ if (Array.isArray(payload.data))
45
+ return payload.data;
46
+ if (isMirrorObject(payload)) {
47
+ return Object.entries(payload)
48
+ .filter(([key]) => MIRROR_ROW_KEY.test(key))
49
+ .map(([, row]) => row);
50
+ }
51
+ return undefined;
52
+ }
53
+ function parseCencRow(row) {
54
+ if (!isRecord(row))
55
+ return undefined;
56
+ const id = toTrimmedString(row.id) ?? toTrimmedString(row.EventID);
57
+ const timeText = toTrimmedString(row.time);
58
+ const latitude = toFiniteNumber(row.latitude);
59
+ const longitude = toFiniteNumber(row.longitude);
60
+ const magnitude = toFiniteNumber(row.magnitude);
61
+ const location = toTrimmedString(row.location) ?? toTrimmedString(row.placeName);
62
+ if (!id || !timeText || !location)
63
+ return undefined;
64
+ if (latitude === undefined || !isValidLatitude(latitude))
65
+ return undefined;
66
+ if (longitude === undefined || !isValidLongitude(longitude))
67
+ return undefined;
68
+ if (magnitude === undefined || !isPlausibleMagnitude(magnitude))
69
+ return undefined;
70
+ const time = parseBeijingTime(timeText);
71
+ if (time === undefined)
72
+ return undefined;
73
+ const depth = toFiniteNumber(row.depth);
74
+ return {
75
+ source: 'cenc',
76
+ id,
77
+ time,
78
+ latitude,
79
+ longitude,
80
+ depthKm: depth !== undefined && depth >= 0 ? depth : undefined,
81
+ magnitude,
82
+ location,
83
+ };
84
+ }
85
+ /** Parses the CENC feed body. Throws only when the body is not JSON or has no list at all. */
86
+ export function parseCencFeed(body) {
87
+ let payload;
88
+ try {
89
+ payload = JSON.parse(body);
90
+ }
91
+ catch (error) {
92
+ throw new Error('CENC 返回的不是有效 JSON', { cause: error });
93
+ }
94
+ const rows = extractRows(payload);
95
+ if (!rows) {
96
+ throw new Error('CENC 返回的数据格式无法识别');
97
+ }
98
+ const events = [];
99
+ for (const row of rows) {
100
+ const event = parseCencRow(row);
101
+ if (event)
102
+ events.push(event);
103
+ }
104
+ if (rows.length > 0 && events.length === 0) {
105
+ // Every row is unreadable: the schema probably changed. Fail so callers fall back.
106
+ throw new Error('CENC 返回的记录全部无法解析');
107
+ }
108
+ return events;
109
+ }
110
+ //# sourceMappingURL=cenc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cenc.js","sourceRoot":"","sources":["../../src/sources/cenc.ts"],"names":[],"mappings":"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"}