@goodandready/dsh-cron 0.2.2 → 0.2.4
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 +99 -14
- package/docs/README.ru.md +96 -15
- package/docs/README.zh.md +99 -14
- package/docs/design/DESIGN.md +13 -7
- package/lib/channels.js +422 -0
- package/lib/client.js +697 -26
- package/lib/index.js +278 -19
- package/lib/integrations.js +3 -1
- package/lib/runner.js +214 -29
- package/lib/runtimes.js +174 -0
- package/lib/scheduler.js +179 -73
- package/lib/secrets.js +123 -0
- package/lib/store.js +92 -21
- package/lib/telegram.js +20 -4
- package/lib/templates.js +88 -0
- package/package.json +1 -1
package/lib/telegram.js
CHANGED
|
@@ -37,13 +37,27 @@ export function extractFirstAllowedChatId(text) {
|
|
|
37
37
|
return undefined;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Harness home for this process. DSH_HOME wins so an isolated profile reads
|
|
42
|
+
* its own settings file instead of another home's (same rule as the store).
|
|
43
|
+
*/
|
|
44
|
+
export function getDshHomeDir() {
|
|
45
|
+
const fromEnv = process.env.DSH_HOME;
|
|
46
|
+
if (fromEnv && String(fromEnv).trim()) return String(fromEnv).trim();
|
|
47
|
+
return path.join(process.env.HOME || os.homedir(), '.dsh');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getDshSettingsPath() {
|
|
51
|
+
return path.join(getDshHomeDir(), 'settings.yaml');
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
/**
|
|
41
55
|
* Read system DSH settings for fallback credentials.
|
|
42
56
|
* Fails soft with a logged warning so silent misconfiguration is visible (#93).
|
|
43
57
|
*/
|
|
44
58
|
export function getDshDefaultTelegramCredentials() {
|
|
45
59
|
try {
|
|
46
|
-
const p =
|
|
60
|
+
const p = getDshSettingsPath();
|
|
47
61
|
if (fs.existsSync(p)) {
|
|
48
62
|
let raw;
|
|
49
63
|
try {
|
|
@@ -105,7 +119,7 @@ export function formatTaskTelegramMessage(task, runInfo) {
|
|
|
105
119
|
/**
|
|
106
120
|
* Send telegram message via Bot API.
|
|
107
121
|
*/
|
|
108
|
-
export async function sendTelegramMessage({ botToken, chatId, text, parseMode = 'Markdown' }) {
|
|
122
|
+
export async function sendTelegramMessage({ botToken, chatId, text, parseMode = 'Markdown', fetchFn = globalThis.fetch, signal }) {
|
|
109
123
|
if (!botToken) throw new Error('Telegram botToken is not configured');
|
|
110
124
|
if (!chatId) throw new Error('Telegram chatId is not configured');
|
|
111
125
|
|
|
@@ -117,20 +131,22 @@ export async function sendTelegramMessage({ botToken, chatId, text, parseMode =
|
|
|
117
131
|
};
|
|
118
132
|
if (parseMode) body.parse_mode = parseMode;
|
|
119
133
|
|
|
120
|
-
const res = await
|
|
134
|
+
const res = await fetchFn(url, {
|
|
121
135
|
method: 'POST',
|
|
122
136
|
headers: { 'Content-Type': 'application/json' },
|
|
123
137
|
body: JSON.stringify(body),
|
|
138
|
+
signal,
|
|
124
139
|
});
|
|
125
140
|
|
|
126
141
|
const data = await res.json().catch(() => ({}));
|
|
127
142
|
if (!res.ok || !data.ok) {
|
|
128
143
|
if (parseMode && data.description && data.description.includes("can't parse entities")) {
|
|
129
144
|
delete body.parse_mode;
|
|
130
|
-
const retryRes = await
|
|
145
|
+
const retryRes = await fetchFn(url, {
|
|
131
146
|
method: 'POST',
|
|
132
147
|
headers: { 'Content-Type': 'application/json' },
|
|
133
148
|
body: JSON.stringify(body),
|
|
149
|
+
signal,
|
|
134
150
|
});
|
|
135
151
|
const retryData = await retryRes.json().catch(() => ({}));
|
|
136
152
|
if (retryRes.ok && retryData.ok) {
|
package/lib/templates.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification template engine (#25).
|
|
3
|
+
*
|
|
4
|
+
* Messages for every channel are rendered from a template with `{var}`
|
|
5
|
+
* placeholders. Pure functions only — no I/O — so the whole matrix is
|
|
6
|
+
* unit-testable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const TEMPLATE_VARIABLES = [
|
|
10
|
+
'title',
|
|
11
|
+
'id',
|
|
12
|
+
'status',
|
|
13
|
+
'output',
|
|
14
|
+
'error',
|
|
15
|
+
'duration',
|
|
16
|
+
'schedule',
|
|
17
|
+
'time',
|
|
18
|
+
'tokens',
|
|
19
|
+
'cost',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_PLAIN_TEMPLATE = '⏰ {title}\nStatus: {status}\nSchedule: {schedule}\nDuration: {duration}\n{output}';
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_FAILURE_TEMPLATE = '❌ {title} failed\nSchedule: {schedule}\nDuration: {duration}\n{error}';
|
|
25
|
+
|
|
26
|
+
const MAX_FIELD = 2000;
|
|
27
|
+
|
|
28
|
+
export function truncateText(text, limit = MAX_FIELD) {
|
|
29
|
+
const s = String(text == null ? '' : text);
|
|
30
|
+
if (s.length <= limit) return s;
|
|
31
|
+
return s.slice(0, limit) + '…';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatDuration(ms) {
|
|
35
|
+
if (ms === null || ms === undefined || ms === '') return '—';
|
|
36
|
+
const n = Number(ms);
|
|
37
|
+
if (!Number.isFinite(n) || n < 0) return '—';
|
|
38
|
+
if (n < 1000) return `${Math.round(n)} ms`;
|
|
39
|
+
const seconds = n / 1000;
|
|
40
|
+
if (seconds < 60) return `${seconds.toFixed(1)} s`;
|
|
41
|
+
const minutes = Math.floor(seconds / 60);
|
|
42
|
+
return `${minutes}m ${Math.round(seconds % 60)}s`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Build the placeholder map for one run. */
|
|
46
|
+
export function buildTemplateVars(task, runInfo = {}) {
|
|
47
|
+
const status = runInfo.status || 'unknown';
|
|
48
|
+
const output = status === 'success' ? (runInfo.output || '') : '';
|
|
49
|
+
const error = runInfo.error || (status === 'error' || status === 'timeout' ? runInfo.output || '' : '');
|
|
50
|
+
const usage = runInfo.usage || {};
|
|
51
|
+
const tokens = (usage.inputTokens || 0) + (usage.outputTokens || 0) + (usage.cacheReadTokens || 0);
|
|
52
|
+
return {
|
|
53
|
+
title: task.title || 'Task',
|
|
54
|
+
id: task.id || '',
|
|
55
|
+
status,
|
|
56
|
+
output: truncateText(output),
|
|
57
|
+
error: truncateText(error),
|
|
58
|
+
duration: formatDuration(runInfo.durationMs),
|
|
59
|
+
schedule: task.scheduleText || task.schedule || '',
|
|
60
|
+
time: new Date(runInfo.at || Date.now()).toISOString(),
|
|
61
|
+
tokens: String(tokens),
|
|
62
|
+
cost: `$${Number(runInfo.costUsd || 0).toFixed(4)}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Replace {var} placeholders; unknown placeholders are left untouched. */
|
|
67
|
+
export function renderTemplate(template, vars) {
|
|
68
|
+
return String(template == null ? '' : template).replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (match, name) => {
|
|
69
|
+
if (Object.prototype.hasOwnProperty.call(vars, name)) {
|
|
70
|
+
const value = vars[name];
|
|
71
|
+
return value === null || value === undefined ? '' : String(value);
|
|
72
|
+
}
|
|
73
|
+
return match;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Pick the template for a run: failure template for failed runs unless a
|
|
79
|
+
* custom template is configured, plain template otherwise.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveTemplateText({ template, task, runInfo }) {
|
|
82
|
+
const custom = typeof template === 'string' && template.trim() ? template.trim() : '';
|
|
83
|
+
if (custom) return renderTemplate(custom, buildTemplateVars(task, runInfo));
|
|
84
|
+
const failed = runInfo.status === 'error' || runInfo.status === 'timeout';
|
|
85
|
+
const base = failed ? DEFAULT_FAILURE_TEMPLATE : DEFAULT_PLAIN_TEMPLATE;
|
|
86
|
+
const text = renderTemplate(base, buildTemplateVars(task, runInfo));
|
|
87
|
+
return text.replace(/\n{3,}/g, '\n\n').trim();
|
|
88
|
+
}
|