pikiclaw 0.2.35
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/LICENSE +21 -0
- package/README.md +315 -0
- package/dist/agent-driver.js +24 -0
- package/dist/bot-command-ui.js +299 -0
- package/dist/bot-commands.js +236 -0
- package/dist/bot-feishu-render.js +527 -0
- package/dist/bot-feishu.js +752 -0
- package/dist/bot-handler.js +115 -0
- package/dist/bot-menu.js +44 -0
- package/dist/bot-streaming.js +165 -0
- package/dist/bot-telegram-directory.js +74 -0
- package/dist/bot-telegram-live-preview.js +192 -0
- package/dist/bot-telegram-render.js +369 -0
- package/dist/bot-telegram.js +789 -0
- package/dist/bot.js +897 -0
- package/dist/channel-base.js +46 -0
- package/dist/channel-feishu.js +873 -0
- package/dist/channel-states.js +3 -0
- package/dist/channel-telegram.js +773 -0
- package/dist/cli-channels.js +24 -0
- package/dist/cli.js +484 -0
- package/dist/code-agent.js +1080 -0
- package/dist/config-validation.js +244 -0
- package/dist/dashboard-ui.js +31 -0
- package/dist/dashboard.js +840 -0
- package/dist/driver-claude.js +520 -0
- package/dist/driver-codex.js +1055 -0
- package/dist/driver-gemini.js +230 -0
- package/dist/mcp-bridge.js +192 -0
- package/dist/mcp-session-server.js +321 -0
- package/dist/onboarding.js +138 -0
- package/dist/process-control.js +259 -0
- package/dist/run.js +275 -0
- package/dist/session-status.js +43 -0
- package/dist/setup-wizard.js +231 -0
- package/dist/user-config.js +195 -0
- package/package.json +60 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { encodeCommandAction } from './bot-command-ui.js';
|
|
2
|
+
import { fmtUptime, formatThinkingForDisplay, thinkLabel } from './bot.js';
|
|
3
|
+
import { formatActivityCommandSummary, parseActivitySummary, renderPlanForPreview, summarizeActivityForPreview } from './bot-streaming.js';
|
|
4
|
+
export function escapeHtml(t) {
|
|
5
|
+
return t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
6
|
+
}
|
|
7
|
+
export function truncateMiddle(text, maxChars = 36) {
|
|
8
|
+
const value = String(text || '');
|
|
9
|
+
if (value.length <= maxChars)
|
|
10
|
+
return value;
|
|
11
|
+
if (maxChars <= 3)
|
|
12
|
+
return value.slice(0, maxChars);
|
|
13
|
+
const visible = maxChars - 3;
|
|
14
|
+
const head = Math.ceil(visible / 2);
|
|
15
|
+
const tail = Math.floor(visible / 2);
|
|
16
|
+
return `${value.slice(0, head)}...${value.slice(-tail)}`;
|
|
17
|
+
}
|
|
18
|
+
export function compactCode(text, maxChars = 36) {
|
|
19
|
+
return `<code>${escapeHtml(truncateMiddle(text, maxChars))}</code>`;
|
|
20
|
+
}
|
|
21
|
+
export function buildCompactSelectionTitle(title, detail) {
|
|
22
|
+
const cleanDetail = String(detail || '').trim();
|
|
23
|
+
if (!cleanDetail)
|
|
24
|
+
return `<b>${escapeHtml(title)}</b>`;
|
|
25
|
+
return `<b>${escapeHtml(title)}</b> · ${compactCode(cleanDetail, 20)}`;
|
|
26
|
+
}
|
|
27
|
+
export function buildCompactSelectionNotice(title, value, detail, codeMaxChars = 40) {
|
|
28
|
+
const lines = [`<b>${escapeHtml(title)}</b>`, compactCode(value, codeMaxChars)];
|
|
29
|
+
const cleanDetail = String(detail || '').trim();
|
|
30
|
+
if (cleanDetail)
|
|
31
|
+
lines.push(`<i>${escapeHtml(cleanDetail)}</i>`);
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
}
|
|
34
|
+
function selectionStateSymbol(state) {
|
|
35
|
+
switch (state) {
|
|
36
|
+
case 'current': return '●';
|
|
37
|
+
case 'running': return '◐';
|
|
38
|
+
case 'unavailable': return '✕';
|
|
39
|
+
default: return '○';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function formatCommandItemHtml(item, index) {
|
|
43
|
+
const parts = [
|
|
44
|
+
selectionStateSymbol(item.state),
|
|
45
|
+
`<b>${index + 1}.</b>`,
|
|
46
|
+
escapeHtml(item.label),
|
|
47
|
+
];
|
|
48
|
+
if (item.detail)
|
|
49
|
+
parts.push(escapeHtml(item.detail));
|
|
50
|
+
return parts.join(' ');
|
|
51
|
+
}
|
|
52
|
+
function formatCommandButtonLabel(button, maxChars = 24) {
|
|
53
|
+
const prefix = button.state && button.state !== 'default'
|
|
54
|
+
? `${selectionStateSymbol(button.state)} `
|
|
55
|
+
: '';
|
|
56
|
+
return truncateMiddle(`${prefix}${button.label}`.trim(), maxChars);
|
|
57
|
+
}
|
|
58
|
+
export function renderCommandNoticeHtml(notice) {
|
|
59
|
+
const lines = [`<b>${escapeHtml(notice.title)}</b>`];
|
|
60
|
+
if (notice.value) {
|
|
61
|
+
if (notice.valueMode === 'plain')
|
|
62
|
+
lines.push(escapeHtml(notice.value));
|
|
63
|
+
else
|
|
64
|
+
lines.push(compactCode(notice.value, 40));
|
|
65
|
+
}
|
|
66
|
+
if (notice.detail)
|
|
67
|
+
lines.push(`<i>${escapeHtml(notice.detail)}</i>`);
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
}
|
|
70
|
+
export function renderCommandSelectionHtml(view) {
|
|
71
|
+
const lines = [buildCompactSelectionTitle(view.title, view.detail)];
|
|
72
|
+
if (view.metaLines.length)
|
|
73
|
+
lines.push(...view.metaLines.map(line => `<i>${escapeHtml(line)}</i>`));
|
|
74
|
+
if (view.items.length) {
|
|
75
|
+
lines.push('', ...view.items.map((item, index) => formatCommandItemHtml(item, index)));
|
|
76
|
+
}
|
|
77
|
+
else if (view.emptyText) {
|
|
78
|
+
lines.push('', `<i>${escapeHtml(view.emptyText)}</i>`);
|
|
79
|
+
}
|
|
80
|
+
if (view.helperText)
|
|
81
|
+
lines.push('', `<i>${escapeHtml(view.helperText)}</i>`);
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
export function renderCommandSelectionKeyboard(view) {
|
|
85
|
+
if (!view.rows.length)
|
|
86
|
+
return undefined;
|
|
87
|
+
return {
|
|
88
|
+
inline_keyboard: view.rows.map(row => row.map(button => ({
|
|
89
|
+
text: formatCommandButtonLabel(button),
|
|
90
|
+
callback_data: encodeCommandAction(button.action),
|
|
91
|
+
}))),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function mdInline(line) {
|
|
95
|
+
const parts = [];
|
|
96
|
+
let rest = line;
|
|
97
|
+
while (rest.includes('`')) {
|
|
98
|
+
const a = rest.indexOf('`');
|
|
99
|
+
const b = rest.indexOf('`', a + 1);
|
|
100
|
+
if (b === -1)
|
|
101
|
+
break;
|
|
102
|
+
parts.push(formatMarkdownSegment(rest.slice(0, a)));
|
|
103
|
+
parts.push(`<code>${escapeHtml(rest.slice(a + 1, b))}</code>`);
|
|
104
|
+
rest = rest.slice(b + 1);
|
|
105
|
+
}
|
|
106
|
+
parts.push(formatMarkdownSegment(rest));
|
|
107
|
+
return parts.join('');
|
|
108
|
+
}
|
|
109
|
+
function formatMarkdownSegment(text) {
|
|
110
|
+
let value = escapeHtml(text);
|
|
111
|
+
value = value.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
|
|
112
|
+
value = value.replace(/__(.+?)__/g, '<b>$1</b>');
|
|
113
|
+
value = value.replace(/(?<!\w)\*([^*]+?)\*(?!\w)/g, '<i>$1</i>');
|
|
114
|
+
value = value.replace(/(?<!\w)_([^_]+?)_(?!\w)/g, '<i>$1</i>');
|
|
115
|
+
value = value.replace(/~~(.+?)~~/g, '<s>$1</s>');
|
|
116
|
+
value = value.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
export function mdToTgHtml(text) {
|
|
120
|
+
const result = [];
|
|
121
|
+
const lines = text.split('\n');
|
|
122
|
+
let i = 0;
|
|
123
|
+
let inCode = false;
|
|
124
|
+
let codeLang = '';
|
|
125
|
+
let codeLines = [];
|
|
126
|
+
while (i < lines.length) {
|
|
127
|
+
const line = lines[i];
|
|
128
|
+
const stripped = line.trim();
|
|
129
|
+
if (stripped.startsWith('```')) {
|
|
130
|
+
if (!inCode) {
|
|
131
|
+
inCode = true;
|
|
132
|
+
codeLang = stripped.slice(3).trim().split(/\s/)[0] || '';
|
|
133
|
+
codeLines = [];
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
inCode = false;
|
|
137
|
+
const content = escapeHtml(codeLines.join('\n'));
|
|
138
|
+
result.push(codeLang
|
|
139
|
+
? `<pre><code class="language-${escapeHtml(codeLang)}">${content}</code></pre>`
|
|
140
|
+
: `<pre>${content}</pre>`);
|
|
141
|
+
}
|
|
142
|
+
i++;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (inCode) {
|
|
146
|
+
codeLines.push(line);
|
|
147
|
+
i++;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (stripped.startsWith('|') && stripped.endsWith('|')) {
|
|
151
|
+
const tableLines = [];
|
|
152
|
+
while (i < lines.length) {
|
|
153
|
+
const tableLine = lines[i].trim();
|
|
154
|
+
if (!tableLine.startsWith('|'))
|
|
155
|
+
break;
|
|
156
|
+
if (/^\|[\s\-:|]+\|$/.test(tableLine)) {
|
|
157
|
+
i++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
tableLines.push(tableLine);
|
|
161
|
+
i++;
|
|
162
|
+
}
|
|
163
|
+
if (tableLines.length)
|
|
164
|
+
result.push(`<pre>${escapeHtml(tableLines.join('\n'))}</pre>`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
168
|
+
if (heading) {
|
|
169
|
+
result.push(`<b>${mdInline(heading[2])}</b>`);
|
|
170
|
+
i++;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
result.push(mdInline(line));
|
|
174
|
+
i++;
|
|
175
|
+
}
|
|
176
|
+
if (inCode && codeLines.length)
|
|
177
|
+
result.push(`<pre>${escapeHtml(codeLines.join('\n'))}</pre>`);
|
|
178
|
+
return result.join('\n');
|
|
179
|
+
}
|
|
180
|
+
export function renderSessionTurnHtml(userText, assistantText) {
|
|
181
|
+
const parts = [];
|
|
182
|
+
const user = String(userText || '').trim();
|
|
183
|
+
const assistant = String(assistantText || '').trim();
|
|
184
|
+
if (user || assistant)
|
|
185
|
+
parts.push('<b>Recent Context</b>');
|
|
186
|
+
if (user)
|
|
187
|
+
parts.push(`<blockquote expandable>${escapeHtml(user)}</blockquote>`);
|
|
188
|
+
if (assistant)
|
|
189
|
+
parts.push(mdToTgHtml(assistant));
|
|
190
|
+
return parts.join('\n\n');
|
|
191
|
+
}
|
|
192
|
+
export function formatMenuLines(commands) {
|
|
193
|
+
return commands.map(cmd => `/${cmd.command} — ${escapeHtml(cmd.description)}`);
|
|
194
|
+
}
|
|
195
|
+
export function renderSkillsListHtml(d) {
|
|
196
|
+
const lines = [
|
|
197
|
+
`<b>Project Skills</b> (${d.skills.length})`,
|
|
198
|
+
'',
|
|
199
|
+
`<b>Agent:</b> ${escapeHtml(d.agent)}`,
|
|
200
|
+
`<b>Workdir:</b> <code>${escapeHtml(d.workdir)}</code>`,
|
|
201
|
+
];
|
|
202
|
+
if (!d.skills.length) {
|
|
203
|
+
lines.push('', '<i>No project skills found in .pikiclaw/skills/ or .claude/commands/.</i>');
|
|
204
|
+
return lines.join('\n');
|
|
205
|
+
}
|
|
206
|
+
lines.push('');
|
|
207
|
+
for (const skill of d.skills) {
|
|
208
|
+
lines.push(`<b>/${escapeHtml(skill.command)}</b> — ${escapeHtml(skill.label)}`);
|
|
209
|
+
if (skill.description)
|
|
210
|
+
lines.push(escapeHtml(skill.description));
|
|
211
|
+
}
|
|
212
|
+
return lines.join('\n');
|
|
213
|
+
}
|
|
214
|
+
function fmtCompactUptime(ms) {
|
|
215
|
+
return fmtUptime(ms).replace(/\s+/g, '');
|
|
216
|
+
}
|
|
217
|
+
function footerStatusSymbol(status) {
|
|
218
|
+
switch (status) {
|
|
219
|
+
case 'running': return '●';
|
|
220
|
+
case 'done': return '✓';
|
|
221
|
+
case 'failed': return '✗';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function formatFooterSummary(agent, elapsedMs, meta, contextPercent) {
|
|
225
|
+
const parts = [agent];
|
|
226
|
+
const ctx = contextPercent ?? meta?.contextPercent ?? null;
|
|
227
|
+
if (ctx != null)
|
|
228
|
+
parts.push(`${ctx}%`);
|
|
229
|
+
parts.push(fmtCompactUptime(Math.max(0, Math.round(elapsedMs))));
|
|
230
|
+
return parts.join(' · ');
|
|
231
|
+
}
|
|
232
|
+
export function formatPreviewFooterHtml(agent, elapsedMs, meta) {
|
|
233
|
+
return escapeHtml(`${footerStatusSymbol('running')} ${formatFooterSummary(agent, elapsedMs, meta)}`);
|
|
234
|
+
}
|
|
235
|
+
function formatFinalFooterHtml(status, agent, elapsedMs, contextPercent) {
|
|
236
|
+
return escapeHtml(`${footerStatusSymbol(status)} ${formatFooterSummary(agent, elapsedMs, null, contextPercent ?? null)}`);
|
|
237
|
+
}
|
|
238
|
+
function rawUsageLine(parts) {
|
|
239
|
+
return parts.filter(part => !!part && String(part).trim()).join(' ');
|
|
240
|
+
}
|
|
241
|
+
export function formatProviderUsageLines(usage) {
|
|
242
|
+
const lines = ['', '<b>Provider Usage</b>'];
|
|
243
|
+
if (!usage.ok) {
|
|
244
|
+
lines.push(` Unavailable: ${escapeHtml(usage.error || 'No recent usage data found.')}`);
|
|
245
|
+
return lines;
|
|
246
|
+
}
|
|
247
|
+
if (usage.capturedAt) {
|
|
248
|
+
const capturedAtMs = Date.parse(usage.capturedAt);
|
|
249
|
+
if (Number.isFinite(capturedAtMs)) {
|
|
250
|
+
lines.push(` Updated: ${fmtUptime(Math.max(0, Date.now() - capturedAtMs))} ago`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!usage.windows.length) {
|
|
254
|
+
lines.push(` ${escapeHtml(usage.status ? `status=${usage.status}` : 'No window data')}`);
|
|
255
|
+
return lines;
|
|
256
|
+
}
|
|
257
|
+
for (const window of usage.windows) {
|
|
258
|
+
const details = rawUsageLine([
|
|
259
|
+
window.usedPercent != null ? `${window.usedPercent}% used` : null,
|
|
260
|
+
window.status ? `status=${window.status}` : null,
|
|
261
|
+
window.resetAfterSeconds != null ? `resetAfterSeconds=${window.resetAfterSeconds}` : null,
|
|
262
|
+
]);
|
|
263
|
+
lines.push(` ${escapeHtml(window.label)}: ${escapeHtml(details || 'No details')}`);
|
|
264
|
+
}
|
|
265
|
+
return lines;
|
|
266
|
+
}
|
|
267
|
+
function trimActivityForPreview(text, maxChars = 900) {
|
|
268
|
+
if (text.length <= maxChars)
|
|
269
|
+
return text;
|
|
270
|
+
const lines = text.split('\n').filter(line => line.trim());
|
|
271
|
+
if (lines.length <= 1)
|
|
272
|
+
return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
|
|
273
|
+
const tailCount = Math.min(2, Math.max(1, lines.length - 1));
|
|
274
|
+
const tail = lines.slice(-tailCount);
|
|
275
|
+
const headCandidates = lines.slice(0, Math.max(0, lines.length - tailCount));
|
|
276
|
+
const reserved = tail.join('\n').length + 5;
|
|
277
|
+
const budget = Math.max(0, maxChars - reserved);
|
|
278
|
+
const head = [];
|
|
279
|
+
let used = 0;
|
|
280
|
+
for (const line of headCandidates) {
|
|
281
|
+
const extra = line.length + (head.length ? 1 : 0);
|
|
282
|
+
if (used + extra > budget)
|
|
283
|
+
break;
|
|
284
|
+
head.push(line);
|
|
285
|
+
used += extra;
|
|
286
|
+
}
|
|
287
|
+
if (!head.length)
|
|
288
|
+
return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
|
|
289
|
+
return [...head, '...', ...tail].join('\n');
|
|
290
|
+
}
|
|
291
|
+
export function buildInitialPreviewHtml(agent) {
|
|
292
|
+
return formatPreviewFooterHtml(agent, 0);
|
|
293
|
+
}
|
|
294
|
+
export function buildStreamPreviewHtml(input) {
|
|
295
|
+
const maxBody = 2400;
|
|
296
|
+
const display = input.bodyText.trim();
|
|
297
|
+
const rawThinking = input.thinking.trim();
|
|
298
|
+
const thinkDisplay = formatThinkingForDisplay(input.thinking, maxBody);
|
|
299
|
+
const planDisplay = renderPlanForPreview(input.plan ?? null);
|
|
300
|
+
const activityDisplay = summarizeActivityForPreview(input.activity);
|
|
301
|
+
const maxActivity = !display && !thinkDisplay && !planDisplay ? 1800 : 900;
|
|
302
|
+
const parts = [];
|
|
303
|
+
const label = thinkLabel(input.agent);
|
|
304
|
+
if (planDisplay) {
|
|
305
|
+
parts.push(`<blockquote><b>Plan</b>\n${escapeHtml(planDisplay)}</blockquote>`);
|
|
306
|
+
}
|
|
307
|
+
if (activityDisplay) {
|
|
308
|
+
parts.push(`<blockquote><b>Activity</b>\n${escapeHtml(trimActivityForPreview(activityDisplay, maxActivity))}</blockquote>`);
|
|
309
|
+
}
|
|
310
|
+
if (thinkDisplay && !display) {
|
|
311
|
+
parts.push(`<blockquote><b>${escapeHtml(label)}</b>\n${escapeHtml(thinkDisplay)}</blockquote>`);
|
|
312
|
+
}
|
|
313
|
+
else if (display) {
|
|
314
|
+
if (rawThinking)
|
|
315
|
+
parts.push(`<i>${escapeHtml(`${label} (${rawThinking.length} chars)`)}</i>`);
|
|
316
|
+
const preview = display.length > maxBody ? '(...truncated)\n' + display.slice(-maxBody) : display;
|
|
317
|
+
parts.push(mdToTgHtml(preview));
|
|
318
|
+
}
|
|
319
|
+
parts.push(formatPreviewFooterHtml(input.agent, input.elapsedMs, input.meta ?? null));
|
|
320
|
+
return parts.join('\n\n');
|
|
321
|
+
}
|
|
322
|
+
export function buildFinalReplyRender(agent, result) {
|
|
323
|
+
const footerStatus = result.incomplete || !result.ok ? 'failed' : 'done';
|
|
324
|
+
const footerHtml = `\n\n${formatFinalFooterHtml(footerStatus, agent, result.elapsedS * 1000, result.contextPercent ?? null)}`;
|
|
325
|
+
let activityHtml = '';
|
|
326
|
+
let activityNoteHtml = '';
|
|
327
|
+
if (result.activity) {
|
|
328
|
+
const summary = parseActivitySummary(result.activity);
|
|
329
|
+
const narrative = summary.narrative.join('\n');
|
|
330
|
+
if (narrative) {
|
|
331
|
+
let display = narrative;
|
|
332
|
+
if (display.length > 800)
|
|
333
|
+
display = '...\n' + display.slice(-800);
|
|
334
|
+
activityHtml = `<blockquote><b>Activity</b>\n${escapeHtml(display)}</blockquote>\n\n`;
|
|
335
|
+
}
|
|
336
|
+
const commandSummary = formatActivityCommandSummary(summary.completedCommands, summary.activeCommands, summary.failedCommands);
|
|
337
|
+
if (commandSummary)
|
|
338
|
+
activityNoteHtml = `<i>${escapeHtml(commandSummary)}</i>\n\n`;
|
|
339
|
+
}
|
|
340
|
+
let thinkingHtml = '';
|
|
341
|
+
if (result.thinking) {
|
|
342
|
+
thinkingHtml = `<blockquote><b>${thinkLabel(agent)}</b>\n${escapeHtml(formatThinkingForDisplay(result.thinking, 800))}</blockquote>\n\n`;
|
|
343
|
+
}
|
|
344
|
+
let statusHtml = '';
|
|
345
|
+
if (result.incomplete) {
|
|
346
|
+
const statusLines = [];
|
|
347
|
+
if (result.stopReason === 'max_tokens')
|
|
348
|
+
statusLines.push('Output limit reached. Response may be truncated.');
|
|
349
|
+
if (result.stopReason === 'timeout') {
|
|
350
|
+
statusLines.push(`Timed out after ${fmtUptime(Math.max(0, Math.round(result.elapsedS * 1000)))} before the agent reported completion.`);
|
|
351
|
+
}
|
|
352
|
+
if (!result.ok) {
|
|
353
|
+
const detail = result.error?.trim();
|
|
354
|
+
if (detail && detail !== result.message.trim() && !statusLines.includes(detail))
|
|
355
|
+
statusLines.push(detail);
|
|
356
|
+
else if (result.stopReason !== 'timeout')
|
|
357
|
+
statusLines.push('Agent exited before reporting completion.');
|
|
358
|
+
}
|
|
359
|
+
statusHtml = `<blockquote expandable><b>Incomplete Response</b>\n${statusLines.map(escapeHtml).join('\n')}</blockquote>\n\n`;
|
|
360
|
+
}
|
|
361
|
+
const headerHtml = `${activityHtml}${activityNoteHtml}${statusHtml}${thinkingHtml}`;
|
|
362
|
+
const bodyHtml = mdToTgHtml(result.message);
|
|
363
|
+
return {
|
|
364
|
+
fullHtml: `${headerHtml}${bodyHtml}${footerHtml}`,
|
|
365
|
+
headerHtml,
|
|
366
|
+
bodyHtml,
|
|
367
|
+
footerHtml,
|
|
368
|
+
};
|
|
369
|
+
}
|