@easyoref/shared 1.21.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/dist/config.d.ts +128 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +158 -0
- package/dist/config.js.map +1 -0
- package/dist/helpers.d.ts +6 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +15 -0
- package/dist/helpers.js.map +1 -0
- package/dist/i18n.d.ts +51 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +248 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/redis.d.ts +6 -0
- package/dist/redis.d.ts.map +1 -0
- package/dist/redis.js +21 -0
- package/dist/redis.js.map +1 -0
- package/dist/schemas.d.ts +1496 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +556 -0
- package/dist/schemas.js.map +1 -0
- package/dist/store.d.ts +55 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +162 -0
- package/dist/store.js.map +1 -0
- package/package.json +22 -0
- package/src/config.ts +248 -0
- package/src/helpers.ts +17 -0
- package/src/i18n.ts +306 -0
- package/src/index.ts +6 -0
- package/src/redis.ts +23 -0
- package/src/schemas.ts +712 -0
- package/src/store.ts +232 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EACV,aAAa,EACb,SAAS,EACT,SAAS,EACT,WAAW,EACX,cAAc,EACd,eAAe,EAChB,MAAM,cAAc,CAAC;AAItB,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,CAAC;AAGvE,eAAO,MAAM,cAAc,UAAU,CAAC;AAKtC,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAWzD;AAOD,0DAA0D;AAC1D,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAItD,CAAC;AAEF,yCAAyC;AACzC,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAI3D,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAI5D,CAAC;AAIF,wBAAsB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAOlE;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAIhC;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAItE;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAI9D;AAID,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5E;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAI3E;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CASlD;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAG9D;AAID,wBAAsB,cAAc,IAAI,OAAO,CAC3C;IACE,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,SAAS,CAAC;CACtB,GACD,SAAS,CACZ,CAQA;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,WAAW,GAChB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,WAAW,EAAE,CAAC,CAExB;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5E;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,cAAc,CAAC,CAMjE;AAMD,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAIvD;AAED,wBAAsB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG/D;AAMD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAS9B;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,OAAO,CAAC,IAAI,CAAC,CAKf"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-based alert state store — Redis operations.
|
|
3
|
+
*
|
|
4
|
+
* A "session" spans the lifecycle of one attack event:
|
|
5
|
+
* early_warning → (optional red_alert) → resolved → +10 min tail
|
|
6
|
+
*
|
|
7
|
+
* Keys:
|
|
8
|
+
* session:active — ActiveSession JSON TTL 45min
|
|
9
|
+
* session:posts — LPUSH list of ChannelPost TTL 45min
|
|
10
|
+
* session:ext_cache — HASH {post_hash → extraction JSON} TTL 45min
|
|
11
|
+
* alert:{alertId}:meta — AlertMeta JSON TTL 20min
|
|
12
|
+
*
|
|
13
|
+
* Only the LATEST alert's Telegram message gets enrichment edits.
|
|
14
|
+
* Posts accumulate across the entire session (shared context).
|
|
15
|
+
*/
|
|
16
|
+
import { getRedis } from "./redis.js";
|
|
17
|
+
import { createEmptyEnrichmentData } from "./schemas.js";
|
|
18
|
+
// Schema version for migration handling
|
|
19
|
+
export const SCHEMA_VERSION = "2.0.0";
|
|
20
|
+
const SCHEMA_VERSION_KEY = "schema:version";
|
|
21
|
+
let schemaVersionChecked = false;
|
|
22
|
+
export async function ensureSchemaVersion() {
|
|
23
|
+
if (schemaVersionChecked)
|
|
24
|
+
return;
|
|
25
|
+
schemaVersionChecked = true;
|
|
26
|
+
const redis = getRedis();
|
|
27
|
+
const stored = await redis.get(SCHEMA_VERSION_KEY);
|
|
28
|
+
if (stored !== SCHEMA_VERSION) {
|
|
29
|
+
await redis.flushall();
|
|
30
|
+
await redis.set(SCHEMA_VERSION_KEY, SCHEMA_VERSION);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const META_TTL_S = 20 * 60; // 20 minutes
|
|
34
|
+
const SESSION_TTL_S = 45 * 60; // 45 min worst case
|
|
35
|
+
// ── Session phase timeouts ─────────────────────────────
|
|
36
|
+
/** Max duration (ms) for each phase before auto-expire */
|
|
37
|
+
export const PHASE_TIMEOUT_MS = {
|
|
38
|
+
early_warning: 30 * 60 * 1000, // 30 min
|
|
39
|
+
red_alert: 15 * 60 * 1000, // 15 min
|
|
40
|
+
resolved: 10 * 60 * 1000, // 10 min tail
|
|
41
|
+
};
|
|
42
|
+
/** Enrichment interval (ms) per phase */
|
|
43
|
+
export const PHASE_ENRICH_DELAY_MS = {
|
|
44
|
+
early_warning: 60_000, // 60s — channels need time to post; saves tokens
|
|
45
|
+
red_alert: 45_000, // 45s
|
|
46
|
+
resolved: 150_000, // 150s (2.5 min) — per user requirement: 10 min window, update every 2.5 min
|
|
47
|
+
};
|
|
48
|
+
/** Initial enrichment delay — first job after alert (channels need time to post) */
|
|
49
|
+
export const PHASE_INITIAL_DELAY_MS = {
|
|
50
|
+
early_warning: 120_000, // 2 min — wait for launch reports
|
|
51
|
+
red_alert: 15_000, // 15s
|
|
52
|
+
resolved: 90_000, // 90s — wait for first wave of post-incident reports
|
|
53
|
+
};
|
|
54
|
+
// ──Alert Meta (per-alert) ─────────────────────────────
|
|
55
|
+
export async function saveAlertMeta(meta) {
|
|
56
|
+
const redis = getRedis();
|
|
57
|
+
await redis.setex(`alert:${meta.alertId}:meta`, META_TTL_S, JSON.stringify(meta));
|
|
58
|
+
}
|
|
59
|
+
export async function getAlertMeta(alertId) {
|
|
60
|
+
const redis = getRedis();
|
|
61
|
+
const raw = await redis.get(`alert:${alertId}:meta`);
|
|
62
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
63
|
+
}
|
|
64
|
+
// ── Session posts (shared across entire session) ───────
|
|
65
|
+
export async function pushSessionPost(post) {
|
|
66
|
+
const redis = getRedis();
|
|
67
|
+
await redis.lpush("session:posts", JSON.stringify(post));
|
|
68
|
+
await redis.expire("session:posts", SESSION_TTL_S);
|
|
69
|
+
}
|
|
70
|
+
export async function getSessionPosts() {
|
|
71
|
+
const redis = getRedis();
|
|
72
|
+
const items = await redis.lrange("session:posts", 0, -1);
|
|
73
|
+
return items.map((i) => JSON.parse(i));
|
|
74
|
+
}
|
|
75
|
+
// ── Active session ─────────────────────────────────────
|
|
76
|
+
export async function setActiveSession(session) {
|
|
77
|
+
const redis = getRedis();
|
|
78
|
+
await redis.setex("session:active", SESSION_TTL_S, JSON.stringify(session));
|
|
79
|
+
}
|
|
80
|
+
export async function getActiveSession() {
|
|
81
|
+
const redis = getRedis();
|
|
82
|
+
const raw = await redis.get("session:active");
|
|
83
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
84
|
+
}
|
|
85
|
+
export async function clearSession() {
|
|
86
|
+
const redis = getRedis();
|
|
87
|
+
await redis.del("session:active", "session:posts", "session:enrichment", EXT_CACHE_KEY, LAST_UPDATE_KEY);
|
|
88
|
+
}
|
|
89
|
+
export function isPhaseExpired(session) {
|
|
90
|
+
const elapsed = Date.now() - session.phaseStartTs;
|
|
91
|
+
return elapsed >= PHASE_TIMEOUT_MS[session.phase];
|
|
92
|
+
}
|
|
93
|
+
// ── Compat shims (used by gramjs-monitor, graph) ───────
|
|
94
|
+
export async function getActiveAlert() {
|
|
95
|
+
const s = await getActiveSession();
|
|
96
|
+
if (!s)
|
|
97
|
+
return undefined;
|
|
98
|
+
return {
|
|
99
|
+
alertId: s.latestAlertId,
|
|
100
|
+
alertTs: s.latestAlertTs,
|
|
101
|
+
alertType: s.phase,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export async function pushChannelPost(_alertId, post) {
|
|
105
|
+
await pushSessionPost(post);
|
|
106
|
+
}
|
|
107
|
+
export async function getChannelPosts(_alertId) {
|
|
108
|
+
return getSessionPosts();
|
|
109
|
+
}
|
|
110
|
+
// ── Enrichment data (cross-phase persistence) ──────────
|
|
111
|
+
export async function saveEnrichmentData(data) {
|
|
112
|
+
const redis = getRedis();
|
|
113
|
+
await redis.setex("session:enrichment", SESSION_TTL_S, JSON.stringify(data));
|
|
114
|
+
}
|
|
115
|
+
export async function getEnrichmentData() {
|
|
116
|
+
const redis = getRedis();
|
|
117
|
+
const raw = await redis.get("session:enrichment");
|
|
118
|
+
return raw
|
|
119
|
+
? JSON.parse(raw)
|
|
120
|
+
: createEmptyEnrichmentData();
|
|
121
|
+
}
|
|
122
|
+
// ── Last update timestamp (tracks when last enrichment job ran) ──
|
|
123
|
+
const LAST_UPDATE_KEY = "session:last_update_ts";
|
|
124
|
+
export async function getLastUpdateTs() {
|
|
125
|
+
const redis = getRedis();
|
|
126
|
+
const raw = await redis.get(LAST_UPDATE_KEY);
|
|
127
|
+
return raw ? Number(raw) : 0;
|
|
128
|
+
}
|
|
129
|
+
export async function setLastUpdateTs(ts) {
|
|
130
|
+
const redis = getRedis();
|
|
131
|
+
await redis.setex(LAST_UPDATE_KEY, SESSION_TTL_S, String(ts));
|
|
132
|
+
}
|
|
133
|
+
// ── Extraction cache (post-level dedup between jobs) ───
|
|
134
|
+
const EXT_CACHE_KEY = "session:ext_cache";
|
|
135
|
+
/**
|
|
136
|
+
* Get cached extraction results for a batch of post hashes.
|
|
137
|
+
* Returns a map: postHash → serialized ValidatedExtraction JSON.
|
|
138
|
+
*/
|
|
139
|
+
export async function getCachedExtractions(postHashes) {
|
|
140
|
+
if (postHashes.length === 0)
|
|
141
|
+
return new Map();
|
|
142
|
+
const redis = getRedis();
|
|
143
|
+
const results = await redis.hmget(EXT_CACHE_KEY, ...postHashes);
|
|
144
|
+
const map = new Map();
|
|
145
|
+
postHashes.forEach((hash, i) => {
|
|
146
|
+
if (results[i])
|
|
147
|
+
map.set(hash, results[i]);
|
|
148
|
+
});
|
|
149
|
+
return map;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Save new extraction results to cache.
|
|
153
|
+
* @param entries - Record of postHash → serialized ValidatedExtraction JSON
|
|
154
|
+
*/
|
|
155
|
+
export async function saveCachedExtractions(entries) {
|
|
156
|
+
if (Object.keys(entries).length === 0)
|
|
157
|
+
return;
|
|
158
|
+
const redis = getRedis();
|
|
159
|
+
await redis.hset(EXT_CACHE_KEY, entries);
|
|
160
|
+
await redis.expire(EXT_CACHE_KEY, SESSION_TTL_S);
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@easyoref/shared",
|
|
3
|
+
"version": "1.21.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"js-yaml": "^4.1.1",
|
|
13
|
+
"ioredis": "^5.3.0",
|
|
14
|
+
"zod": "^4.3.6",
|
|
15
|
+
"@langchain/langgraph": "^1.2.1"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/js-yaml": "^4.0.9",
|
|
19
|
+
"@types/node": "^22.0.0",
|
|
20
|
+
"typescript": "^5.7.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EasyOref — Centralized Configuration
|
|
3
|
+
*
|
|
4
|
+
* Primary: config.yaml (searched in cwd, /app, /etc/easyoref)
|
|
5
|
+
* Fallback: environment variables + Docker secrets (for backward compat)
|
|
6
|
+
*
|
|
7
|
+
* See config.yaml.example for all available options.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import yaml from "js-yaml";
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join, resolve } from "node:path";
|
|
14
|
+
import { isValidLanguage, type Language } from "./i18n.js";
|
|
15
|
+
import { type AlertTypeConfig, type GifMode } from "./schemas.js";
|
|
16
|
+
|
|
17
|
+
// ── Types ────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const VALID_GIF_MODES: GifMode[] = ["funny_cats", "none"];
|
|
20
|
+
|
|
21
|
+
const ALL_ALERT_TYPES: AlertTypeConfig[] = ["early", "red_alert", "resolved"];
|
|
22
|
+
|
|
23
|
+
/** Raw YAML schema */
|
|
24
|
+
interface ConfigYaml {
|
|
25
|
+
alert_types?: AlertTypeConfig[];
|
|
26
|
+
city_ids?: number[];
|
|
27
|
+
language?: string;
|
|
28
|
+
gif_mode?: string;
|
|
29
|
+
emoji_override?: Partial<Record<AlertTypeConfig, string>>;
|
|
30
|
+
title_override?: Partial<Record<AlertTypeConfig, string>>;
|
|
31
|
+
description_override?: Partial<Record<AlertTypeConfig, string>>;
|
|
32
|
+
observability?: {
|
|
33
|
+
betterstack_token?: string;
|
|
34
|
+
};
|
|
35
|
+
telegram?: {
|
|
36
|
+
bot_token?: string;
|
|
37
|
+
chat_id?: string | string[];
|
|
38
|
+
};
|
|
39
|
+
health_port?: number;
|
|
40
|
+
poll_interval_ms?: number;
|
|
41
|
+
data_dir?: string;
|
|
42
|
+
oref_api_url?: string;
|
|
43
|
+
oref_history_url?: string;
|
|
44
|
+
ai?: ConfigYamlAi;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ConfigYamlAi {
|
|
48
|
+
enabled?: boolean;
|
|
49
|
+
openrouter_api_key?: string;
|
|
50
|
+
openrouter_filter_model?: string;
|
|
51
|
+
openrouter_extract_model?: string;
|
|
52
|
+
redis_url?: string;
|
|
53
|
+
socks5_proxy?: string;
|
|
54
|
+
enrich_delay_ms?: number;
|
|
55
|
+
confidence_threshold?: number;
|
|
56
|
+
window_minutes?: number;
|
|
57
|
+
timeout_minutes?: number;
|
|
58
|
+
/** Enable MCP tool calling for low-confidence clarification */
|
|
59
|
+
mcp_tools?: boolean;
|
|
60
|
+
/** Number of recent posts to fetch per channel during clarify (1-4) */
|
|
61
|
+
clarify_fetch_count?: number;
|
|
62
|
+
mtproto?: {
|
|
63
|
+
api_id?: number;
|
|
64
|
+
api_hash?: string;
|
|
65
|
+
session_string?: string;
|
|
66
|
+
};
|
|
67
|
+
channels?: string[];
|
|
68
|
+
/** Map monitored area prefix → human-readable region label */
|
|
69
|
+
area_labels?: Record<string, string>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── YAML Loader ──────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/** Config dir in user home — ~/.easyoref/ */
|
|
75
|
+
export const CONFIG_DIR = join(homedir(), ".easyoref");
|
|
76
|
+
export const HOME_CONFIG_PATH = join(CONFIG_DIR, "config.yaml");
|
|
77
|
+
|
|
78
|
+
const CONFIG_SEARCH_PATHS = [
|
|
79
|
+
HOME_CONFIG_PATH,
|
|
80
|
+
"config.yaml",
|
|
81
|
+
"config.yml",
|
|
82
|
+
"/app/config.yaml",
|
|
83
|
+
"/etc/easyoref/config.yaml",
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
function findConfigFile(): string | null {
|
|
87
|
+
const envPath = process.env.EASYOREF_CONFIG;
|
|
88
|
+
if (envPath && existsSync(envPath)) return envPath;
|
|
89
|
+
for (const p of CONFIG_SEARCH_PATHS) {
|
|
90
|
+
const abs = resolve(p);
|
|
91
|
+
if (existsSync(abs)) return abs;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function loadYaml(): ConfigYaml {
|
|
97
|
+
const path = findConfigFile();
|
|
98
|
+
if (path) {
|
|
99
|
+
try {
|
|
100
|
+
const raw = readFileSync(path, "utf-8");
|
|
101
|
+
const parsed = yaml.load(raw) as ConfigYaml;
|
|
102
|
+
// eslint-disable-next-line no-console
|
|
103
|
+
console.log(`[config] Loaded from ${path}`);
|
|
104
|
+
return parsed ?? {};
|
|
105
|
+
} catch (err) {
|
|
106
|
+
// eslint-disable-next-line no-console
|
|
107
|
+
console.error(`[config] Failed to parse ${path}:`, err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Helpers ──────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
function readSecret(envKey: string, secretPaths: string[]): string {
|
|
116
|
+
for (const p of secretPaths) {
|
|
117
|
+
if (existsSync(p)) return readFileSync(p, "utf-8").trim();
|
|
118
|
+
}
|
|
119
|
+
return process.env[envKey] ?? "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseGifMode(raw: string): GifMode {
|
|
123
|
+
const lower = raw.toLowerCase() as GifMode;
|
|
124
|
+
return VALID_GIF_MODES.includes(lower) ? lower : "none";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseAlertTypes(raw?: AlertTypeConfig[]): AlertTypeConfig[] {
|
|
128
|
+
if (!raw || !Array.isArray(raw)) return ALL_ALERT_TYPES;
|
|
129
|
+
return raw.filter((t) => ALL_ALERT_TYPES.includes(t));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Build Config ─────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
const yml = loadYaml();
|
|
135
|
+
|
|
136
|
+
const parsedChatIds: string[] = (() => {
|
|
137
|
+
const raw = yml.telegram?.chat_id ?? process.env.CHAT_ID ?? "";
|
|
138
|
+
if (Array.isArray(raw)) return raw.map(String).filter(Boolean);
|
|
139
|
+
return String(raw)
|
|
140
|
+
.split(",")
|
|
141
|
+
.map((s) => s.trim())
|
|
142
|
+
.filter(Boolean);
|
|
143
|
+
})();
|
|
144
|
+
|
|
145
|
+
export const config = {
|
|
146
|
+
/** Telegram bot token */
|
|
147
|
+
botToken:
|
|
148
|
+
yml.telegram?.bot_token ??
|
|
149
|
+
readSecret("BOT_TOKEN", ["/run/secrets/bot_token", "secrets/bot_token"]),
|
|
150
|
+
|
|
151
|
+
/** All Telegram chat IDs to broadcast to */
|
|
152
|
+
chatIds: parsedChatIds,
|
|
153
|
+
|
|
154
|
+
/** Primary Telegram chat ID (first in the list) */
|
|
155
|
+
chatId: parsedChatIds[0] ?? "",
|
|
156
|
+
|
|
157
|
+
/** City IDs to monitor (resolved to Hebrew names at startup via cities.json) */
|
|
158
|
+
cityIds: yml.city_ids ?? [],
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Hebrew area names — legacy fallback for AREAS env var.
|
|
162
|
+
* Populated at startup from cityIds OR from AREAS env if no cityIds.
|
|
163
|
+
*/
|
|
164
|
+
areas: [] as string[],
|
|
165
|
+
|
|
166
|
+
/** Which alert types to send */
|
|
167
|
+
alertTypes: parseAlertTypes(yml.alert_types),
|
|
168
|
+
|
|
169
|
+
/** Message language */
|
|
170
|
+
language: ((): Language => {
|
|
171
|
+
const raw = (yml.language ?? process.env.LANGUAGE ?? "ru").toLowerCase();
|
|
172
|
+
return isValidLanguage(raw) ? raw : "ru";
|
|
173
|
+
})(),
|
|
174
|
+
|
|
175
|
+
/** Emoji overrides per alert type */
|
|
176
|
+
emojiOverride: yml.emoji_override ?? {},
|
|
177
|
+
|
|
178
|
+
/** Title overrides per alert type */
|
|
179
|
+
titleOverride: yml.title_override ?? {},
|
|
180
|
+
|
|
181
|
+
/** Description overrides per alert type */
|
|
182
|
+
descriptionOverride: yml.description_override ?? {},
|
|
183
|
+
|
|
184
|
+
/** Oref API polling interval (ms) */
|
|
185
|
+
pollIntervalMs:
|
|
186
|
+
yml.poll_interval_ms ?? Number(process.env.OREF_POLL_INTERVAL_MS ?? "2000"),
|
|
187
|
+
|
|
188
|
+
/** Health endpoint port */
|
|
189
|
+
healthPort: yml.health_port ?? Number(process.env.HEALTH_PORT ?? "3100"),
|
|
190
|
+
|
|
191
|
+
/** Oref API URL */
|
|
192
|
+
orefApiUrl:
|
|
193
|
+
yml.oref_api_url ??
|
|
194
|
+
process.env.OREF_API_URL ??
|
|
195
|
+
"https://www.oref.org.il/WarningMessages/alert/alerts.json",
|
|
196
|
+
|
|
197
|
+
/** Oref alert history URL (base, without date params) */
|
|
198
|
+
orefHistoryUrl: yml.oref_history_url ?? process.env.OREF_HISTORY_URL ?? "",
|
|
199
|
+
|
|
200
|
+
/** Better Stack Logtail token */
|
|
201
|
+
logtailToken:
|
|
202
|
+
yml.observability?.betterstack_token ?? process.env.LOGTAIL_TOKEN ?? "",
|
|
203
|
+
|
|
204
|
+
/** GIF mode */
|
|
205
|
+
gifMode: parseGifMode(yml.gif_mode ?? process.env.GIF_MODE ?? "none"),
|
|
206
|
+
|
|
207
|
+
/** Path for persistent data */
|
|
208
|
+
dataDir: yml.data_dir ?? process.env.DATA_DIR ?? join(CONFIG_DIR, "data"),
|
|
209
|
+
|
|
210
|
+
/** AI enrichment config (YAML key: `ai`) */
|
|
211
|
+
agent: (() => {
|
|
212
|
+
const ai = yml.ai;
|
|
213
|
+
return {
|
|
214
|
+
enabled: ai?.enabled ?? false,
|
|
215
|
+
apiKey: ai?.openrouter_api_key ?? process.env.OPENROUTER_API_KEY ?? "",
|
|
216
|
+
filterModel:
|
|
217
|
+
ai?.openrouter_filter_model ?? "google/gemini-2.5-flash-lite",
|
|
218
|
+
extractModel:
|
|
219
|
+
ai?.openrouter_extract_model ?? "google/gemini-3.1-flash-lite-preview",
|
|
220
|
+
redisUrl:
|
|
221
|
+
ai?.redis_url ?? process.env.REDIS_URL ?? "redis://localhost:6379",
|
|
222
|
+
socks5Proxy: ai?.socks5_proxy ?? process.env.SOCKS5_PROXY ?? "",
|
|
223
|
+
enrichDelayMs: ai?.enrich_delay_ms ?? 20_000,
|
|
224
|
+
confidenceThreshold: ai?.confidence_threshold ?? 0.7,
|
|
225
|
+
windowMinutes: ai?.window_minutes ?? 2,
|
|
226
|
+
timeoutMinutes: ai?.timeout_minutes ?? 15,
|
|
227
|
+
mtproto: {
|
|
228
|
+
apiId: ai?.mtproto?.api_id ?? Number(process.env.TG_API_ID ?? "0"),
|
|
229
|
+
apiHash: ai?.mtproto?.api_hash ?? process.env.TG_API_HASH ?? "",
|
|
230
|
+
sessionString:
|
|
231
|
+
ai?.mtproto?.session_string ?? process.env.TG_SESSION ?? "",
|
|
232
|
+
},
|
|
233
|
+
channels: ai?.channels ?? [],
|
|
234
|
+
areaLabels: ai?.area_labels ?? {},
|
|
235
|
+
/** Enable MCP tool calling — deterministic fan-out on low confidence */
|
|
236
|
+
mcpTools: ai?.mcp_tools ?? false,
|
|
237
|
+
/** Posts per channel to fetch during clarify (1-4, default 3) */
|
|
238
|
+
clarifyFetchCount: Math.min(4, Math.max(1, ai?.clarify_fetch_count ?? 3)),
|
|
239
|
+
};
|
|
240
|
+
})(),
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/** Exported for testing */
|
|
244
|
+
export {
|
|
245
|
+
loadYaml as _loadYaml,
|
|
246
|
+
parseAlertTypes as _parseAlertTypes,
|
|
247
|
+
type ConfigYaml,
|
|
248
|
+
};
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Shared utility functions for the agent subsystem. */
|
|
2
|
+
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
/** Format timestamp as HH:MM Israel time */
|
|
6
|
+
export function toIsraelTime(ts: number): string {
|
|
7
|
+
return new Date(ts).toLocaleTimeString("he-IL", {
|
|
8
|
+
hour: "2-digit",
|
|
9
|
+
minute: "2-digit",
|
|
10
|
+
timeZone: "Asia/Jerusalem",
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** MD5 hash for dedup */
|
|
15
|
+
export function textHash(text: string): string {
|
|
16
|
+
return createHash("md5").update(text).digest("hex");
|
|
17
|
+
}
|