@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.
- package/README.md +115 -2
- package/commands//345/234/260/351/234/207/index.js +25 -0
- package/commands//345/234/260/351/234/207/manage-permission.js +20 -0
- package/commands//345/234/260/351/234/207//350/256/242/351/230/205/index.js +51 -0
- package/commands//345/234/260/351/234/207//350/256/242/351/230/205/347/212/266/346/200/201/index.js +41 -0
- package/commands//345/234/260/351/234/207//351/200/200/350/256/242/index.js +19 -0
- package/lib/config.d.ts +58 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +138 -0
- package/lib/config.js.map +1 -0
- package/lib/conversation.d.ts +14 -0
- package/lib/conversation.d.ts.map +1 -0
- package/lib/conversation.js +54 -0
- package/lib/conversation.js.map +1 -0
- package/lib/dedupe.d.ts +60 -0
- package/lib/dedupe.d.ts.map +1 -0
- package/lib/dedupe.js +122 -0
- package/lib/dedupe.js.map +1 -0
- package/lib/filters.d.ts +26 -0
- package/lib/filters.d.ts.map +1 -0
- package/lib/filters.js +47 -0
- package/lib/filters.js.map +1 -0
- package/lib/format.d.ts +14 -0
- package/lib/format.d.ts.map +1 -0
- package/lib/format.js +66 -0
- package/lib/format.js.map +1 -0
- package/lib/http.d.ts +71 -0
- package/lib/http.d.ts.map +1 -0
- package/lib/http.js +138 -0
- package/lib/http.js.map +1 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +14 -0
- package/lib/index.js.map +1 -0
- package/lib/query-text.d.ts +11 -0
- package/lib/query-text.d.ts.map +1 -0
- package/lib/query-text.js +37 -0
- package/lib/query-text.js.map +1 -0
- package/lib/rate-limit.d.ts +17 -0
- package/lib/rate-limit.d.ts.map +1 -0
- package/lib/rate-limit.js +41 -0
- package/lib/rate-limit.js.map +1 -0
- package/lib/runtime.d.ts +4 -0
- package/lib/runtime.d.ts.map +1 -0
- package/lib/runtime.js +4 -0
- package/lib/runtime.js.map +1 -0
- package/lib/service.d.ts +77 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +229 -0
- package/lib/service.js.map +1 -0
- package/lib/sources/cenc.d.ts +37 -0
- package/lib/sources/cenc.d.ts.map +1 -0
- package/lib/sources/cenc.js +110 -0
- package/lib/sources/cenc.js.map +1 -0
- package/lib/sources/feed-source.d.ts +51 -0
- package/lib/sources/feed-source.d.ts.map +1 -0
- package/lib/sources/feed-source.js +115 -0
- package/lib/sources/feed-source.js.map +1 -0
- package/lib/sources/parse-helpers.d.ts +10 -0
- package/lib/sources/parse-helpers.d.ts.map +1 -0
- package/lib/sources/parse-helpers.js +36 -0
- package/lib/sources/parse-helpers.js.map +1 -0
- package/lib/sources/usgs.d.ts +14 -0
- package/lib/sources/usgs.d.ts.map +1 -0
- package/lib/sources/usgs.js +81 -0
- package/lib/sources/usgs.js.map +1 -0
- package/lib/subscribe-args.d.ts +9 -0
- package/lib/subscribe-args.d.ts.map +1 -0
- package/lib/subscribe-args.js +40 -0
- package/lib/subscribe-args.js.map +1 -0
- package/lib/subscriptions.d.ts +36 -0
- package/lib/subscriptions.d.ts.map +1 -0
- package/lib/subscriptions.js +122 -0
- package/lib/subscriptions.js.map +1 -0
- package/lib/types.d.ts +36 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/package.json +105 -3
- package/plugin.js +71 -0
- package/schema.json +153 -0
- package/skills/earthquake/SKILL.md +37 -0
- package/skills/earthquake/tools/recent_earthquakes/index.js +19 -0
package/README.md
CHANGED
|
@@ -1,3 +1,116 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zhin.js/plugin-earthquake
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
地震查询与群推送插件:合并中国地震台网(CENC)与 USGS 的公开数据,按震级和地区订阅速报。不需要任何 API Key,不含适配器专用代码,所有平台通用。
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm exec zhin install @zhin.js/plugin-earthquake
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
安装命令会在主配置 `plugins` 映射中挂载 `earthquake` 实例。对应的 `zhin.config.yml` 节点:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
configVersion: 2
|
|
17
|
+
plugins:
|
|
18
|
+
earthquake: "@zhin.js/plugin-earthquake"
|
|
19
|
+
earthquake: {}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 命令
|
|
23
|
+
|
|
24
|
+
| 命令 | 说明 |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `地震 [条数]` | 查询最近地震。选项:`--min/-m` 最小震级(默认 3)、`--region/-r` 地区关键词、`--hours/-t` 时间范围(小时,默认 24,最多 168)。每人 5 秒冷却 |
|
|
27
|
+
| `地震 订阅 [最小震级] [地区...]` | 在当前会话订阅速报(群聊限群主、管理员或机器人主人,见下文权限),例如 `地震 订阅 5 中国`、`地震 订阅 6 日本 Japan`。不写震级时用 `defaultMinMagnitude`(默认 5);不写地区表示全球。再次执行会覆盖原订阅 |
|
|
28
|
+
| `地震 退订` | 取消当前会话的订阅(权限同上) |
|
|
29
|
+
| `地震 订阅状态` | 查看当前会话的订阅与各数据源健康状况 |
|
|
30
|
+
|
|
31
|
+
### 权限
|
|
32
|
+
|
|
33
|
+
- **群聊 / 频道**:只有群主(`owner`)、群管理员(`admin`)或机器人主人(`master`)可以执行 `地震 订阅`、`地震 退订`;其他成员会收到“只有群主、群管理员或机器人主人可以在群聊中修改地震订阅”的提示。
|
|
34
|
+
- **私聊**:用户可以直接订阅、退订自己的私聊会话,不受限制。
|
|
35
|
+
- `地震` 查询与 `地震 订阅状态` 对所有人开放。
|
|
36
|
+
|
|
37
|
+
判断使用框架内置的 permit 检查器(`private()` 或 `role(owner,admin,master)`),角色来自适配器投影的通用 `sender.role`,不含平台专用代码。如果某个平台的适配器不提供群角色,群里只有机器人主人能修改订阅。
|
|
38
|
+
|
|
39
|
+
地区是对地点文字的关键词匹配(不区分大小写),同时匹配 CENC 的中文地名与 USGS 的英文地名;`中国` / `国内` 是预设,匹配 CENC 中以省级行政区开头的地点以及 USGS 中含 China / Taiwan 的地点。震级阈值按各数据源报告中的最大震级判断。
|
|
40
|
+
|
|
41
|
+
### Agent 工具
|
|
42
|
+
|
|
43
|
+
技能 `earthquake` 提供只读工具 `recent_earthquakes`(参数 `limit`、`minMagnitude`、`region`、`hours`),回答“刚才哪里地震了”之类的问题。
|
|
44
|
+
|
|
45
|
+
### 推送前提
|
|
46
|
+
|
|
47
|
+
订阅推送需要 Outbound Host 与 Schedule Host(标准 Runtime 默认提供)。配置数据库时订阅持久保存在插件状态中;没有数据库时保存在内存里,重启后丢失。
|
|
48
|
+
|
|
49
|
+
## 数据源
|
|
50
|
+
|
|
51
|
+
| 数据源 | 默认地址 | 说明 |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| CENC | `https://api.wolfx.jp/cenc_eqlist.json`(主,第三方公开镜像) → `https://ceic.ac.cn/data/data.json`(备用,官网“最新地震”页面的静态数据) | 中文地名;时间为北京时间;可用 `cencUrls` 改为自建镜像 |
|
|
54
|
+
| USGS | `https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/<usgsFeed>.geojson` | 全球覆盖,作为 CENC 失败时的兜底;`usgsFeed` 默认 `2.5_day`(24 小时内 M2.5+) |
|
|
55
|
+
|
|
56
|
+
旧的 CENC 接口(`news.ceic.ac.cn/ajax/google`、`www.ceic.ac.cn/ajax/*`)已不可用,本插件不使用。两个数据源都没有公开的接口契约,解析按“容错”设计:数字字符串可接受,坏记录逐条跳过;整批都无法解析时视为该地址失败并切换下一个地址或数据源。
|
|
57
|
+
|
|
58
|
+
`地震 --hours` 超过 USGS 汇总源的时间跨度时(例如 `2.5_day` 只有 24 小时),更早的数据只来自 CENC。
|
|
59
|
+
|
|
60
|
+
## 去重
|
|
61
|
+
|
|
62
|
+
同一地震在不同数据源中的 ID、时间、震中和震级类型(CENC 的 M 与 USGS 的 mb/Mw)都会略有差异。满足以下全部条件即视为同一事件:
|
|
63
|
+
|
|
64
|
+
- 发震时间相差 ≤ 60 秒;
|
|
65
|
+
- 震中距离(大圆距离)≤ 100 km;
|
|
66
|
+
- 震级相差 ≤ 1.0(CENC M 与 USGS mb 常相差约 1 级)。
|
|
67
|
+
|
|
68
|
+
同一数据源内 ID 相同,或时间相差 ≤ 5 秒且距离 ≤ 50 km 也视为同一事件(不同 CENC 镜像会给同一地震分配不同 ID)。合并后的事件以 `sources` 中靠前的数据源作为主报告(提供地点文字)。
|
|
69
|
+
|
|
70
|
+
## 容错设计
|
|
71
|
+
|
|
72
|
+
- **超时**:每次 HTTP 请求单独超时(`requestTimeoutMs`,默认 10 秒)。
|
|
73
|
+
- **重试**:网络错误、超时、408/425/429/5xx 按指数退避加随机抖动重试(`maxRetries` 默认 2 次,首次等待 `retryBaseDelayMs` 1 秒,单次上限 `retryMaxDelayMs` 30 秒);其他 4xx 不重试。
|
|
74
|
+
- **Retry-After**:遵守秒数或 HTTP 日期格式;要求的等待超过单次上限时本轮放弃,并让该数据源冷却到指定时间(最长 1 小时)。
|
|
75
|
+
- **冷却**:一轮全部失败的数据源按 30 秒、60 秒、120 秒……递增冷却,上限 10 分钟,期间由其他数据源兜底。
|
|
76
|
+
- **镜像切换**:CENC 的多个地址按顺序尝试。
|
|
77
|
+
- **条件请求**:使用 ETag / Last-Modified,数据未变化时服务器返回 304,复用缓存。
|
|
78
|
+
- **过期数据兜底**:所有数据源都失败时,`地震` 查询返回上次成功获取的数据并明确标注。
|
|
79
|
+
- **轮询不中断**:轮询从不抛错,失败只记录日志,下一轮照常执行;上一轮未结束时跳过本轮。
|
|
80
|
+
- **不刷屏**:每个数据源的第一次成功获取只建立基线、不推送;超过 `maxEventAgeMinutes`(默认 60 分钟)的事件不推送;每个事件只推送一次(发送前标记,发送失败只记录日志、不重试,避免重复投递)。
|
|
81
|
+
- **推送限流**:每个会话每 `pushWindowMinutes`(默认 10 分钟)最多 `pushLimit`(默认 3)条消息;一轮中的多个事件合并为一条消息,最多列出 `maxEventsPerMessage`(默认 5)个。被限流跳过的事件数会在下一条推送中注明。限流状态只保存在内存中。
|
|
82
|
+
|
|
83
|
+
## Configuration
|
|
84
|
+
|
|
85
|
+
所有字段均可省略,越界数值会被收敛到安全范围。完整字段见 `schema.json` 与生成的配置参考。
|
|
86
|
+
|
|
87
|
+
| 字段 | 默认值 | 说明 |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| `sources` | `["cenc", "usgs"]` | 启用的数据源及优先顺序 |
|
|
90
|
+
| `cencUrls` | 见上表 | CENC 地址,按顺序尝试;仅 HTTPS,最多 5 个 |
|
|
91
|
+
| `usgsFeed` | `2.5_day` | USGS 汇总源 |
|
|
92
|
+
| `pollCron` | `0 */2 * * * *` | 订阅轮询频率(6 段 Cron) |
|
|
93
|
+
| `timezone` | `Asia/Shanghai` | 显示时间使用的 IANA 时区 |
|
|
94
|
+
| `requestTimeoutMs` | `10000` | 单次请求超时 |
|
|
95
|
+
| `maxRetries` | `2` | 瞬时错误重试次数 |
|
|
96
|
+
| `retryBaseDelayMs` | `1000` | 退避首次等待 |
|
|
97
|
+
| `retryMaxDelayMs` | `30000` | 单次退避 / Retry-After 上限 |
|
|
98
|
+
| `defaultMinMagnitude` | `5` | 订阅默认最小震级 |
|
|
99
|
+
| `queryMinMagnitude` | `3` | 查询默认最小震级 |
|
|
100
|
+
| `queryLimit` | `5` | 查询默认条数 |
|
|
101
|
+
| `maxEventAgeMinutes` | `60` | 只推送这么多分钟以内发生的地震 |
|
|
102
|
+
| `pushLimit` / `pushWindowMinutes` | `3` / `10` | 每会话推送限流 |
|
|
103
|
+
| `maxEventsPerMessage` | `5` | 单条推送最多列出的地震数 |
|
|
104
|
+
| `maxSubscriptions` | `100` | 本实例订阅总数上限 |
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
earthquake:
|
|
108
|
+
sources: [cenc, usgs]
|
|
109
|
+
defaultMinMagnitude: 4.5
|
|
110
|
+
pushLimit: 2
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 说明
|
|
114
|
+
|
|
115
|
+
- 地震数据仅供参考,不能替代官方预警与应急信息。
|
|
116
|
+
- 功能设计参考了 kivibot 的地震速报插件,但仅参考功能,代码与数据均为独立实现。
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineCommand } from 'zhin.js/command';
|
|
3
|
+
import { renderRecentEarthquakes } from "../../lib/query-text.js";
|
|
4
|
+
import { earthquakeServiceToken } from "../../lib/runtime.js";
|
|
5
|
+
export default defineCommand({
|
|
6
|
+
description: '查询最近的地震(CENC 中国地震台网 / USGS)',
|
|
7
|
+
params: {
|
|
8
|
+
count: { type: 'integer', optional: true, min: 1, max: 20, description: '列出条数' },
|
|
9
|
+
},
|
|
10
|
+
options: {
|
|
11
|
+
min: { type: 'float', alias: 'm', min: 0, max: 10, description: '最小震级' },
|
|
12
|
+
region: { type: 'string', alias: 'r', description: '地区关键词,如 四川、日本、中国' },
|
|
13
|
+
hours: { type: 'integer', alias: 't', min: 1, max: 168, description: '时间范围(小时),默认 24' },
|
|
14
|
+
},
|
|
15
|
+
execution: { cooldownMs: 5_000, progressAfterMs: 3_000 },
|
|
16
|
+
async execute({ params, options, use }) {
|
|
17
|
+
const service = use(earthquakeServiceToken);
|
|
18
|
+
return renderRecentEarthquakes(service, {
|
|
19
|
+
limit: params.count,
|
|
20
|
+
minMagnitude: options.min,
|
|
21
|
+
region: options.region,
|
|
22
|
+
hours: options.hours,
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
/**
|
|
3
|
+
* Who may change a conversation's earthquake subscription.
|
|
4
|
+
*
|
|
5
|
+
* - Private chat: the user manages their own conversation, no restriction.
|
|
6
|
+
* - Group or channel: only the group owner, group admins or the bot master.
|
|
7
|
+
*
|
|
8
|
+
* `permit` lists are AND-only, so this "private OR role(...)" rule is evaluated
|
|
9
|
+
* with the same built-in permit checker the command router uses. Roles come
|
|
10
|
+
* from the adapter's platform-agnostic projection (`sender.role`).
|
|
11
|
+
*/
|
|
12
|
+
import { checkBuiltinPermit } from '@zhin.js/command';
|
|
13
|
+
export const SUBSCRIPTION_MANAGER_ROLES_PERMIT = 'role(owner,admin,master)';
|
|
14
|
+
export const PRIVATE_CHAT_PERMIT = 'private()';
|
|
15
|
+
export const SUBSCRIPTION_PERMISSION_DENIED_MESSAGE = '只有群主、群管理员或机器人主人可以在群聊中修改地震订阅;私聊中可以直接订阅本会话。';
|
|
16
|
+
export function canManageSubscription(subject) {
|
|
17
|
+
if (checkBuiltinPermit(PRIVATE_CHAT_PERMIT, subject))
|
|
18
|
+
return true;
|
|
19
|
+
return checkBuiltinPermit(SUBSCRIPTION_MANAGER_ROLES_PERMIT, subject);
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineCommand } from 'zhin.js/command';
|
|
3
|
+
import { conversationError, extractConversation } from "../../../lib/conversation.js";
|
|
4
|
+
import { canManageSubscription, SUBSCRIPTION_PERMISSION_DENIED_MESSAGE } from '../manage-permission.js';
|
|
5
|
+
import { earthquakeServiceToken } from "../../../lib/runtime.js";
|
|
6
|
+
import { parseSubscribeArgs } from "../../../lib/subscribe-args.js";
|
|
7
|
+
import { SubscriptionLimitError } from "../../../lib/subscriptions.js";
|
|
8
|
+
export default defineCommand({
|
|
9
|
+
description: '在当前会话订阅地震速报:地震 订阅 [最小震级] [地区...](群聊限群主、管理员或机器人主人)',
|
|
10
|
+
params: {
|
|
11
|
+
filters: { type: 'word', rest: true, description: '可选:最小震级与地区关键词,如 5 四川 云南' },
|
|
12
|
+
},
|
|
13
|
+
async execute({ params, input, use, adapter, scene, sender }) {
|
|
14
|
+
const service = use(earthquakeServiceToken);
|
|
15
|
+
const target = extractConversation(input);
|
|
16
|
+
if (!target)
|
|
17
|
+
return conversationError(input);
|
|
18
|
+
if (!canManageSubscription({ adapter, scene, sender }))
|
|
19
|
+
return SUBSCRIPTION_PERMISSION_DENIED_MESSAGE;
|
|
20
|
+
const parsed = parseSubscribeArgs(params.filters ?? []);
|
|
21
|
+
if ('error' in parsed)
|
|
22
|
+
return parsed.error;
|
|
23
|
+
const minMagnitude = parsed.minMagnitude ?? service.config.defaultMinMagnitude;
|
|
24
|
+
try {
|
|
25
|
+
const { created } = await service.store.upsert({
|
|
26
|
+
adapter: target.adapter,
|
|
27
|
+
endpointKey: target.endpointKey,
|
|
28
|
+
conversationKind: target.conversationKind,
|
|
29
|
+
conversationId: target.conversationId,
|
|
30
|
+
minMagnitude,
|
|
31
|
+
regions: parsed.regions,
|
|
32
|
+
createdBy: target.senderId,
|
|
33
|
+
createdAt: new Date().toISOString(),
|
|
34
|
+
});
|
|
35
|
+
const regionText = parsed.regions.length > 0 ? parsed.regions.join('、') : '全球';
|
|
36
|
+
const lines = [
|
|
37
|
+
`${created ? '已订阅' : '已更新订阅'}:M${minMagnitude.toFixed(1)} 以上,地区:${regionText}`,
|
|
38
|
+
];
|
|
39
|
+
if (!service.canPush) {
|
|
40
|
+
lines.push('⚠️ 当前运行环境没有出站发送能力,订阅已保存但暂时不会推送');
|
|
41
|
+
}
|
|
42
|
+
lines.push('取消订阅请发送:地震 退订');
|
|
43
|
+
return lines.join('\n');
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof SubscriptionLimitError)
|
|
47
|
+
return error.message;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
package/commands//345/234/260/351/234/207//350/256/242/351/230/205/347/212/266/346/200/201/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineCommand } from 'zhin.js/command';
|
|
3
|
+
import { conversationError, extractConversation } from "../../../lib/conversation.js";
|
|
4
|
+
import { formatEventTime, sourceLabel } from "../../../lib/format.js";
|
|
5
|
+
import { earthquakeServiceToken } from "../../../lib/runtime.js";
|
|
6
|
+
import { subscriptionKey } from "../../../lib/subscriptions.js";
|
|
7
|
+
export default defineCommand({
|
|
8
|
+
description: '查看当前会话的地震订阅与数据源状态',
|
|
9
|
+
async execute({ input, use }) {
|
|
10
|
+
const service = use(earthquakeServiceToken);
|
|
11
|
+
const target = extractConversation(input);
|
|
12
|
+
if (!target)
|
|
13
|
+
return conversationError(input);
|
|
14
|
+
const subscription = await service.store.get(subscriptionKey(target));
|
|
15
|
+
const timezone = service.config.timezone;
|
|
16
|
+
const lines = [];
|
|
17
|
+
if (subscription) {
|
|
18
|
+
const regionText = subscription.regions.length > 0 ? subscription.regions.join('、') : '全球';
|
|
19
|
+
lines.push(`本会话订阅:M${subscription.minMagnitude.toFixed(1)} 以上,地区:${regionText}`);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
lines.push('本会话未订阅。订阅示例:地震 订阅 5 中国');
|
|
23
|
+
}
|
|
24
|
+
lines.push('数据源:');
|
|
25
|
+
for (const health of service.health()) {
|
|
26
|
+
let status = '正常';
|
|
27
|
+
if (health.coolingDownUntil > 0) {
|
|
28
|
+
status = `冷却至 ${formatEventTime(health.coolingDownUntil, timezone)}`;
|
|
29
|
+
}
|
|
30
|
+
else if (health.consecutiveFailures > 0) {
|
|
31
|
+
status = `连续失败 ${health.consecutiveFailures} 次`;
|
|
32
|
+
}
|
|
33
|
+
else if (health.lastSuccessAt === undefined) {
|
|
34
|
+
status = '尚未请求';
|
|
35
|
+
}
|
|
36
|
+
const reason = health.lastError ? `(${health.lastError})` : '';
|
|
37
|
+
lines.push(`- ${sourceLabel(health.id)}:${status}${reason}`);
|
|
38
|
+
}
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineCommand } from 'zhin.js/command';
|
|
3
|
+
import { conversationError, extractConversation } from "../../../lib/conversation.js";
|
|
4
|
+
import { canManageSubscription, SUBSCRIPTION_PERMISSION_DENIED_MESSAGE } from '../manage-permission.js';
|
|
5
|
+
import { earthquakeServiceToken } from "../../../lib/runtime.js";
|
|
6
|
+
import { subscriptionKey } from "../../../lib/subscriptions.js";
|
|
7
|
+
export default defineCommand({
|
|
8
|
+
description: '取消当前会话的地震速报订阅(群聊限群主、管理员或机器人主人)',
|
|
9
|
+
async execute({ input, use, adapter, scene, sender }) {
|
|
10
|
+
const service = use(earthquakeServiceToken);
|
|
11
|
+
const target = extractConversation(input);
|
|
12
|
+
if (!target)
|
|
13
|
+
return conversationError(input);
|
|
14
|
+
if (!canManageSubscription({ adapter, scene, sender }))
|
|
15
|
+
return SUBSCRIPTION_PERMISSION_DENIED_MESSAGE;
|
|
16
|
+
const removed = await service.store.remove(subscriptionKey(target));
|
|
17
|
+
return removed ? '已取消本会话的地震速报订阅' : '本会话没有地震速报订阅';
|
|
18
|
+
},
|
|
19
|
+
});
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin configuration with conservative, documented defaults.
|
|
3
|
+
*
|
|
4
|
+
* Every field is optional. `resolveEarthquakeConfig` fills defaults and clamps
|
|
5
|
+
* out-of-range numbers so a typo in `zhin.config.yml` degrades to a safe value
|
|
6
|
+
* instead of hammering the public feeds or flooding a group.
|
|
7
|
+
*/
|
|
8
|
+
export declare const EARTHQUAKE_SOURCE_IDS: readonly ["cenc", "usgs"];
|
|
9
|
+
export type EarthquakeSourceId = (typeof EARTHQUAKE_SOURCE_IDS)[number];
|
|
10
|
+
/** USGS summary feeds that make sense for alerts (larger feeds are wasteful). */
|
|
11
|
+
export declare const USGS_FEEDS: readonly ["significant_hour", "significant_day", "4.5_hour", "4.5_day", "2.5_hour", "2.5_day", "1.0_hour", "1.0_day"];
|
|
12
|
+
export type UsgsFeed = (typeof USGS_FEEDS)[number];
|
|
13
|
+
export interface EarthquakeConfig {
|
|
14
|
+
/** Sources in preference order. The first one wins location text when events merge. */
|
|
15
|
+
readonly sources?: readonly EarthquakeSourceId[];
|
|
16
|
+
/** CENC catalogue URLs: primary first, then mirrors/fallbacks. HTTPS only. */
|
|
17
|
+
readonly cencUrls?: readonly string[];
|
|
18
|
+
/** USGS GeoJSON summary feed name. */
|
|
19
|
+
readonly usgsFeed?: UsgsFeed;
|
|
20
|
+
/** 6-field solar cron for the subscription poller. */
|
|
21
|
+
readonly pollCron?: string;
|
|
22
|
+
/** IANA time zone used to display event times. */
|
|
23
|
+
readonly timezone?: string;
|
|
24
|
+
/** Per-attempt HTTP timeout. */
|
|
25
|
+
readonly requestTimeoutMs?: number;
|
|
26
|
+
/** Retries after the first attempt for transient failures (network, 408, 425, 429, 5xx). */
|
|
27
|
+
readonly maxRetries?: number;
|
|
28
|
+
/** Base delay for exponential backoff with jitter. */
|
|
29
|
+
readonly retryBaseDelayMs?: number;
|
|
30
|
+
/** Upper bound for a single backoff or Retry-After wait inside one request. */
|
|
31
|
+
readonly retryMaxDelayMs?: number;
|
|
32
|
+
/** Magnitude threshold for new subscriptions that do not name one. */
|
|
33
|
+
readonly defaultMinMagnitude?: number;
|
|
34
|
+
/** Magnitude threshold for the `地震` query when none is given. */
|
|
35
|
+
readonly queryMinMagnitude?: number;
|
|
36
|
+
/** Default number of events listed by the `地震` query. */
|
|
37
|
+
readonly queryLimit?: number;
|
|
38
|
+
/** Events older than this are never pushed (protects against late or replayed feeds). */
|
|
39
|
+
readonly maxEventAgeMinutes?: number;
|
|
40
|
+
/** Messages allowed per conversation inside one push window. */
|
|
41
|
+
readonly pushLimit?: number;
|
|
42
|
+
/** Length of the push rate-limit window. */
|
|
43
|
+
readonly pushWindowMinutes?: number;
|
|
44
|
+
/** Events listed in one push message; the rest are summarised as a count. */
|
|
45
|
+
readonly maxEventsPerMessage?: number;
|
|
46
|
+
/** Total subscriptions across all conversations for this plugin instance. */
|
|
47
|
+
readonly maxSubscriptions?: number;
|
|
48
|
+
}
|
|
49
|
+
export type ResolvedEarthquakeConfig = {
|
|
50
|
+
readonly [K in keyof EarthquakeConfig]-?: NonNullable<EarthquakeConfig[K]>;
|
|
51
|
+
};
|
|
52
|
+
export declare const DEFAULT_EARTHQUAKE_CONFIG: ResolvedEarthquakeConfig;
|
|
53
|
+
/** Maximum regions a single subscription may name. */
|
|
54
|
+
export declare const MAX_REGIONS_PER_SUBSCRIPTION = 5;
|
|
55
|
+
/** Throws when the configured time zone is unknown to the runtime's Intl data. */
|
|
56
|
+
export declare function assertEarthquakeTimezone(timezone: string): void;
|
|
57
|
+
export declare function resolveEarthquakeConfig(raw: unknown): ResolvedEarthquakeConfig;
|
|
58
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,qBAAqB,2BAA4B,CAAC;AAC/D,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE,iFAAiF;AACjF,eAAO,MAAM,UAAU,uHASb,CAAC;AACX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,MAAM,WAAW,gBAAgB;IAC/B,uFAAuF;IACvF,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACjD,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,kDAAkD;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,gCAAgC;IAChC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,4FAA4F;IAC5F,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,sDAAsD;IACtD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,+EAA+E;IAC/E,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,iEAAiE;IACjE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,yDAAyD;IACzD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,yFAAyF;IACzF,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,gEAAgE;IAChE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,4CAA4C;IAC5C,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,6EAA6E;IAC7E,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,6EAA6E;IAC7E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,CAAC,IAAI,MAAM,gBAAgB,CAAC,CAAC,GAAG,WAAW,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;CAC3E,CAAC;AAEF,eAAO,MAAM,yBAAyB,EAAE,wBAqBtC,CAAC;AAEH,sDAAsD;AACtD,eAAO,MAAM,4BAA4B,IAAI,CAAC;AA6D9C,kFAAkF;AAClF,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAM/D;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,wBAAwB,CAwB9E"}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin configuration with conservative, documented defaults.
|
|
3
|
+
*
|
|
4
|
+
* Every field is optional. `resolveEarthquakeConfig` fills defaults and clamps
|
|
5
|
+
* out-of-range numbers so a typo in `zhin.config.yml` degrades to a safe value
|
|
6
|
+
* instead of hammering the public feeds or flooding a group.
|
|
7
|
+
*/
|
|
8
|
+
export const EARTHQUAKE_SOURCE_IDS = ['cenc', 'usgs'];
|
|
9
|
+
/** USGS summary feeds that make sense for alerts (larger feeds are wasteful). */
|
|
10
|
+
export const USGS_FEEDS = [
|
|
11
|
+
'significant_hour',
|
|
12
|
+
'significant_day',
|
|
13
|
+
'4.5_hour',
|
|
14
|
+
'4.5_day',
|
|
15
|
+
'2.5_hour',
|
|
16
|
+
'2.5_day',
|
|
17
|
+
'1.0_hour',
|
|
18
|
+
'1.0_day',
|
|
19
|
+
];
|
|
20
|
+
export const DEFAULT_EARTHQUAKE_CONFIG = Object.freeze({
|
|
21
|
+
sources: Object.freeze(['cenc', 'usgs']),
|
|
22
|
+
cencUrls: Object.freeze([
|
|
23
|
+
'https://api.wolfx.jp/cenc_eqlist.json',
|
|
24
|
+
'https://ceic.ac.cn/data/data.json',
|
|
25
|
+
]),
|
|
26
|
+
usgsFeed: '2.5_day',
|
|
27
|
+
pollCron: '0 */2 * * * *',
|
|
28
|
+
timezone: 'Asia/Shanghai',
|
|
29
|
+
requestTimeoutMs: 10_000,
|
|
30
|
+
maxRetries: 2,
|
|
31
|
+
retryBaseDelayMs: 1_000,
|
|
32
|
+
retryMaxDelayMs: 30_000,
|
|
33
|
+
defaultMinMagnitude: 5,
|
|
34
|
+
queryMinMagnitude: 3,
|
|
35
|
+
queryLimit: 5,
|
|
36
|
+
maxEventAgeMinutes: 60,
|
|
37
|
+
pushLimit: 3,
|
|
38
|
+
pushWindowMinutes: 10,
|
|
39
|
+
maxEventsPerMessage: 5,
|
|
40
|
+
maxSubscriptions: 100,
|
|
41
|
+
});
|
|
42
|
+
/** Maximum regions a single subscription may name. */
|
|
43
|
+
export const MAX_REGIONS_PER_SUBSCRIPTION = 5;
|
|
44
|
+
function clampNumber(value, fallback, min, max) {
|
|
45
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
return Math.min(max, Math.max(min, value));
|
|
49
|
+
}
|
|
50
|
+
function resolveSources(value) {
|
|
51
|
+
if (!Array.isArray(value)) {
|
|
52
|
+
return DEFAULT_EARTHQUAKE_CONFIG.sources;
|
|
53
|
+
}
|
|
54
|
+
const known = new Set(EARTHQUAKE_SOURCE_IDS);
|
|
55
|
+
const unique = [];
|
|
56
|
+
for (const entry of value) {
|
|
57
|
+
if (typeof entry !== 'string')
|
|
58
|
+
continue;
|
|
59
|
+
if (!known.has(entry))
|
|
60
|
+
continue;
|
|
61
|
+
if (unique.includes(entry))
|
|
62
|
+
continue;
|
|
63
|
+
unique.push(entry);
|
|
64
|
+
}
|
|
65
|
+
if (unique.length === 0) {
|
|
66
|
+
return DEFAULT_EARTHQUAKE_CONFIG.sources;
|
|
67
|
+
}
|
|
68
|
+
return Object.freeze(unique);
|
|
69
|
+
}
|
|
70
|
+
const MAX_CENC_URLS = 5;
|
|
71
|
+
function resolveCencUrls(value) {
|
|
72
|
+
if (!Array.isArray(value)) {
|
|
73
|
+
return DEFAULT_EARTHQUAKE_CONFIG.cencUrls;
|
|
74
|
+
}
|
|
75
|
+
const urls = [];
|
|
76
|
+
for (const entry of value) {
|
|
77
|
+
if (typeof entry !== 'string')
|
|
78
|
+
continue;
|
|
79
|
+
const trimmed = entry.trim();
|
|
80
|
+
if (!trimmed.startsWith('https://'))
|
|
81
|
+
continue;
|
|
82
|
+
if (urls.includes(trimmed))
|
|
83
|
+
continue;
|
|
84
|
+
urls.push(trimmed);
|
|
85
|
+
}
|
|
86
|
+
if (urls.length === 0) {
|
|
87
|
+
return DEFAULT_EARTHQUAKE_CONFIG.cencUrls;
|
|
88
|
+
}
|
|
89
|
+
return Object.freeze(urls.slice(0, MAX_CENC_URLS));
|
|
90
|
+
}
|
|
91
|
+
function resolveUsgsFeed(value) {
|
|
92
|
+
const known = new Set(USGS_FEEDS);
|
|
93
|
+
if (typeof value === 'string' && known.has(value)) {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
return DEFAULT_EARTHQUAKE_CONFIG.usgsFeed;
|
|
97
|
+
}
|
|
98
|
+
function resolveString(value, fallback) {
|
|
99
|
+
if (typeof value !== 'string')
|
|
100
|
+
return fallback;
|
|
101
|
+
const trimmed = value.trim();
|
|
102
|
+
return trimmed || fallback;
|
|
103
|
+
}
|
|
104
|
+
/** Throws when the configured time zone is unknown to the runtime's Intl data. */
|
|
105
|
+
export function assertEarthquakeTimezone(timezone) {
|
|
106
|
+
try {
|
|
107
|
+
new Intl.DateTimeFormat('zh-CN', { timeZone: timezone });
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
throw new Error(`地震插件 timezone 无效:${timezone}(需要 IANA 时区,例如 Asia/Shanghai)`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function resolveEarthquakeConfig(raw) {
|
|
114
|
+
const input = (raw !== null && typeof raw === 'object' ? raw : {});
|
|
115
|
+
const defaults = DEFAULT_EARTHQUAKE_CONFIG;
|
|
116
|
+
const retryBaseDelayMs = clampNumber(input.retryBaseDelayMs, defaults.retryBaseDelayMs, 100, 10_000);
|
|
117
|
+
const retryMaxDelayMs = clampNumber(input.retryMaxDelayMs, defaults.retryMaxDelayMs, 1_000, 120_000);
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
sources: resolveSources(input.sources),
|
|
120
|
+
cencUrls: resolveCencUrls(input.cencUrls),
|
|
121
|
+
usgsFeed: resolveUsgsFeed(input.usgsFeed),
|
|
122
|
+
pollCron: resolveString(input.pollCron, defaults.pollCron),
|
|
123
|
+
timezone: resolveString(input.timezone, defaults.timezone),
|
|
124
|
+
requestTimeoutMs: clampNumber(input.requestTimeoutMs, defaults.requestTimeoutMs, 1_000, 60_000),
|
|
125
|
+
maxRetries: Math.round(clampNumber(input.maxRetries, defaults.maxRetries, 0, 5)),
|
|
126
|
+
retryBaseDelayMs,
|
|
127
|
+
retryMaxDelayMs: Math.max(retryBaseDelayMs, retryMaxDelayMs),
|
|
128
|
+
defaultMinMagnitude: clampNumber(input.defaultMinMagnitude, defaults.defaultMinMagnitude, 0, 10),
|
|
129
|
+
queryMinMagnitude: clampNumber(input.queryMinMagnitude, defaults.queryMinMagnitude, 0, 10),
|
|
130
|
+
queryLimit: Math.round(clampNumber(input.queryLimit, defaults.queryLimit, 1, 20)),
|
|
131
|
+
maxEventAgeMinutes: clampNumber(input.maxEventAgeMinutes, defaults.maxEventAgeMinutes, 5, 1_440),
|
|
132
|
+
pushLimit: Math.round(clampNumber(input.pushLimit, defaults.pushLimit, 1, 60)),
|
|
133
|
+
pushWindowMinutes: clampNumber(input.pushWindowMinutes, defaults.pushWindowMinutes, 1, 1_440),
|
|
134
|
+
maxEventsPerMessage: Math.round(clampNumber(input.maxEventsPerMessage, defaults.maxEventsPerMessage, 1, 10)),
|
|
135
|
+
maxSubscriptions: Math.round(clampNumber(input.maxSubscriptions, defaults.maxSubscriptions, 1, 1_000)),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ConversationKind } from './subscriptions.js';
|
|
2
|
+
export interface ConversationTarget {
|
|
3
|
+
readonly adapter: string;
|
|
4
|
+
readonly endpointKey: string;
|
|
5
|
+
readonly conversationKind: ConversationKind;
|
|
6
|
+
readonly conversationId: string;
|
|
7
|
+
readonly senderId: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const CONVERSATION_REQUIRED_MESSAGE = "\u8BF7\u5728\u673A\u5668\u4EBA\u79C1\u804A\u3001\u7FA4\u804A\u6216\u9891\u9053\u4E2D\u4F7F\u7528\u6B64\u547D\u4EE4";
|
|
10
|
+
export declare const THREAD_UNSUPPORTED_MESSAGE = "\u6682\u4E0D\u652F\u6301\u5728\u8BDD\u9898\uFF0F\u5B50\u9891\u9053\u5185\u8BA2\u9605\uFF0C\u8BF7\u5728\u4E3B\u4F1A\u8BDD\u4E2D\u4F7F\u7528";
|
|
11
|
+
export declare function conversationError(input: unknown): string;
|
|
12
|
+
/** Returns null when the input has no complete, un-threaded conversation address. */
|
|
13
|
+
export declare function extractConversation(input: unknown): ConversationTarget | null;
|
|
14
|
+
//# sourceMappingURL=conversation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../src/conversation.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAC5C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,6BAA6B,uHAAwB,CAAC;AACnE,eAAO,MAAM,0BAA0B,+IAA4B,CAAC;AAYpE,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGxD;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CAuB7E"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts a push destination from the public Message fields of a command input.
|
|
3
|
+
* Platform-agnostic: only adapter name, endpoint, conversation kind/id and sender id.
|
|
4
|
+
*/
|
|
5
|
+
import { isRecord } from './sources/parse-helpers.js';
|
|
6
|
+
export const CONVERSATION_REQUIRED_MESSAGE = '请在机器人私聊、群聊或频道中使用此命令';
|
|
7
|
+
export const THREAD_UNSUPPORTED_MESSAGE = '暂不支持在话题/子频道内订阅,请在主会话中使用';
|
|
8
|
+
function isIdentifier(value) {
|
|
9
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
10
|
+
}
|
|
11
|
+
function isThreaded(input) {
|
|
12
|
+
const conversation = input.conversation;
|
|
13
|
+
if (!isRecord(conversation))
|
|
14
|
+
return false;
|
|
15
|
+
return conversation.parent !== undefined || conversation.threadId !== undefined;
|
|
16
|
+
}
|
|
17
|
+
export function conversationError(input) {
|
|
18
|
+
if (isRecord(input) && isThreaded(input))
|
|
19
|
+
return THREAD_UNSUPPORTED_MESSAGE;
|
|
20
|
+
return CONVERSATION_REQUIRED_MESSAGE;
|
|
21
|
+
}
|
|
22
|
+
/** Returns null when the input has no complete, un-threaded conversation address. */
|
|
23
|
+
export function extractConversation(input) {
|
|
24
|
+
if (!isRecord(input))
|
|
25
|
+
return null;
|
|
26
|
+
if (isThreaded(input))
|
|
27
|
+
return null;
|
|
28
|
+
const conversation = input.conversation;
|
|
29
|
+
if (!isRecord(conversation))
|
|
30
|
+
return null;
|
|
31
|
+
const kind = conversation.kind;
|
|
32
|
+
if (kind !== 'private' && kind !== 'group' && kind !== 'channel')
|
|
33
|
+
return null;
|
|
34
|
+
// Same precedence as core: explicit message fields, then the conversation's endpoint.
|
|
35
|
+
const endpoint = isRecord(conversation.endpoint) ? conversation.endpoint : {};
|
|
36
|
+
const adapter = isIdentifier(input.clientAdapter) ? input.clientAdapter : endpoint.adapter;
|
|
37
|
+
const endpointKey = isIdentifier(input.endpointId) ? input.endpointId : endpoint.id;
|
|
38
|
+
if (!isIdentifier(adapter))
|
|
39
|
+
return null;
|
|
40
|
+
if (!isIdentifier(endpointKey))
|
|
41
|
+
return null;
|
|
42
|
+
if (!isIdentifier(conversation.id))
|
|
43
|
+
return null;
|
|
44
|
+
const sender = input.sender;
|
|
45
|
+
const senderId = isRecord(sender) && isIdentifier(sender.id) ? sender.id : '';
|
|
46
|
+
return {
|
|
47
|
+
adapter,
|
|
48
|
+
endpointKey,
|
|
49
|
+
conversationKind: kind,
|
|
50
|
+
conversationId: conversation.id,
|
|
51
|
+
senderId,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=conversation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation.js","sourceRoot":"","sources":["../src/conversation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAWtD,MAAM,CAAC,MAAM,6BAA6B,GAAG,qBAAqB,CAAC;AACnE,MAAM,CAAC,MAAM,0BAA0B,GAAG,yBAAyB,CAAC;AAEpE,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,UAAU,CAAC,KAA8B;IAChD,MAAM,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;IACxC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1C,OAAO,YAAY,CAAC,MAAM,KAAK,SAAS,IAAI,YAAY,CAAC,QAAQ,KAAK,SAAS,CAAC;AAClF,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,0BAA0B,CAAC;IAC5E,OAAO,6BAA6B,CAAC;AACvC,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,mBAAmB,CAAC,KAAc;IAChD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,IAAI,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;IACxC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC;IAC/B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC9E,sFAAsF;IACtF,MAAM,QAAQ,GAAG,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9E,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;IAC3F,MAAM,WAAW,GAAG,YAAY,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;IACpF,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IAChD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9E,OAAO;QACL,OAAO;QACP,WAAW;QACX,gBAAgB,EAAE,IAAI;QACtB,cAAc,EAAE,YAAY,CAAC,EAAE;QAC/B,QAAQ;KACT,CAAC;AACJ,CAAC"}
|
package/lib/dedupe.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-source de-duplication.
|
|
3
|
+
*
|
|
4
|
+
* CENC and USGS report the same physical event with different ids, slightly
|
|
5
|
+
* different origin times, epicentres and magnitude types (Ms vs Mw). Two reports
|
|
6
|
+
* are the same event when all three tolerances hold:
|
|
7
|
+
*
|
|
8
|
+
* - origin time within `timeToleranceMs` (default 60 s)
|
|
9
|
+
* - epicentres within `distanceToleranceKm` (default 100 km, great-circle)
|
|
10
|
+
* - magnitudes within `magnitudeTolerance` (default 1.0; CENC M vs USGS mb often differ by ~1)
|
|
11
|
+
*
|
|
12
|
+
* Reports from the same source match when their ids match, or when origin
|
|
13
|
+
* times are within `sameSourceTimeToleranceMs` (default 5 s) and epicentres
|
|
14
|
+
* within `sameSourceDistanceKm` (default 50 km). The second rule covers CENC
|
|
15
|
+
* mirrors that assign different ids to one event, and revised reports.
|
|
16
|
+
*/
|
|
17
|
+
import type { EarthquakeSourceId } from './config.js';
|
|
18
|
+
import type { EarthquakeEvent, SourceEarthquake } from './types.js';
|
|
19
|
+
export interface DedupeTolerance {
|
|
20
|
+
readonly timeToleranceMs: number;
|
|
21
|
+
readonly distanceToleranceKm: number;
|
|
22
|
+
readonly magnitudeTolerance: number;
|
|
23
|
+
readonly sameSourceTimeToleranceMs: number;
|
|
24
|
+
readonly sameSourceDistanceKm: number;
|
|
25
|
+
}
|
|
26
|
+
export declare const DEFAULT_DEDUPE_TOLERANCE: DedupeTolerance;
|
|
27
|
+
/** Great-circle distance (haversine) in kilometres. */
|
|
28
|
+
export declare function distanceKm(a: {
|
|
29
|
+
readonly latitude: number;
|
|
30
|
+
readonly longitude: number;
|
|
31
|
+
}, b: {
|
|
32
|
+
readonly latitude: number;
|
|
33
|
+
readonly longitude: number;
|
|
34
|
+
}): number;
|
|
35
|
+
/** True when two reports describe the same physical event. */
|
|
36
|
+
export declare function isSameEvent(a: SourceEarthquake, b: SourceEarthquake, tolerance?: DedupeTolerance): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Merges reports into events. The primary report of each event is the one from
|
|
39
|
+
* the most preferred source. Output is sorted newest first.
|
|
40
|
+
*/
|
|
41
|
+
export declare function mergeReports(reports: readonly SourceEarthquake[], preference: readonly EarthquakeSourceId[], tolerance?: DedupeTolerance): EarthquakeEvent[];
|
|
42
|
+
/**
|
|
43
|
+
* Remembers events that were already announced so the poller pushes each
|
|
44
|
+
* physical event once, even when the second source reports it minutes later.
|
|
45
|
+
* Bounded by count and age.
|
|
46
|
+
*/
|
|
47
|
+
export declare class SeenEventLog {
|
|
48
|
+
#private;
|
|
49
|
+
constructor(options?: {
|
|
50
|
+
readonly maxEntries?: number;
|
|
51
|
+
readonly maxAgeMs?: number;
|
|
52
|
+
readonly tolerance?: DedupeTolerance;
|
|
53
|
+
});
|
|
54
|
+
get size(): number;
|
|
55
|
+
has(event: EarthquakeEvent): boolean;
|
|
56
|
+
/** Records every report of the event so later reports from either source match. */
|
|
57
|
+
add(event: EarthquakeEvent): void;
|
|
58
|
+
prune(now: number): void;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=dedupe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../src/dedupe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEpE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,yBAAyB,EAAE,MAAM,CAAC;IAC3C,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;CACvC;AAED,eAAO,MAAM,wBAAwB,EAAE,eAMrC,CAAC;AAQH,uDAAuD;AACvD,wBAAgB,UAAU,CACxB,CAAC,EAAE;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EAC5D,CAAC,EAAE;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC3D,MAAM,CAQR;AAED,8DAA8D;AAC9D,wBAAgB,WAAW,CACzB,CAAC,EAAE,gBAAgB,EACnB,CAAC,EAAE,gBAAgB,EACnB,SAAS,GAAE,eAA0C,GACpD,OAAO,CAST;AAOD;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,SAAS,gBAAgB,EAAE,EACpC,UAAU,EAAE,SAAS,kBAAkB,EAAE,EACzC,SAAS,GAAE,eAA0C,GACpD,eAAe,EAAE,CAiCnB;AAED;;;;GAIG;AACH,qBAAa,YAAY;;gBAMX,OAAO,GAAE;QACnB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,eAAe,CAAC;KACjC;IAMN,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,GAAG,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO;IAIpC,mFAAmF;IACnF,GAAG,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI;IAOjC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAWzB"}
|