@goodandready/dsh-messenger-gateway 0.3.21 ā 0.4.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/LICENSE +1 -1
- package/cordis.patch.yml +1 -1
- package/lib/adapters/discord.js +9 -1
- package/lib/adapters/slack.js +7 -1
- package/lib/adapters/telegram-inbound.js +141 -0
- package/lib/adapters/telegram.js +42 -154
- package/lib/alerts.js +104 -63
- package/lib/api-health.js +38 -0
- package/lib/artifacts.js +118 -118
- package/lib/ask.js +203 -152
- package/lib/client.js +1660 -1312
- package/lib/commands.js +53 -0
- package/lib/config.js +104 -104
- package/lib/content-guard.js +14 -14
- package/lib/documents.js +1 -1
- package/lib/file-manager.js +27 -1
- package/lib/forum-mirror.js +105 -0
- package/lib/gateway-callbacks.js +187 -0
- package/lib/gateway-commands.js +584 -0
- package/lib/gateway-turn.js +285 -0
- package/lib/gateway.js +590 -1737
- package/lib/http.js +12 -1
- package/lib/index.js +48 -8
- package/lib/integrations.js +1 -1
- package/lib/locales/en.js +123 -114
- package/lib/locales/index.js +21 -21
- package/lib/locales/zh.js +123 -114
- package/lib/media.js +1 -9
- package/lib/models.js +6 -3
- package/lib/outbound.js +1 -1
- package/lib/pairing.js +8 -4
- package/lib/personas.js +8 -4
- package/lib/photos.js +1 -1
- package/lib/scheduler.js +14 -4
- package/lib/stream.js +115 -115
- package/lib/text.js +39 -39
- package/lib/topics.js +0 -14
- package/lib/tts.js +0 -10
- package/lib/updater.js +304 -0
- package/lib/voice-prefs.js +8 -4
- package/package.json +69 -65
package/lib/alerts.js
CHANGED
|
@@ -1,64 +1,105 @@
|
|
|
1
|
-
import { escapeHtml } from './telegram-format.js'
|
|
2
|
-
import { normalizeThreadId } from './topics.js'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
`
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
1
|
+
import { escapeHtml } from './telegram-format.js'
|
|
2
|
+
import { normalizeThreadId } from './topics.js'
|
|
3
|
+
import { t } from './locales/index.js'
|
|
4
|
+
|
|
5
|
+
export function formatAlertMessage(type, payload = {}) {
|
|
6
|
+
const timestamp = new Date().toLocaleTimeString()
|
|
7
|
+
|
|
8
|
+
if (type === 'pairing') {
|
|
9
|
+
const { userId, username, code } = payload
|
|
10
|
+
const userStr = username ? `@${username} (id: <code>${userId}</code>)` : `id: <code>${userId}</code>`
|
|
11
|
+
return [
|
|
12
|
+
`š <b>[Pairing Request]</b> <i>(${timestamp})</i>`,
|
|
13
|
+
'',
|
|
14
|
+
`User: ${userStr}`,
|
|
15
|
+
`Pairing Code: <code>${code}</code>`,
|
|
16
|
+
'',
|
|
17
|
+
`To approve, send to bot:`,
|
|
18
|
+
`<code>/pair ${code}</code>`,
|
|
19
|
+
].join('\n')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (type === 'error') {
|
|
23
|
+
const { message, code, sessionId, chatId, threadId } = payload
|
|
24
|
+
const location = chatId ? `Chat: <code>${chatId}</code>${threadId ? ` / thread <code>${threadId}</code>` : ''}` : ''
|
|
25
|
+
const sess = sessionId ? `Session: <code>${sessionId}</code>` : ''
|
|
26
|
+
const meta = [location, sess].filter(Boolean).join('\n')
|
|
27
|
+
|
|
28
|
+
return [
|
|
29
|
+
`šØ <b>[Gateway Error]</b> <i>(${timestamp})</i>`,
|
|
30
|
+
meta ? `\n${meta}` : '',
|
|
31
|
+
`Error code: <b>${escapeHtml(String(code || 'error'))}</b>`,
|
|
32
|
+
`<code>${escapeHtml(String(message || 'unknown error'))}</code>`,
|
|
33
|
+
].filter(Boolean).join('\n')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (type === 'status') {
|
|
37
|
+
const { title, details } = payload
|
|
38
|
+
return [
|
|
39
|
+
`ā” <b>[Gateway: ${escapeHtml(title || 'Status')}]</b> <i>(${timestamp})</i>`,
|
|
40
|
+
details ? `\n${escapeHtml(details)}` : '',
|
|
41
|
+
].filter(Boolean).join('\n')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return `š <b>[Alert: ${type}]</b> <i>(${timestamp})</i>\n${escapeHtml(JSON.stringify(payload))}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveAlertTarget(gateway) {
|
|
48
|
+
const alertsCfg = gateway?.config?.telegram?.alerts
|
|
49
|
+
if (!alertsCfg || alertsCfg.enabled === false) return null
|
|
50
|
+
|
|
51
|
+
// If a named home is specified
|
|
52
|
+
if (alertsCfg.home) {
|
|
53
|
+
const home = gateway.resolveHomeTarget('telegram', alertsCfg.home)
|
|
54
|
+
if (home) return home
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const chatId = alertsCfg.chatId
|
|
58
|
+
if (!chatId) return null
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
platform: 'telegram',
|
|
62
|
+
chatId,
|
|
63
|
+
threadId: normalizeThreadId(alertsCfg.threadId),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function handleSetAlertCommand(gw, input, locale) {
|
|
68
|
+
const { reply, userId, chatId, threadId = 0 } = input
|
|
69
|
+
if (!gw.isUserAllowed(userId)) return reply(t('msg.not_allowed', {}, locale))
|
|
70
|
+
const nextTg = {
|
|
71
|
+
...gw.tg(),
|
|
72
|
+
alerts: {
|
|
73
|
+
...(gw.tg().alerts || {}),
|
|
74
|
+
enabled: true,
|
|
75
|
+
chatId,
|
|
76
|
+
threadId: threadId || 0,
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
gw.config.telegram = nextTg
|
|
80
|
+
try { await gw.hooks?.persistHomes?.(nextTg) } catch (err) { gw.logger?.warn?.('persistHomes error:', err?.message || err) }
|
|
81
|
+
return reply(`š This chat assigned as alert channel (chat: ${chatId}${threadId ? `, topic: ${threadId}` : ''}).`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function handleAlertCommand(gw, input, parts) {
|
|
85
|
+
const { reply, userId } = input
|
|
86
|
+
const sub = parts[1]?.toLowerCase()
|
|
87
|
+
if (sub === 'test') {
|
|
88
|
+
const target = resolveAlertTarget(gw)
|
|
89
|
+
if (!target) return reply('Alert channel not configured. Configure: /setalert')
|
|
90
|
+
await gw.sendAlert('status', { title: 'Test Alert', details: `Sent by user ID ${userId}` })
|
|
91
|
+
return reply('Test alert sent to alert channel.')
|
|
92
|
+
}
|
|
93
|
+
const target = resolveAlertTarget(gw)
|
|
94
|
+
const alertsCfg = gw.tg().alerts || {}
|
|
95
|
+
return reply([
|
|
96
|
+
'š <b>Alert Channel:</b>',
|
|
97
|
+
`Status: ${alertsCfg.enabled ? 'enabled' : 'disabled'}`,
|
|
98
|
+
`Chat: ${target ? `${target.chatId}${target.threadId ? ` (topic: ${target.threadId})` : ''}` : '(not assigned)'}`,
|
|
99
|
+
`Events: ${(alertsCfg.events || ['error', 'pairing']).join(', ')}`,
|
|
100
|
+
'',
|
|
101
|
+
'Commands:',
|
|
102
|
+
'/setalert ā assign current chat as alert channel',
|
|
103
|
+
'/alert test ā send test alert',
|
|
104
|
+
].join('\n'))
|
|
64
105
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class ApiHealthTracker {
|
|
2
|
+
constructor(options = {}) {
|
|
3
|
+
this.logger = options.logger || console
|
|
4
|
+
this.degradedThreshold = Number(options.degradedThreshold) || 3
|
|
5
|
+
this.warnThreshold = Number(options.warnThreshold) || 5
|
|
6
|
+
this.consecutiveFailures = 0
|
|
7
|
+
this.lastError = null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
recordFailure(op, err) {
|
|
11
|
+
this.consecutiveFailures++
|
|
12
|
+
this.lastError = {
|
|
13
|
+
op,
|
|
14
|
+
message: err?.message || String(err),
|
|
15
|
+
time: Date.now(),
|
|
16
|
+
}
|
|
17
|
+
this.logger?.debug?.(`dsh-messenger-gateway: Telegram API ${op} failed (${this.consecutiveFailures} consecutive):`, err?.message || err)
|
|
18
|
+
if (this.consecutiveFailures >= this.warnThreshold && this.consecutiveFailures % this.warnThreshold === 0) {
|
|
19
|
+
this.logger?.warn?.(`dsh-messenger-gateway: Telegram API experiencing repeated failures (${this.consecutiveFailures} in a row). Last op: ${op}, error: ${err?.message || err}`)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
recordSuccess() {
|
|
24
|
+
this.consecutiveFailures = 0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
isDegraded() {
|
|
28
|
+
return this.consecutiveFailures >= this.degradedThreshold
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getSnapshot() {
|
|
32
|
+
return {
|
|
33
|
+
degraded: this.isDegraded(),
|
|
34
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
35
|
+
lastError: this.lastError,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/lib/artifacts.js
CHANGED
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
export function extractMermaidDiagrams(text) {
|
|
2
|
-
if (!text || typeof text !== 'string') return []
|
|
3
|
-
const regex = /```mermaid\s*\n([\s\S]*?)```/gi
|
|
4
|
-
const diagrams = []
|
|
5
|
-
let match
|
|
6
|
-
while ((match = regex.exec(text)) !== null) {
|
|
7
|
-
diagrams.push({
|
|
8
|
-
fullMatch: match[0],
|
|
9
|
-
code: match[1].trim(),
|
|
10
|
-
index: match.index,
|
|
11
|
-
})
|
|
12
|
-
}
|
|
13
|
-
return diagrams
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function escapeSvgXml(str = '') {
|
|
17
|
-
return String(str)
|
|
18
|
-
.replace(/&/g, '&')
|
|
19
|
-
.replace(/</g, '<')
|
|
20
|
-
.replace(/>/g, '>')
|
|
21
|
-
.replace(/"/g, '"')
|
|
22
|
-
.replace(/'/g, ''')
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function generateDiagramSvg(code, title = 'Mermaid Diagram') {
|
|
26
|
-
const lines = code.split('\n').slice(0, 30)
|
|
27
|
-
const lineSpans = lines.map((line, i) => {
|
|
28
|
-
const escaped = escapeSvgXml(line)
|
|
29
|
-
return `<tspan x="24" dy="${i === 0 ? '0' : '1.4em'}">${escaped}</tspan>`
|
|
30
|
-
}).join('')
|
|
31
|
-
|
|
32
|
-
const height = Math.max(160, Math.min(800, lines.length * 24 + 100))
|
|
33
|
-
const safeTitle = escapeSvgXml(title)
|
|
34
|
-
|
|
35
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
36
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 ${height}" width="800" height="${height}">
|
|
37
|
-
<defs>
|
|
38
|
-
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
39
|
-
<stop offset="0%" stop-color="#1e1e2e"/>
|
|
40
|
-
<stop offset="100%" stop-color="#181825"/>
|
|
41
|
-
</linearGradient>
|
|
42
|
-
</defs>
|
|
43
|
-
<rect width="100%" height="100%" rx="12" fill="url(#bg)" stroke="#313244" stroke-width="2"/>
|
|
44
|
-
<circle cx="28" cy="28" r="6" fill="#f38ba8" />
|
|
45
|
-
<circle cx="48" cy="28" r="6" fill="#f9e2af" />
|
|
46
|
-
<circle cx="68" cy="28" r="6" fill="#a6e3a1" />
|
|
47
|
-
<text x="96" y="32" fill="#cdd6f4" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace" font-size="14" font-weight="600">${safeTitle}</text>
|
|
48
|
-
<line x1="16" y1="48" x2="784" y2="48" stroke="#313244" stroke-width="1"/>
|
|
49
|
-
<text x="24" y="76" fill="#a6adc8" font-family="'JetBrains Mono', 'Fira Code', monospace" font-size="13" xml:space="preserve">
|
|
50
|
-
${lineSpans}
|
|
51
|
-
</text>
|
|
52
|
-
</svg>`
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function formatMarkdownTables(text) {
|
|
56
|
-
if (!text || typeof text !== 'string') return text
|
|
57
|
-
const tableRegex = /((?:^|\n)\|[^\n]+\|\n\|[\s\-:|]+\|\n(?:\|[^\n]+\|\n?)+)/g
|
|
58
|
-
|
|
59
|
-
return text.replace(tableRegex, (match) => {
|
|
60
|
-
const rawLines = match.trim().split('\n')
|
|
61
|
-
if (rawLines.length < 3) return match
|
|
62
|
-
|
|
63
|
-
const rows = rawLines.map((line) => {
|
|
64
|
-
const cells = line.split('|').slice(1, -1).map((c) => c.trim())
|
|
65
|
-
return cells
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
const colCount = Math.max(...rows.map((r) => r.length))
|
|
69
|
-
const colWidths = Array(colCount).fill(0)
|
|
70
|
-
|
|
71
|
-
rows.forEach((row, rowIdx) => {
|
|
72
|
-
if (rowIdx === 1) return
|
|
73
|
-
row.forEach((cell, colIdx) => {
|
|
74
|
-
colWidths[colIdx] = Math.max(colWidths[colIdx] || 0, cell.length)
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
const formattedRows = rows.map((row, rowIdx) => {
|
|
79
|
-
if (rowIdx === 1) {
|
|
80
|
-
return colWidths.map((w) => '-'.repeat(Math.max(w, 3))).join(' | ')
|
|
81
|
-
}
|
|
82
|
-
return row.map((cell, colIdx) => cell.padEnd(colWidths[colIdx] || 3)).join(' | ')
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
const alignedTable = formattedRows.join('\n')
|
|
86
|
-
return `\n\`\`\`\n${alignedTable}\n\`\`\`\n`
|
|
87
|
-
})
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function processDiagramsAndTables(rawText, options = {}) {
|
|
91
|
-
const { artifactPreviews = true } = options
|
|
92
|
-
let text = String(rawText || '')
|
|
93
|
-
const files = []
|
|
94
|
-
|
|
95
|
-
if (artifactPreviews) {
|
|
96
|
-
const diagrams = extractMermaidDiagrams(text)
|
|
97
|
-
if (diagrams.length > 0) {
|
|
98
|
-
let diagramIndex = 1
|
|
99
|
-
for (const diag of diagrams) {
|
|
100
|
-
const svgContent = generateDiagramSvg(diag.code, `Diagram ${diagramIndex}`)
|
|
101
|
-
const base64 = Buffer.from(svgContent, 'utf-8').toString('base64')
|
|
102
|
-
files.push({
|
|
103
|
-
name: `diagram-${diagramIndex}.svg`,
|
|
104
|
-
mime: 'image/svg+xml',
|
|
105
|
-
kind: 'document',
|
|
106
|
-
dataBase64: base64,
|
|
107
|
-
bytes: Buffer.from(svgContent, 'utf-8'),
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
text = text.replace(diag.fullMatch, `\nš <b>[Diagram ${diagramIndex}: see attachment]</b>\n`)
|
|
111
|
-
diagramIndex++
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
text = formatMarkdownTables(text)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return { text, files, diagramsCount: files.length }
|
|
118
|
-
}
|
|
1
|
+
export function extractMermaidDiagrams(text) {
|
|
2
|
+
if (!text || typeof text !== 'string') return []
|
|
3
|
+
const regex = /```mermaid\s*\n([\s\S]*?)```/gi
|
|
4
|
+
const diagrams = []
|
|
5
|
+
let match
|
|
6
|
+
while ((match = regex.exec(text)) !== null) {
|
|
7
|
+
diagrams.push({
|
|
8
|
+
fullMatch: match[0],
|
|
9
|
+
code: match[1].trim(),
|
|
10
|
+
index: match.index,
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
return diagrams
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function escapeSvgXml(str = '') {
|
|
17
|
+
return String(str)
|
|
18
|
+
.replace(/&/g, '&')
|
|
19
|
+
.replace(/</g, '<')
|
|
20
|
+
.replace(/>/g, '>')
|
|
21
|
+
.replace(/"/g, '"')
|
|
22
|
+
.replace(/'/g, ''')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function generateDiagramSvg(code, title = 'Mermaid Diagram') {
|
|
26
|
+
const lines = code.split('\n').slice(0, 30)
|
|
27
|
+
const lineSpans = lines.map((line, i) => {
|
|
28
|
+
const escaped = escapeSvgXml(line)
|
|
29
|
+
return `<tspan x="24" dy="${i === 0 ? '0' : '1.4em'}">${escaped}</tspan>`
|
|
30
|
+
}).join('')
|
|
31
|
+
|
|
32
|
+
const height = Math.max(160, Math.min(800, lines.length * 24 + 100))
|
|
33
|
+
const safeTitle = escapeSvgXml(title)
|
|
34
|
+
|
|
35
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
36
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 ${height}" width="800" height="${height}">
|
|
37
|
+
<defs>
|
|
38
|
+
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
39
|
+
<stop offset="0%" stop-color="#1e1e2e"/>
|
|
40
|
+
<stop offset="100%" stop-color="#181825"/>
|
|
41
|
+
</linearGradient>
|
|
42
|
+
</defs>
|
|
43
|
+
<rect width="100%" height="100%" rx="12" fill="url(#bg)" stroke="#313244" stroke-width="2"/>
|
|
44
|
+
<circle cx="28" cy="28" r="6" fill="#f38ba8" />
|
|
45
|
+
<circle cx="48" cy="28" r="6" fill="#f9e2af" />
|
|
46
|
+
<circle cx="68" cy="28" r="6" fill="#a6e3a1" />
|
|
47
|
+
<text x="96" y="32" fill="#cdd6f4" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace" font-size="14" font-weight="600">${safeTitle}</text>
|
|
48
|
+
<line x1="16" y1="48" x2="784" y2="48" stroke="#313244" stroke-width="1"/>
|
|
49
|
+
<text x="24" y="76" fill="#a6adc8" font-family="'JetBrains Mono', 'Fira Code', monospace" font-size="13" xml:space="preserve">
|
|
50
|
+
${lineSpans}
|
|
51
|
+
</text>
|
|
52
|
+
</svg>`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatMarkdownTables(text) {
|
|
56
|
+
if (!text || typeof text !== 'string') return text
|
|
57
|
+
const tableRegex = /((?:^|\n)\|[^\n]+\|\n\|[\s\-:|]+\|\n(?:\|[^\n]+\|\n?)+)/g
|
|
58
|
+
|
|
59
|
+
return text.replace(tableRegex, (match) => {
|
|
60
|
+
const rawLines = match.trim().split('\n')
|
|
61
|
+
if (rawLines.length < 3) return match
|
|
62
|
+
|
|
63
|
+
const rows = rawLines.map((line) => {
|
|
64
|
+
const cells = line.split('|').slice(1, -1).map((c) => c.trim())
|
|
65
|
+
return cells
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const colCount = Math.max(...rows.map((r) => r.length))
|
|
69
|
+
const colWidths = Array(colCount).fill(0)
|
|
70
|
+
|
|
71
|
+
rows.forEach((row, rowIdx) => {
|
|
72
|
+
if (rowIdx === 1) return
|
|
73
|
+
row.forEach((cell, colIdx) => {
|
|
74
|
+
colWidths[colIdx] = Math.max(colWidths[colIdx] || 0, cell.length)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const formattedRows = rows.map((row, rowIdx) => {
|
|
79
|
+
if (rowIdx === 1) {
|
|
80
|
+
return colWidths.map((w) => '-'.repeat(Math.max(w, 3))).join(' | ')
|
|
81
|
+
}
|
|
82
|
+
return row.map((cell, colIdx) => cell.padEnd(colWidths[colIdx] || 3)).join(' | ')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const alignedTable = formattedRows.join('\n')
|
|
86
|
+
return `\n\`\`\`\n${alignedTable}\n\`\`\`\n`
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function processDiagramsAndTables(rawText, options = {}) {
|
|
91
|
+
const { artifactPreviews = true } = options
|
|
92
|
+
let text = String(rawText || '')
|
|
93
|
+
const files = []
|
|
94
|
+
|
|
95
|
+
if (artifactPreviews) {
|
|
96
|
+
const diagrams = extractMermaidDiagrams(text)
|
|
97
|
+
if (diagrams.length > 0) {
|
|
98
|
+
let diagramIndex = 1
|
|
99
|
+
for (const diag of diagrams) {
|
|
100
|
+
const svgContent = generateDiagramSvg(diag.code, `Diagram ${diagramIndex}`)
|
|
101
|
+
const base64 = Buffer.from(svgContent, 'utf-8').toString('base64')
|
|
102
|
+
files.push({
|
|
103
|
+
name: `diagram-${diagramIndex}.svg`,
|
|
104
|
+
mime: 'image/svg+xml',
|
|
105
|
+
kind: 'document',
|
|
106
|
+
dataBase64: base64,
|
|
107
|
+
bytes: Buffer.from(svgContent, 'utf-8'),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
text = text.replace(diag.fullMatch, `\nš <b>[Diagram ${diagramIndex}: see attachment]</b>\n`)
|
|
111
|
+
diagramIndex++
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
text = formatMarkdownTables(text)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { text, files, diagramsCount: files.length }
|
|
118
|
+
}
|