meodp 0.0.9 → 0.2.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 +230 -21
- package/bin/index.mjs +10 -2
- package/dist/check/cli.cjs +257 -0
- package/dist/check/cli.d.cts +5 -0
- package/dist/check/cli.d.mts +5 -0
- package/dist/check/cli.d.ts +5 -0
- package/dist/check/cli.mjs +249 -0
- package/dist/check/index.cjs +29 -0
- package/dist/check/index.d.cts +44 -0
- package/dist/check/index.d.mts +44 -0
- package/dist/check/index.d.ts +44 -0
- package/dist/check/index.mjs +13 -0
- package/dist/cli/index.cjs +2 -2
- package/dist/cli/index.d.cts +1 -1
- package/dist/cli/index.d.mts +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.mjs +2 -2
- package/dist/cli/main.cjs +272 -0
- package/dist/cli/main.d.cts +4 -0
- package/dist/cli/main.d.mts +4 -0
- package/dist/cli/main.d.ts +4 -0
- package/dist/cli/main.mjs +266 -0
- package/dist/config/index.cjs +7 -0
- package/dist/config/index.d.cts +47 -0
- package/dist/config/index.d.mts +47 -0
- package/dist/config/index.d.ts +47 -0
- package/dist/config/index.mjs +5 -0
- package/dist/notify/cli.cjs +107 -0
- package/dist/notify/cli.d.cts +3 -0
- package/dist/notify/cli.d.mts +3 -0
- package/dist/notify/cli.d.ts +3 -0
- package/dist/notify/cli.mjs +101 -0
- package/dist/notify/email.cjs +38 -0
- package/dist/notify/email.d.cts +29 -0
- package/dist/notify/email.d.mts +29 -0
- package/dist/notify/email.d.ts +29 -0
- package/dist/notify/email.mjs +35 -0
- package/dist/notify/feishu.cjs +162 -0
- package/dist/notify/feishu.d.cts +74 -0
- package/dist/notify/feishu.d.mts +74 -0
- package/dist/notify/feishu.d.ts +74 -0
- package/dist/notify/feishu.mjs +157 -0
- package/dist/notify/index.cjs +64 -0
- package/dist/notify/index.d.cts +27 -0
- package/dist/notify/index.d.mts +27 -0
- package/dist/notify/index.d.ts +27 -0
- package/dist/notify/index.mjs +62 -0
- package/dist/shared/meodp.2b00b0ae.cjs +228 -0
- package/dist/shared/meodp.4f1da070.d.cts +41 -0
- package/dist/shared/meodp.63483e20.cjs +30 -0
- package/dist/shared/meodp.80ff918d.mjs +465 -0
- package/dist/shared/meodp.8369c4c1.d.ts +41 -0
- package/dist/shared/meodp.93e06e00.d.cts +64 -0
- package/dist/shared/meodp.93e06e00.d.mts +64 -0
- package/dist/shared/meodp.93e06e00.d.ts +64 -0
- package/dist/shared/meodp.9c706389.mjs +220 -0
- package/dist/shared/meodp.a704f8db.d.mts +41 -0
- package/dist/shared/meodp.c4c70565.cjs +477 -0
- package/dist/shared/meodp.f96d0510.mjs +24 -0
- package/package.json +104 -31
- package/bin/index.ts +0 -6
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { LinkNotification } from './index.cjs';
|
|
2
|
+
import '../shared/meodp.93e06e00.cjs';
|
|
3
|
+
|
|
4
|
+
interface FeishuOptions {
|
|
5
|
+
transport?: string;
|
|
6
|
+
appId?: string;
|
|
7
|
+
appSecret?: string;
|
|
8
|
+
receiveId?: string;
|
|
9
|
+
receiveIdType?: string;
|
|
10
|
+
webhook?: string;
|
|
11
|
+
keyword?: string;
|
|
12
|
+
secret?: string;
|
|
13
|
+
timestamp?: number;
|
|
14
|
+
reportUrl?: string;
|
|
15
|
+
runUrl?: string;
|
|
16
|
+
reportLabel?: string;
|
|
17
|
+
timeZone?: string;
|
|
18
|
+
maxItems?: number;
|
|
19
|
+
}
|
|
20
|
+
type FeishuFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
21
|
+
interface PlainText {
|
|
22
|
+
tag: 'plain_text';
|
|
23
|
+
content: string;
|
|
24
|
+
}
|
|
25
|
+
interface CardButton {
|
|
26
|
+
tag: 'button';
|
|
27
|
+
text: PlainText;
|
|
28
|
+
type: 'default' | 'primary';
|
|
29
|
+
url: string;
|
|
30
|
+
}
|
|
31
|
+
type CardElement = {
|
|
32
|
+
tag: 'div';
|
|
33
|
+
text: PlainText;
|
|
34
|
+
} | {
|
|
35
|
+
tag: 'div';
|
|
36
|
+
fields: {
|
|
37
|
+
is_short: boolean;
|
|
38
|
+
text: PlainText;
|
|
39
|
+
}[];
|
|
40
|
+
} | {
|
|
41
|
+
tag: 'note';
|
|
42
|
+
elements: PlainText[];
|
|
43
|
+
} | {
|
|
44
|
+
tag: 'hr';
|
|
45
|
+
} | {
|
|
46
|
+
tag: 'markdown';
|
|
47
|
+
content: string;
|
|
48
|
+
} | {
|
|
49
|
+
tag: 'action';
|
|
50
|
+
actions: CardButton[];
|
|
51
|
+
};
|
|
52
|
+
declare function signFeishu(timestamp: string | number, secret: string): string;
|
|
53
|
+
declare function createFeishuCard(message: LinkNotification, { keyword, reportUrl, runUrl, reportLabel, timeZone, maxItems }?: FeishuOptions): {
|
|
54
|
+
config: {
|
|
55
|
+
wide_screen_mode: boolean;
|
|
56
|
+
enable_forward: boolean;
|
|
57
|
+
};
|
|
58
|
+
header: {
|
|
59
|
+
template: string;
|
|
60
|
+
title: PlainText;
|
|
61
|
+
};
|
|
62
|
+
elements: CardElement[];
|
|
63
|
+
};
|
|
64
|
+
declare function createFeishuPayload(message: LinkNotification, options?: FeishuOptions): {
|
|
65
|
+
msg_type: "interactive";
|
|
66
|
+
card: ReturnType<typeof createFeishuCard>;
|
|
67
|
+
timestamp?: string;
|
|
68
|
+
sign?: string;
|
|
69
|
+
};
|
|
70
|
+
declare function sendFeishuNotification(message: LinkNotification, options: FeishuOptions, fetcher?: FeishuFetch): Promise<{
|
|
71
|
+
messageId: string;
|
|
72
|
+
} | undefined>;
|
|
73
|
+
|
|
74
|
+
export { type FeishuFetch, type FeishuOptions, createFeishuCard, createFeishuPayload, sendFeishuNotification, signFeishu };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { LinkNotification } from './index.mjs';
|
|
2
|
+
import '../shared/meodp.93e06e00.mjs';
|
|
3
|
+
|
|
4
|
+
interface FeishuOptions {
|
|
5
|
+
transport?: string;
|
|
6
|
+
appId?: string;
|
|
7
|
+
appSecret?: string;
|
|
8
|
+
receiveId?: string;
|
|
9
|
+
receiveIdType?: string;
|
|
10
|
+
webhook?: string;
|
|
11
|
+
keyword?: string;
|
|
12
|
+
secret?: string;
|
|
13
|
+
timestamp?: number;
|
|
14
|
+
reportUrl?: string;
|
|
15
|
+
runUrl?: string;
|
|
16
|
+
reportLabel?: string;
|
|
17
|
+
timeZone?: string;
|
|
18
|
+
maxItems?: number;
|
|
19
|
+
}
|
|
20
|
+
type FeishuFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
21
|
+
interface PlainText {
|
|
22
|
+
tag: 'plain_text';
|
|
23
|
+
content: string;
|
|
24
|
+
}
|
|
25
|
+
interface CardButton {
|
|
26
|
+
tag: 'button';
|
|
27
|
+
text: PlainText;
|
|
28
|
+
type: 'default' | 'primary';
|
|
29
|
+
url: string;
|
|
30
|
+
}
|
|
31
|
+
type CardElement = {
|
|
32
|
+
tag: 'div';
|
|
33
|
+
text: PlainText;
|
|
34
|
+
} | {
|
|
35
|
+
tag: 'div';
|
|
36
|
+
fields: {
|
|
37
|
+
is_short: boolean;
|
|
38
|
+
text: PlainText;
|
|
39
|
+
}[];
|
|
40
|
+
} | {
|
|
41
|
+
tag: 'note';
|
|
42
|
+
elements: PlainText[];
|
|
43
|
+
} | {
|
|
44
|
+
tag: 'hr';
|
|
45
|
+
} | {
|
|
46
|
+
tag: 'markdown';
|
|
47
|
+
content: string;
|
|
48
|
+
} | {
|
|
49
|
+
tag: 'action';
|
|
50
|
+
actions: CardButton[];
|
|
51
|
+
};
|
|
52
|
+
declare function signFeishu(timestamp: string | number, secret: string): string;
|
|
53
|
+
declare function createFeishuCard(message: LinkNotification, { keyword, reportUrl, runUrl, reportLabel, timeZone, maxItems }?: FeishuOptions): {
|
|
54
|
+
config: {
|
|
55
|
+
wide_screen_mode: boolean;
|
|
56
|
+
enable_forward: boolean;
|
|
57
|
+
};
|
|
58
|
+
header: {
|
|
59
|
+
template: string;
|
|
60
|
+
title: PlainText;
|
|
61
|
+
};
|
|
62
|
+
elements: CardElement[];
|
|
63
|
+
};
|
|
64
|
+
declare function createFeishuPayload(message: LinkNotification, options?: FeishuOptions): {
|
|
65
|
+
msg_type: "interactive";
|
|
66
|
+
card: ReturnType<typeof createFeishuCard>;
|
|
67
|
+
timestamp?: string;
|
|
68
|
+
sign?: string;
|
|
69
|
+
};
|
|
70
|
+
declare function sendFeishuNotification(message: LinkNotification, options: FeishuOptions, fetcher?: FeishuFetch): Promise<{
|
|
71
|
+
messageId: string;
|
|
72
|
+
} | undefined>;
|
|
73
|
+
|
|
74
|
+
export { type FeishuFetch, type FeishuOptions, createFeishuCard, createFeishuPayload, sendFeishuNotification, signFeishu };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { LinkNotification } from './index.js';
|
|
2
|
+
import '../shared/meodp.93e06e00.js';
|
|
3
|
+
|
|
4
|
+
interface FeishuOptions {
|
|
5
|
+
transport?: string;
|
|
6
|
+
appId?: string;
|
|
7
|
+
appSecret?: string;
|
|
8
|
+
receiveId?: string;
|
|
9
|
+
receiveIdType?: string;
|
|
10
|
+
webhook?: string;
|
|
11
|
+
keyword?: string;
|
|
12
|
+
secret?: string;
|
|
13
|
+
timestamp?: number;
|
|
14
|
+
reportUrl?: string;
|
|
15
|
+
runUrl?: string;
|
|
16
|
+
reportLabel?: string;
|
|
17
|
+
timeZone?: string;
|
|
18
|
+
maxItems?: number;
|
|
19
|
+
}
|
|
20
|
+
type FeishuFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
21
|
+
interface PlainText {
|
|
22
|
+
tag: 'plain_text';
|
|
23
|
+
content: string;
|
|
24
|
+
}
|
|
25
|
+
interface CardButton {
|
|
26
|
+
tag: 'button';
|
|
27
|
+
text: PlainText;
|
|
28
|
+
type: 'default' | 'primary';
|
|
29
|
+
url: string;
|
|
30
|
+
}
|
|
31
|
+
type CardElement = {
|
|
32
|
+
tag: 'div';
|
|
33
|
+
text: PlainText;
|
|
34
|
+
} | {
|
|
35
|
+
tag: 'div';
|
|
36
|
+
fields: {
|
|
37
|
+
is_short: boolean;
|
|
38
|
+
text: PlainText;
|
|
39
|
+
}[];
|
|
40
|
+
} | {
|
|
41
|
+
tag: 'note';
|
|
42
|
+
elements: PlainText[];
|
|
43
|
+
} | {
|
|
44
|
+
tag: 'hr';
|
|
45
|
+
} | {
|
|
46
|
+
tag: 'markdown';
|
|
47
|
+
content: string;
|
|
48
|
+
} | {
|
|
49
|
+
tag: 'action';
|
|
50
|
+
actions: CardButton[];
|
|
51
|
+
};
|
|
52
|
+
declare function signFeishu(timestamp: string | number, secret: string): string;
|
|
53
|
+
declare function createFeishuCard(message: LinkNotification, { keyword, reportUrl, runUrl, reportLabel, timeZone, maxItems }?: FeishuOptions): {
|
|
54
|
+
config: {
|
|
55
|
+
wide_screen_mode: boolean;
|
|
56
|
+
enable_forward: boolean;
|
|
57
|
+
};
|
|
58
|
+
header: {
|
|
59
|
+
template: string;
|
|
60
|
+
title: PlainText;
|
|
61
|
+
};
|
|
62
|
+
elements: CardElement[];
|
|
63
|
+
};
|
|
64
|
+
declare function createFeishuPayload(message: LinkNotification, options?: FeishuOptions): {
|
|
65
|
+
msg_type: "interactive";
|
|
66
|
+
card: ReturnType<typeof createFeishuCard>;
|
|
67
|
+
timestamp?: string;
|
|
68
|
+
sign?: string;
|
|
69
|
+
};
|
|
70
|
+
declare function sendFeishuNotification(message: LinkNotification, options: FeishuOptions, fetcher?: FeishuFetch): Promise<{
|
|
71
|
+
messageId: string;
|
|
72
|
+
} | undefined>;
|
|
73
|
+
|
|
74
|
+
export { type FeishuFetch, type FeishuOptions, createFeishuCard, createFeishuPayload, sendFeishuNotification, signFeishu };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createHmac, createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
function truncate(text, limit) {
|
|
4
|
+
const characters = Array.from(String(text));
|
|
5
|
+
return characters.length > limit ? `${characters.slice(0, limit).join("")}
|
|
6
|
+
\u2026\u5B8C\u6574\u5185\u5BB9\u8BF7\u67E5\u770B\u62A5\u544A\u3002` : characters.join("");
|
|
7
|
+
}
|
|
8
|
+
function safeLink(url) {
|
|
9
|
+
let parsed;
|
|
10
|
+
try {
|
|
11
|
+
parsed = new URL(url);
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error("Notification links must use HTTP(S) without credentials.");
|
|
14
|
+
}
|
|
15
|
+
if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
16
|
+
throw new Error("Notification links must use HTTP(S) without credentials.");
|
|
17
|
+
return parsed.href;
|
|
18
|
+
}
|
|
19
|
+
function signFeishu(timestamp, secret) {
|
|
20
|
+
return createHmac("sha256", `${timestamp}
|
|
21
|
+
${secret}`).digest("base64");
|
|
22
|
+
}
|
|
23
|
+
function createFeishuCard(message, { keyword = "", reportUrl, runUrl, reportLabel = "\u67E5\u770B\u5B8C\u6574\u62A5\u544A", timeZone = "UTC", maxItems = 6 } = {}) {
|
|
24
|
+
if (!Number.isSafeInteger(maxItems) || maxItems < 1 || maxItems > 6)
|
|
25
|
+
throw new Error("maxItems must be between 1 and 6.");
|
|
26
|
+
const text = (content) => ({ tag: "plain_text", content });
|
|
27
|
+
const elements = [];
|
|
28
|
+
if (message.test)
|
|
29
|
+
elements.push({ tag: "div", text: text(message.text) });
|
|
30
|
+
if (message.report) {
|
|
31
|
+
const { total, reachable, restricted, unavailable } = message.report.summary;
|
|
32
|
+
elements.push({
|
|
33
|
+
tag: "div",
|
|
34
|
+
fields: [["\u{1F310} \u68C0\u6D4B\u7AD9\u70B9", total], ["\u2705 \u53EF\u8BBF\u95EE", reachable], ["\u{1F7E0} \u8BBF\u95EE\u53D7\u9650", restricted], ["\u{1F534} \u672C\u6B21\u4E0D\u53EF\u8BBF\u95EE", unavailable]].map(([label, value]) => ({ is_short: true, text: text(`${label}
|
|
35
|
+
${value}`) }))
|
|
36
|
+
});
|
|
37
|
+
const date = new Date(message.report.completedAt);
|
|
38
|
+
const time = Number.isNaN(date.valueOf()) ? message.report.completedAt : new Intl.DateTimeFormat("zh-CN", {
|
|
39
|
+
timeZone,
|
|
40
|
+
dateStyle: "medium",
|
|
41
|
+
timeStyle: "short",
|
|
42
|
+
hour12: false
|
|
43
|
+
}).format(date);
|
|
44
|
+
elements.push({ tag: "note", elements: [text(`${message.test ? "\u793A\u4F8B\u5FEB\u7167" : "\u68C0\u6D4B\u5B8C\u6210"}\uFF1A${time}\uFF08${timeZone}\uFF09
|
|
45
|
+
\u68C0\u6D4B\u73AF\u5883\uFF1A${truncate(message.report.observer, 80)}`)] });
|
|
46
|
+
if (message.entries?.length) {
|
|
47
|
+
elements.push({ tag: "hr" }, { tag: "markdown", content: "**\u72B6\u6001\u6458\u8981**" });
|
|
48
|
+
for (const { item, reason } of message.entries.slice(0, maxItems)) {
|
|
49
|
+
elements.push({ tag: "div", text: text(`${reason} \xB7 ${truncate(item.name || item.url, 60)}
|
|
50
|
+
${truncate(item.url, 160)}
|
|
51
|
+
HTTP / \u539F\u56E0\uFF1A${truncate(item.httpStatus ?? item.reason ?? "\u2014", 80)} \xB7 \u8FDE\u7EED\u5931\u8D25 ${item.consecutiveFailures} \u6B21`) });
|
|
52
|
+
}
|
|
53
|
+
if (message.entries.length > maxItems)
|
|
54
|
+
elements.push({ tag: "note", elements: [text(`\u53E6\u6709 ${message.entries.length - maxItems} \u9879\uFF0C\u70B9\u51FB\u4E0B\u65B9\u67E5\u770B\u5B8C\u6574\u62A5\u544A\u3002`)] });
|
|
55
|
+
} else {
|
|
56
|
+
elements.push({ tag: "div", text: text("\u672C\u6B21\u6CA1\u6709\u9700\u8981\u5173\u6CE8\u7684\u72B6\u6001\u53D8\u5316\u3002") });
|
|
57
|
+
}
|
|
58
|
+
} else if (!message.test) {
|
|
59
|
+
elements.push({ tag: "div", text: text(truncate(message.text, 2500)) });
|
|
60
|
+
}
|
|
61
|
+
const actions = [];
|
|
62
|
+
for (const [label, url] of [[reportLabel, reportUrl], ["\u67E5\u770B\u672C\u6B21\u8FD0\u884C", runUrl]]) {
|
|
63
|
+
if (url)
|
|
64
|
+
actions.push({ tag: "button", text: text(label), type: actions.length ? "default" : "primary", url: safeLink(url) });
|
|
65
|
+
}
|
|
66
|
+
if (actions.length)
|
|
67
|
+
elements.push({ tag: "action", actions });
|
|
68
|
+
elements.push({ tag: "note", elements: [text("\u8BBF\u95EE\u53D7\u9650\u4E0D\u7B49\u4E8E\u5931\u6548\uFF1B\u8FDE\u7EED\u5931\u8D25\u6B21\u6570\u4E0D\u4EE3\u8868\u671F\u95F4\u6301\u7EED\u5B95\u673A\u3002")] });
|
|
69
|
+
const template = message.test ? "blue" : message.report?.summary?.unavailable ? "orange" : message.report?.summary?.restricted ? "yellow" : "green";
|
|
70
|
+
return {
|
|
71
|
+
config: { wide_screen_mode: true, enable_forward: false },
|
|
72
|
+
header: { template, title: text(`${keyword ? `${truncate(keyword, 64)} \xB7 ` : ""}${truncate(message.subject, 100)}`) },
|
|
73
|
+
elements
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function createFeishuPayload(message, options = {}) {
|
|
77
|
+
const payload = { msg_type: "interactive", card: createFeishuCard(message, options) };
|
|
78
|
+
if (options.secret) {
|
|
79
|
+
payload.timestamp = String(options.timestamp ?? Math.floor(Date.now() / 1e3));
|
|
80
|
+
payload.sign = signFeishu(payload.timestamp, options.secret);
|
|
81
|
+
}
|
|
82
|
+
return payload;
|
|
83
|
+
}
|
|
84
|
+
async function requestFeishu(url, init, fetcher, phase) {
|
|
85
|
+
let response;
|
|
86
|
+
try {
|
|
87
|
+
response = await fetcher(url, { ...init, redirect: "error", signal: AbortSignal.timeout(15e3) });
|
|
88
|
+
} catch {
|
|
89
|
+
throw new Error(`Feishu ${phase} request failed or timed out; check delivery before retrying.`);
|
|
90
|
+
}
|
|
91
|
+
if (!response.ok)
|
|
92
|
+
throw new Error(`Feishu ${phase} returned HTTP ${response.status}.`);
|
|
93
|
+
let result;
|
|
94
|
+
try {
|
|
95
|
+
result = await response.json();
|
|
96
|
+
} catch {
|
|
97
|
+
throw new Error(`Feishu ${phase} returned an invalid response; check delivery before retrying.`);
|
|
98
|
+
}
|
|
99
|
+
if (!result || typeof result !== "object")
|
|
100
|
+
throw new Error(`Feishu ${phase} returned an invalid response; check delivery before retrying.`);
|
|
101
|
+
const code = ("code" in result ? result.code : void 0) ?? ("StatusCode" in result ? result.StatusCode : void 0);
|
|
102
|
+
if (code !== 0)
|
|
103
|
+
throw new Error(`Feishu ${phase} rejected the request (code ${Number.isInteger(code) ? code : "unknown"}). Check the bot configuration and permissions.`);
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
async function sendFeishuNotification(message, options, fetcher = fetch) {
|
|
107
|
+
const transport = options.transport || "webhook";
|
|
108
|
+
if (!["app", "webhook"].includes(transport))
|
|
109
|
+
throw new Error("feishu.transport must be app or webhook.");
|
|
110
|
+
if (transport === "app") {
|
|
111
|
+
for (const [name, value] of [["feishu.appId", options.appId], ["feishu.appSecret", options.appSecret], ["feishu.receiveId", options.receiveId]]) {
|
|
112
|
+
if (!value?.trim())
|
|
113
|
+
throw new Error(`Configure ${name} in the notification configuration.`);
|
|
114
|
+
}
|
|
115
|
+
const receiveIdType = options.receiveIdType || "open_id";
|
|
116
|
+
if (!["open_id", "user_id", "union_id", "email"].includes(receiveIdType))
|
|
117
|
+
throw new Error("feishu.receiveIdType must be open_id, user_id, union_id, or email for direct messages.");
|
|
118
|
+
const card = createFeishuCard(message, options);
|
|
119
|
+
const auth = await requestFeishu("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: { "Content-Type": "application/json" },
|
|
122
|
+
body: JSON.stringify({ app_id: options.appId, app_secret: options.appSecret })
|
|
123
|
+
}, fetcher, "authentication");
|
|
124
|
+
if (!("tenant_access_token" in auth) || typeof auth.tenant_access_token !== "string" || !auth.tenant_access_token)
|
|
125
|
+
throw new Error("Feishu authentication returned no tenant access token.");
|
|
126
|
+
const content = JSON.stringify(card);
|
|
127
|
+
const uuid = createHash("sha256").update(`${options.appId}
|
|
128
|
+
${receiveIdType}
|
|
129
|
+
${options.receiveId}
|
|
130
|
+
${content}`).digest("hex").slice(0, 40);
|
|
131
|
+
const result = await requestFeishu(`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${auth.tenant_access_token}` },
|
|
134
|
+
body: JSON.stringify({ receive_id: options.receiveId, msg_type: "interactive", content, uuid })
|
|
135
|
+
}, fetcher, "direct message");
|
|
136
|
+
if (!("data" in result) || !result.data || typeof result.data !== "object" || !("message_id" in result.data) || typeof result.data.message_id !== "string" || !result.data.message_id) {
|
|
137
|
+
throw new Error("Feishu returned no message ID; check delivery before retrying.");
|
|
138
|
+
}
|
|
139
|
+
return { messageId: result.data.message_id };
|
|
140
|
+
}
|
|
141
|
+
let webhook;
|
|
142
|
+
try {
|
|
143
|
+
webhook = new URL(options.webhook || "");
|
|
144
|
+
} catch {
|
|
145
|
+
throw new Error("Configure feishu.webhook in the notification configuration.");
|
|
146
|
+
}
|
|
147
|
+
if (webhook.protocol !== "https:" || !["open.feishu.cn", "open.larksuite.com"].includes(webhook.hostname) || !/^\/open-apis\/bot\/v2\/hook\/[a-z0-9-]+$/i.test(webhook.pathname) || webhook.username || webhook.password || webhook.search || webhook.hash || webhook.port) {
|
|
148
|
+
throw new Error("feishu.webhook must be an official Feishu/Lark custom bot webhook.");
|
|
149
|
+
}
|
|
150
|
+
await requestFeishu(webhook.href, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: { "Content-Type": "application/json" },
|
|
153
|
+
body: JSON.stringify(createFeishuPayload(message, options))
|
|
154
|
+
}, fetcher, "webhook");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export { createFeishuCard, createFeishuPayload, sendFeishuNotification, signFeishu };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const labels = { reachable: "\u53EF\u8BBF\u95EE", restricted: "\u8BBF\u95EE\u53D7\u9650", unavailable: "\u672C\u6B21\u4E0D\u53EF\u8BBF\u95EE" };
|
|
4
|
+
const singleLine = (value) => String(value).replace(/[\r\n]+/g, " ");
|
|
5
|
+
function createNotification(report, options = {}) {
|
|
6
|
+
const { previousReport: previous, mode = "changes", failureThreshold = 2, observerMismatch = "error", title = "MEODP", reportUrl, runUrl } = options;
|
|
7
|
+
if (mode === "off")
|
|
8
|
+
return void 0;
|
|
9
|
+
if (!Number.isSafeInteger(failureThreshold) || failureThreshold < 1)
|
|
10
|
+
throw new Error("failureThreshold must be a positive integer.");
|
|
11
|
+
if (previous && previous.observer !== report.observer && observerMismatch !== "reset")
|
|
12
|
+
throw new Error("Previous report belongs to another observer; choose observerMismatch: reset to start a new baseline.");
|
|
13
|
+
if (!["changes", "weekly"].includes(mode))
|
|
14
|
+
throw new Error("Notification mode must be off, changes, or weekly.");
|
|
15
|
+
if (!["error", "reset"].includes(observerMismatch))
|
|
16
|
+
throw new Error("observerMismatch must be error or reset.");
|
|
17
|
+
const oldResults = new Map((previous?.observer === report.observer ? previous.results : []).map((item) => [item.url, item]));
|
|
18
|
+
const changes = [];
|
|
19
|
+
for (const item of report.results) {
|
|
20
|
+
const old = oldResults.get(item.url);
|
|
21
|
+
let kind;
|
|
22
|
+
if (item.status === "unavailable" && old?.status !== "unavailable")
|
|
23
|
+
kind = "unavailable";
|
|
24
|
+
else if (item.status === "unavailable" && item.consecutiveFailures >= failureThreshold && old && old.consecutiveFailures < failureThreshold)
|
|
25
|
+
kind = "failure-threshold";
|
|
26
|
+
else if (item.status === "restricted" && old?.status !== "restricted")
|
|
27
|
+
kind = "restricted";
|
|
28
|
+
else if (item.status === "reachable" && old?.status === "unavailable")
|
|
29
|
+
kind = "recovered";
|
|
30
|
+
else if (item.status === "reachable" && old?.status === "restricted")
|
|
31
|
+
kind = "restriction-lifted";
|
|
32
|
+
if (kind) {
|
|
33
|
+
const reasons = { "unavailable": "\u65B0\u589E\u4E0D\u53EF\u8BBF\u95EE", "restricted": "\u65B0\u589E\u8BBF\u95EE\u9650\u5236", "failure-threshold": `\u8FDE\u7EED ${failureThreshold} \u6B21\u68C0\u6D4B\u5931\u8D25`, "recovered": "\u6062\u590D\u8BBF\u95EE", "restriction-lifted": "\u8BBF\u95EE\u9650\u5236\u89E3\u9664" };
|
|
34
|
+
changes.push({ item, kind, reason: reasons[kind] });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (mode === "changes" && !changes.length)
|
|
38
|
+
return void 0;
|
|
39
|
+
const { total, reachable, restricted, unavailable } = report.summary;
|
|
40
|
+
const entries = mode === "changes" ? changes : report.results.filter((item) => item.status !== "reachable").map((item) => ({ item, kind: item.status === "restricted" ? "restricted" : "unavailable", reason: labels[item.status] }));
|
|
41
|
+
const lines = [
|
|
42
|
+
`\u68C0\u6D4B\u65F6\u95F4\uFF1A${report.completedAt}`,
|
|
43
|
+
`\u68C0\u6D4B\u73AF\u5883\uFF1A${singleLine(report.observer)}`,
|
|
44
|
+
`\u5171 ${total} \u4E2A\u7AD9\u70B9\uFF1A${reachable} \u53EF\u8BBF\u95EE\uFF0C${restricted} \u8BBF\u95EE\u53D7\u9650\uFF0C${unavailable} \u672C\u6B21\u4E0D\u53EF\u8BBF\u95EE\u3002`,
|
|
45
|
+
"",
|
|
46
|
+
...entries.map(({ item, reason }) => `- ${reason}\uFF1A${singleLine(item.name || item.url)}
|
|
47
|
+
${item.url}
|
|
48
|
+
HTTP / \u539F\u56E0\uFF1A${item.httpStatus ?? item.reason ?? "\u2014"}\uFF1B\u8FDE\u7EED\u5931\u8D25 ${item.consecutiveFailures} \u6B21`)
|
|
49
|
+
];
|
|
50
|
+
if (mode === "weekly") {
|
|
51
|
+
const recovered = changes.filter(({ kind }) => kind === "recovered" || kind === "restriction-lifted");
|
|
52
|
+
lines.push(...recovered.map(({ item, reason }) => `- ${reason}\uFF1A${singleLine(item.name || item.url)} ${item.url}`));
|
|
53
|
+
entries.push(...recovered);
|
|
54
|
+
}
|
|
55
|
+
lines.push("", ...reportUrl ? [`\u72B6\u6001\u9875\uFF1A${reportUrl}`] : [], ...runUrl ? [`\u672C\u6B21\u8FD0\u884C\u4E0E\u62A5\u544A\u9644\u4EF6\uFF1A${runUrl}`] : [], "", "\u8BBF\u95EE\u53D7\u9650\u4E0D\u7B49\u4E8E\u5931\u6548\uFF1B\u8FDE\u7EED\u5931\u8D25\u6B21\u6570\u662F\u72EC\u7ACB\u89C2\u6D4B\uFF0C\u4E0D\u4EE3\u8868\u671F\u95F4\u6301\u7EED\u5B95\u673A\u3002");
|
|
56
|
+
return {
|
|
57
|
+
subject: mode === "weekly" ? `[${singleLine(title)}] \u6BCF\u5468\u94FE\u63A5\u68C0\u6D4B\u6458\u8981` : `[${singleLine(title)}] ${changes.length} \u9879\u94FE\u63A5\u72B6\u6001\u53D8\u5316`,
|
|
58
|
+
text: lines.join("\n"),
|
|
59
|
+
report,
|
|
60
|
+
entries
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
exports.createNotification = createNotification;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { b as LinkObservation, C as CheckReport } from '../shared/meodp.93e06e00.cjs';
|
|
2
|
+
|
|
3
|
+
type NotificationKind = 'unavailable' | 'restricted' | 'failure-threshold' | 'recovered' | 'restriction-lifted';
|
|
4
|
+
interface NotificationEntry {
|
|
5
|
+
kind: NotificationKind;
|
|
6
|
+
item: LinkObservation;
|
|
7
|
+
reason: string;
|
|
8
|
+
}
|
|
9
|
+
interface LinkNotification {
|
|
10
|
+
subject: string;
|
|
11
|
+
text: string;
|
|
12
|
+
test?: boolean;
|
|
13
|
+
report?: Pick<CheckReport, 'summary' | 'completedAt' | 'observer'>;
|
|
14
|
+
entries?: NotificationEntry[];
|
|
15
|
+
}
|
|
16
|
+
interface NotificationOptions {
|
|
17
|
+
previousReport?: CheckReport;
|
|
18
|
+
mode?: 'off' | 'changes' | 'weekly';
|
|
19
|
+
failureThreshold?: number;
|
|
20
|
+
observerMismatch?: 'error' | 'reset';
|
|
21
|
+
title?: string;
|
|
22
|
+
reportUrl?: string;
|
|
23
|
+
runUrl?: string;
|
|
24
|
+
}
|
|
25
|
+
declare function createNotification(report: CheckReport, options?: NotificationOptions): LinkNotification | undefined;
|
|
26
|
+
|
|
27
|
+
export { type LinkNotification, type NotificationEntry, type NotificationKind, type NotificationOptions, createNotification };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { b as LinkObservation, C as CheckReport } from '../shared/meodp.93e06e00.mjs';
|
|
2
|
+
|
|
3
|
+
type NotificationKind = 'unavailable' | 'restricted' | 'failure-threshold' | 'recovered' | 'restriction-lifted';
|
|
4
|
+
interface NotificationEntry {
|
|
5
|
+
kind: NotificationKind;
|
|
6
|
+
item: LinkObservation;
|
|
7
|
+
reason: string;
|
|
8
|
+
}
|
|
9
|
+
interface LinkNotification {
|
|
10
|
+
subject: string;
|
|
11
|
+
text: string;
|
|
12
|
+
test?: boolean;
|
|
13
|
+
report?: Pick<CheckReport, 'summary' | 'completedAt' | 'observer'>;
|
|
14
|
+
entries?: NotificationEntry[];
|
|
15
|
+
}
|
|
16
|
+
interface NotificationOptions {
|
|
17
|
+
previousReport?: CheckReport;
|
|
18
|
+
mode?: 'off' | 'changes' | 'weekly';
|
|
19
|
+
failureThreshold?: number;
|
|
20
|
+
observerMismatch?: 'error' | 'reset';
|
|
21
|
+
title?: string;
|
|
22
|
+
reportUrl?: string;
|
|
23
|
+
runUrl?: string;
|
|
24
|
+
}
|
|
25
|
+
declare function createNotification(report: CheckReport, options?: NotificationOptions): LinkNotification | undefined;
|
|
26
|
+
|
|
27
|
+
export { type LinkNotification, type NotificationEntry, type NotificationKind, type NotificationOptions, createNotification };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { b as LinkObservation, C as CheckReport } from '../shared/meodp.93e06e00.js';
|
|
2
|
+
|
|
3
|
+
type NotificationKind = 'unavailable' | 'restricted' | 'failure-threshold' | 'recovered' | 'restriction-lifted';
|
|
4
|
+
interface NotificationEntry {
|
|
5
|
+
kind: NotificationKind;
|
|
6
|
+
item: LinkObservation;
|
|
7
|
+
reason: string;
|
|
8
|
+
}
|
|
9
|
+
interface LinkNotification {
|
|
10
|
+
subject: string;
|
|
11
|
+
text: string;
|
|
12
|
+
test?: boolean;
|
|
13
|
+
report?: Pick<CheckReport, 'summary' | 'completedAt' | 'observer'>;
|
|
14
|
+
entries?: NotificationEntry[];
|
|
15
|
+
}
|
|
16
|
+
interface NotificationOptions {
|
|
17
|
+
previousReport?: CheckReport;
|
|
18
|
+
mode?: 'off' | 'changes' | 'weekly';
|
|
19
|
+
failureThreshold?: number;
|
|
20
|
+
observerMismatch?: 'error' | 'reset';
|
|
21
|
+
title?: string;
|
|
22
|
+
reportUrl?: string;
|
|
23
|
+
runUrl?: string;
|
|
24
|
+
}
|
|
25
|
+
declare function createNotification(report: CheckReport, options?: NotificationOptions): LinkNotification | undefined;
|
|
26
|
+
|
|
27
|
+
export { type LinkNotification, type NotificationEntry, type NotificationKind, type NotificationOptions, createNotification };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const labels = { reachable: "\u53EF\u8BBF\u95EE", restricted: "\u8BBF\u95EE\u53D7\u9650", unavailable: "\u672C\u6B21\u4E0D\u53EF\u8BBF\u95EE" };
|
|
2
|
+
const singleLine = (value) => String(value).replace(/[\r\n]+/g, " ");
|
|
3
|
+
function createNotification(report, options = {}) {
|
|
4
|
+
const { previousReport: previous, mode = "changes", failureThreshold = 2, observerMismatch = "error", title = "MEODP", reportUrl, runUrl } = options;
|
|
5
|
+
if (mode === "off")
|
|
6
|
+
return void 0;
|
|
7
|
+
if (!Number.isSafeInteger(failureThreshold) || failureThreshold < 1)
|
|
8
|
+
throw new Error("failureThreshold must be a positive integer.");
|
|
9
|
+
if (previous && previous.observer !== report.observer && observerMismatch !== "reset")
|
|
10
|
+
throw new Error("Previous report belongs to another observer; choose observerMismatch: reset to start a new baseline.");
|
|
11
|
+
if (!["changes", "weekly"].includes(mode))
|
|
12
|
+
throw new Error("Notification mode must be off, changes, or weekly.");
|
|
13
|
+
if (!["error", "reset"].includes(observerMismatch))
|
|
14
|
+
throw new Error("observerMismatch must be error or reset.");
|
|
15
|
+
const oldResults = new Map((previous?.observer === report.observer ? previous.results : []).map((item) => [item.url, item]));
|
|
16
|
+
const changes = [];
|
|
17
|
+
for (const item of report.results) {
|
|
18
|
+
const old = oldResults.get(item.url);
|
|
19
|
+
let kind;
|
|
20
|
+
if (item.status === "unavailable" && old?.status !== "unavailable")
|
|
21
|
+
kind = "unavailable";
|
|
22
|
+
else if (item.status === "unavailable" && item.consecutiveFailures >= failureThreshold && old && old.consecutiveFailures < failureThreshold)
|
|
23
|
+
kind = "failure-threshold";
|
|
24
|
+
else if (item.status === "restricted" && old?.status !== "restricted")
|
|
25
|
+
kind = "restricted";
|
|
26
|
+
else if (item.status === "reachable" && old?.status === "unavailable")
|
|
27
|
+
kind = "recovered";
|
|
28
|
+
else if (item.status === "reachable" && old?.status === "restricted")
|
|
29
|
+
kind = "restriction-lifted";
|
|
30
|
+
if (kind) {
|
|
31
|
+
const reasons = { "unavailable": "\u65B0\u589E\u4E0D\u53EF\u8BBF\u95EE", "restricted": "\u65B0\u589E\u8BBF\u95EE\u9650\u5236", "failure-threshold": `\u8FDE\u7EED ${failureThreshold} \u6B21\u68C0\u6D4B\u5931\u8D25`, "recovered": "\u6062\u590D\u8BBF\u95EE", "restriction-lifted": "\u8BBF\u95EE\u9650\u5236\u89E3\u9664" };
|
|
32
|
+
changes.push({ item, kind, reason: reasons[kind] });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (mode === "changes" && !changes.length)
|
|
36
|
+
return void 0;
|
|
37
|
+
const { total, reachable, restricted, unavailable } = report.summary;
|
|
38
|
+
const entries = mode === "changes" ? changes : report.results.filter((item) => item.status !== "reachable").map((item) => ({ item, kind: item.status === "restricted" ? "restricted" : "unavailable", reason: labels[item.status] }));
|
|
39
|
+
const lines = [
|
|
40
|
+
`\u68C0\u6D4B\u65F6\u95F4\uFF1A${report.completedAt}`,
|
|
41
|
+
`\u68C0\u6D4B\u73AF\u5883\uFF1A${singleLine(report.observer)}`,
|
|
42
|
+
`\u5171 ${total} \u4E2A\u7AD9\u70B9\uFF1A${reachable} \u53EF\u8BBF\u95EE\uFF0C${restricted} \u8BBF\u95EE\u53D7\u9650\uFF0C${unavailable} \u672C\u6B21\u4E0D\u53EF\u8BBF\u95EE\u3002`,
|
|
43
|
+
"",
|
|
44
|
+
...entries.map(({ item, reason }) => `- ${reason}\uFF1A${singleLine(item.name || item.url)}
|
|
45
|
+
${item.url}
|
|
46
|
+
HTTP / \u539F\u56E0\uFF1A${item.httpStatus ?? item.reason ?? "\u2014"}\uFF1B\u8FDE\u7EED\u5931\u8D25 ${item.consecutiveFailures} \u6B21`)
|
|
47
|
+
];
|
|
48
|
+
if (mode === "weekly") {
|
|
49
|
+
const recovered = changes.filter(({ kind }) => kind === "recovered" || kind === "restriction-lifted");
|
|
50
|
+
lines.push(...recovered.map(({ item, reason }) => `- ${reason}\uFF1A${singleLine(item.name || item.url)} ${item.url}`));
|
|
51
|
+
entries.push(...recovered);
|
|
52
|
+
}
|
|
53
|
+
lines.push("", ...reportUrl ? [`\u72B6\u6001\u9875\uFF1A${reportUrl}`] : [], ...runUrl ? [`\u672C\u6B21\u8FD0\u884C\u4E0E\u62A5\u544A\u9644\u4EF6\uFF1A${runUrl}`] : [], "", "\u8BBF\u95EE\u53D7\u9650\u4E0D\u7B49\u4E8E\u5931\u6548\uFF1B\u8FDE\u7EED\u5931\u8D25\u6B21\u6570\u662F\u72EC\u7ACB\u89C2\u6D4B\uFF0C\u4E0D\u4EE3\u8868\u671F\u95F4\u6301\u7EED\u5B95\u673A\u3002");
|
|
54
|
+
return {
|
|
55
|
+
subject: mode === "weekly" ? `[${singleLine(title)}] \u6BCF\u5468\u94FE\u63A5\u68C0\u6D4B\u6458\u8981` : `[${singleLine(title)}] ${changes.length} \u9879\u94FE\u63A5\u72B6\u6001\u53D8\u5316`,
|
|
56
|
+
text: lines.join("\n"),
|
|
57
|
+
report,
|
|
58
|
+
entries
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { createNotification };
|