@goodandready/dsh-messenger-gateway 0.4.0 → 0.4.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/lib/adapters/telegram-inbound.js +141 -141
- package/lib/adapters/telegram.js +596 -596
- package/lib/alerts.js +105 -105
- package/lib/api-health.js +38 -38
- package/lib/client.js +38 -0
- package/lib/commands.js +126 -126
- package/lib/forum-mirror.js +105 -105
- package/lib/gateway-callbacks.js +187 -187
- package/lib/gateway-commands.js +584 -584
- package/lib/gateway-turn.js +285 -285
- package/lib/index.js +540 -540
- package/lib/scheduler.js +169 -169
- package/lib/telegram-errors.js +0 -0
- package/package.json +69 -69
package/lib/alerts.js
CHANGED
|
@@ -1,105 +1,105 @@
|
|
|
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'))
|
|
105
|
-
}
|
|
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'))
|
|
105
|
+
}
|
package/lib/api-health.js
CHANGED
|
@@ -1,38 +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
|
-
}
|
|
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/client.js
CHANGED
|
@@ -12,6 +12,10 @@ window.__ModuleLoader__.load({
|
|
|
12
12
|
|
|
13
13
|
const NS = 'dsh-messenger-gateway'
|
|
14
14
|
const ROUTE_PREFIX = '/dsh-messenger-gateway'
|
|
15
|
+
// Plugins page row seat: key = '<package name>#<row id>', row id as cordis.patch.yml declares it.
|
|
16
|
+
const PKG = '@goodandready/dsh-messenger-gateway'
|
|
17
|
+
const ROW_ID = 'dsh-messenger-gateway'
|
|
18
|
+
const ROW_CONFIG_KEY = PKG + '#' + ROW_ID
|
|
15
19
|
|
|
16
20
|
const en = {
|
|
17
21
|
title: 'Messenger gateway',
|
|
@@ -335,6 +339,7 @@ window.__ModuleLoader__.load({
|
|
|
335
339
|
style.dataset.dshPlugin = NS
|
|
336
340
|
style.textContent = `
|
|
337
341
|
.msgw-page{display:flex;flex-direction:column;gap:16px;color:var(--dsw-alias-label-primary);font-family:inherit}
|
|
342
|
+
.msgw-page-seat{padding:0}
|
|
338
343
|
.msgw-header{display:flex;flex-direction:column;gap:4px;padding-bottom:12px;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
339
344
|
.msgw-page-title{font-size:18px;font-weight:700;display:flex;align-items:center;gap:10px;flex-wrap:wrap;color:var(--dsw-alias-label-primary)}
|
|
340
345
|
.msgw-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.4}
|
|
@@ -1512,6 +1517,23 @@ window.__ModuleLoader__.load({
|
|
|
1512
1517
|
ensureCss()
|
|
1513
1518
|
}, [])
|
|
1514
1519
|
|
|
1520
|
+
// Plugins page row seat ('plugins.row.config'): the host page draws the title,
|
|
1521
|
+
// icon and crumb and provides its own padding, so view 'page' renders the form
|
|
1522
|
+
// bare (no card chrome, which would double the border and clip the left edge)
|
|
1523
|
+
// and view 'summary' renders the one-line state under the row title.
|
|
1524
|
+
if (props && props.view === 'summary') {
|
|
1525
|
+
return React.createElement('div', { className: 'msgw-sub' }, t('description'))
|
|
1526
|
+
}
|
|
1527
|
+
if (props && props.view === 'page') {
|
|
1528
|
+
return React.createElement(
|
|
1529
|
+
'div',
|
|
1530
|
+
{ className: 'msgw-page-seat' },
|
|
1531
|
+
React.createElement(ErrorBoundary, null,
|
|
1532
|
+
React.createElement(SettingsPage, { ...props, ctx: (props && props.ctx) || ctx, t })
|
|
1533
|
+
)
|
|
1534
|
+
)
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1515
1537
|
return React.createElement(
|
|
1516
1538
|
'li',
|
|
1517
1539
|
{ className: 'msgw-card msgw-section-card', style: { listStyle: 'none', marginBottom: '12px' } },
|
|
@@ -1640,6 +1662,22 @@ window.__ModuleLoader__.load({
|
|
|
1640
1662
|
}
|
|
1641
1663
|
}
|
|
1642
1664
|
|
|
1665
|
+
// Row seat on the Plugins page (current core): the plugin's row gains a
|
|
1666
|
+
// configure control whose page is the settings form. Registered first; the
|
|
1667
|
+
// legacy seats below stay as fallbacks so settings never become unreachable.
|
|
1668
|
+
registerSlotWhenReady('plugins.row.config', () => {
|
|
1669
|
+
const slots = ctx?.get?.('slots') || ctx?.slots
|
|
1670
|
+
return slots.register(
|
|
1671
|
+
{
|
|
1672
|
+
name: 'plugins.row.config',
|
|
1673
|
+
key: ROW_CONFIG_KEY,
|
|
1674
|
+
locale: NS,
|
|
1675
|
+
inject: () => ({ ctx }),
|
|
1676
|
+
},
|
|
1677
|
+
(props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
1678
|
+
)
|
|
1679
|
+
})
|
|
1680
|
+
|
|
1643
1681
|
registerSlotWhenReady('settings.plugin.item', () => {
|
|
1644
1682
|
const slots = ctx?.get?.('slots') || ctx?.slots
|
|
1645
1683
|
return slots.register(
|
package/lib/commands.js
CHANGED
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
export const DEFAULT_TELEGRAM_COMMANDS = [
|
|
2
|
-
{ command: 'start', description: 'Connect and show greeting' },
|
|
3
|
-
{ command: 'help', description: 'Show commands help' },
|
|
4
|
-
{ command: 'new', description: 'Start a new agent session' },
|
|
5
|
-
{ command: 'whoami', description: 'Show your messenger user ID' },
|
|
6
|
-
{ command: 'stop', description: 'Interrupt current turn' },
|
|
7
|
-
{ command: 'pair', description: 'Approve pairing code: /pair CODE' },
|
|
8
|
-
{ command: 'sethome', description: 'Set home: /sethome [name]' },
|
|
9
|
-
{ command: 'home', description: 'List registered home channels' },
|
|
10
|
-
{ command: 'model', description: 'Interactive model selector' },
|
|
11
|
-
{ command: 'top', description: 'System resources: CPU, RAM, uptime' },
|
|
12
|
-
{ command: 'topic', description: 'Create forum topic: /topic <name>' },
|
|
13
|
-
{ command: 'status', description: 'Gateway and bot status' },
|
|
14
|
-
{ command: 'setalert', description: 'Designate channel for alerts' },
|
|
15
|
-
{ command: 'alert', description: 'Alert channel status: /alert [test]' },
|
|
16
|
-
{ command: 'keyboard', description: 'Toggle quick actions: /keyboard on|off' },
|
|
17
|
-
{ command: 'role', description: 'Switch persona: /role [name]' },
|
|
18
|
-
{ command: 'bind', description: 'Bind preset/persona to forum topic: /bind <name>' },
|
|
19
|
-
{ command: 'preset', description: 'Show or switch agent preset: /preset [name]' },
|
|
20
|
-
{ command: 'cron', description: 'Manage recurring reports: /cron' },
|
|
21
|
-
{ command: 'skills', description: 'List active tools & skills' },
|
|
22
|
-
{ command: 'tools', description: 'List active tools' },
|
|
23
|
-
{ command: 'fork', description: 'Fork session into new branch' },
|
|
24
|
-
{ command: 'export', description: 'Export history to Markdown' },
|
|
25
|
-
{ command: 'rewind', description: 'Rewind turns: /rewind [N]' },
|
|
26
|
-
{ command: 'files', description: 'Workspace file explorer: /files [dir]' },
|
|
27
|
-
{ command: 'get', description: 'Download file to messenger: /get <path>' },
|
|
28
|
-
{ command: 'remind', description: 'Set reminder: /remind <time> <text>' },
|
|
29
|
-
{ command: 'voice', description: 'Voice reply mode: /voice on|off|status' },
|
|
30
|
-
{ command: 'tts', description: 'Speech in this chat: /tts on|off|status' },
|
|
31
|
-
{ command: 'mute', description: 'Mute notifications in this chat' },
|
|
32
|
-
{ command: 'unmute', description: 'Unmute notifications in this chat' },
|
|
33
|
-
{ command: 'update', description: 'Check or install plugin update: /update [now]' },
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
export function normalizeTelegramCommands(commands) {
|
|
37
|
-
const source = Array.isArray(commands) && commands.length ? commands : DEFAULT_TELEGRAM_COMMANDS
|
|
38
|
-
const out = []
|
|
39
|
-
const seen = new Set()
|
|
40
|
-
for (const raw of source) {
|
|
41
|
-
if (!raw || typeof raw !== 'object') continue
|
|
42
|
-
let command = String(raw.command || '').trim().replace(/^\//, '')
|
|
43
|
-
const description = String(raw.description || '').trim()
|
|
44
|
-
if (!command || !description) continue
|
|
45
|
-
command = command.slice(0, 32).toLowerCase()
|
|
46
|
-
if (seen.has(command)) continue
|
|
47
|
-
seen.add(command)
|
|
48
|
-
out.push({ command, description: description.slice(0, 256) })
|
|
49
|
-
}
|
|
50
|
-
return out.length ? out : [...DEFAULT_TELEGRAM_COMMANDS]
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function mergeDynamicCommands(baseCommands = DEFAULT_TELEGRAM_COMMANDS, dynamicCommands = [], maxTotal = 100) {
|
|
54
|
-
const normalizedBase = normalizeTelegramCommands(baseCommands)
|
|
55
|
-
const out = [...normalizedBase]
|
|
56
|
-
const seen = new Set(out.map((c) => c.command))
|
|
57
|
-
|
|
58
|
-
for (const item of dynamicCommands || []) {
|
|
59
|
-
if (out.length >= maxTotal) break
|
|
60
|
-
if (!item || typeof item !== 'object') continue
|
|
61
|
-
let command = String(item.command || item.name || '').trim().replace(/^\//, '').toLowerCase()
|
|
62
|
-
// Telegram format: lowercase alphanumeric and underscore only, 1-32 chars
|
|
63
|
-
command = command.replace(/[^a-z0-9_]/g, '_').slice(0, 32)
|
|
64
|
-
if (!command || seen.has(command)) continue
|
|
65
|
-
let description = String(item.description || item.title || `Skill ${command}`).trim()
|
|
66
|
-
if (!description) description = `Skill ${command}`
|
|
67
|
-
description = description.slice(0, 256)
|
|
68
|
-
seen.add(command)
|
|
69
|
-
out.push({ command, description })
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return out.slice(0, maxTotal)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function buildQuickActionsKeyboard() {
|
|
76
|
-
return {
|
|
77
|
-
keyboard: [
|
|
78
|
-
[{ text: '🔄 /new' }, { text: '🛑 /stop' }],
|
|
79
|
-
[{ text: '🎙️ /voice' }, { text: '📊 /status' }],
|
|
80
|
-
],
|
|
81
|
-
resize_keyboard: true,
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export const REMOVE_REPLY_KEYBOARD = { remove_keyboard: true }
|
|
86
|
-
|
|
87
|
-
export const HELP_TEXT = [
|
|
88
|
-
'📖 <b>Messenger Gateway Help:</b>',
|
|
89
|
-
'',
|
|
90
|
-
'💬 <b>Session & Chat:</b>',
|
|
91
|
-
'• /help — show this command reference',
|
|
92
|
-
'• /new — start fresh session',
|
|
93
|
-
'• /stop — interrupt current response',
|
|
94
|
-
'• /model — interactive model selector (/model list)',
|
|
95
|
-
'• /role [name] — switch persona or role (/role list)',
|
|
96
|
-
'• /bind [role] — bind persona to topic / chat',
|
|
97
|
-
'• /preset [name] — bind preset to topic / chat',
|
|
98
|
-
'• /lang [en|zh] — switch user language',
|
|
99
|
-
'• /rewind [N] — rewind last N turns',
|
|
100
|
-
'• /fork — fork session into new branch',
|
|
101
|
-
'• /export — export history to Markdown',
|
|
102
|
-
'',
|
|
103
|
-
'🛠️ <b>Tools, Files & Cron:</b>',
|
|
104
|
-
'• /skills / /tools — list active tools & skills',
|
|
105
|
-
'• /files [dir] — workspace file explorer',
|
|
106
|
-
'• /get <path> — download file from workspace',
|
|
107
|
-
'• /remind <time> <text> — set reminder (/remind 10m check deploy)',
|
|
108
|
-
'• /cron <interval> <prompt> — recurring autonomous task',
|
|
109
|
-
'',
|
|
110
|
-
'⚙️ <b>Settings & Stats:</b>',
|
|
111
|
-
'• /status — gateway and active model status',
|
|
112
|
-
'• /top — system resource usage (RAM, uptime)',
|
|
113
|
-
'• /keyboard on|off — quick action keyboard',
|
|
114
|
-
'• /voice on|off|status — voice replies preference',
|
|
115
|
-
'• /tts on|off|status — speech synthesis in this chat',
|
|
116
|
-
'• /mute / /unmute — mute notifications in this chat',
|
|
117
|
-
'',
|
|
118
|
-
'🔒 <b>Access & Channels:</b>',
|
|
119
|
-
'• /whoami — your messenger user ID',
|
|
120
|
-
'• /pair CODE — approve pairing code',
|
|
121
|
-
'• /sethome [name] — set home notification channel',
|
|
122
|
-
'• /setalert — set alert channel',
|
|
123
|
-
].join('\n')
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
1
|
+
export const DEFAULT_TELEGRAM_COMMANDS = [
|
|
2
|
+
{ command: 'start', description: 'Connect and show greeting' },
|
|
3
|
+
{ command: 'help', description: 'Show commands help' },
|
|
4
|
+
{ command: 'new', description: 'Start a new agent session' },
|
|
5
|
+
{ command: 'whoami', description: 'Show your messenger user ID' },
|
|
6
|
+
{ command: 'stop', description: 'Interrupt current turn' },
|
|
7
|
+
{ command: 'pair', description: 'Approve pairing code: /pair CODE' },
|
|
8
|
+
{ command: 'sethome', description: 'Set home: /sethome [name]' },
|
|
9
|
+
{ command: 'home', description: 'List registered home channels' },
|
|
10
|
+
{ command: 'model', description: 'Interactive model selector' },
|
|
11
|
+
{ command: 'top', description: 'System resources: CPU, RAM, uptime' },
|
|
12
|
+
{ command: 'topic', description: 'Create forum topic: /topic <name>' },
|
|
13
|
+
{ command: 'status', description: 'Gateway and bot status' },
|
|
14
|
+
{ command: 'setalert', description: 'Designate channel for alerts' },
|
|
15
|
+
{ command: 'alert', description: 'Alert channel status: /alert [test]' },
|
|
16
|
+
{ command: 'keyboard', description: 'Toggle quick actions: /keyboard on|off' },
|
|
17
|
+
{ command: 'role', description: 'Switch persona: /role [name]' },
|
|
18
|
+
{ command: 'bind', description: 'Bind preset/persona to forum topic: /bind <name>' },
|
|
19
|
+
{ command: 'preset', description: 'Show or switch agent preset: /preset [name]' },
|
|
20
|
+
{ command: 'cron', description: 'Manage recurring reports: /cron' },
|
|
21
|
+
{ command: 'skills', description: 'List active tools & skills' },
|
|
22
|
+
{ command: 'tools', description: 'List active tools' },
|
|
23
|
+
{ command: 'fork', description: 'Fork session into new branch' },
|
|
24
|
+
{ command: 'export', description: 'Export history to Markdown' },
|
|
25
|
+
{ command: 'rewind', description: 'Rewind turns: /rewind [N]' },
|
|
26
|
+
{ command: 'files', description: 'Workspace file explorer: /files [dir]' },
|
|
27
|
+
{ command: 'get', description: 'Download file to messenger: /get <path>' },
|
|
28
|
+
{ command: 'remind', description: 'Set reminder: /remind <time> <text>' },
|
|
29
|
+
{ command: 'voice', description: 'Voice reply mode: /voice on|off|status' },
|
|
30
|
+
{ command: 'tts', description: 'Speech in this chat: /tts on|off|status' },
|
|
31
|
+
{ command: 'mute', description: 'Mute notifications in this chat' },
|
|
32
|
+
{ command: 'unmute', description: 'Unmute notifications in this chat' },
|
|
33
|
+
{ command: 'update', description: 'Check or install plugin update: /update [now]' },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
export function normalizeTelegramCommands(commands) {
|
|
37
|
+
const source = Array.isArray(commands) && commands.length ? commands : DEFAULT_TELEGRAM_COMMANDS
|
|
38
|
+
const out = []
|
|
39
|
+
const seen = new Set()
|
|
40
|
+
for (const raw of source) {
|
|
41
|
+
if (!raw || typeof raw !== 'object') continue
|
|
42
|
+
let command = String(raw.command || '').trim().replace(/^\//, '')
|
|
43
|
+
const description = String(raw.description || '').trim()
|
|
44
|
+
if (!command || !description) continue
|
|
45
|
+
command = command.slice(0, 32).toLowerCase()
|
|
46
|
+
if (seen.has(command)) continue
|
|
47
|
+
seen.add(command)
|
|
48
|
+
out.push({ command, description: description.slice(0, 256) })
|
|
49
|
+
}
|
|
50
|
+
return out.length ? out : [...DEFAULT_TELEGRAM_COMMANDS]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function mergeDynamicCommands(baseCommands = DEFAULT_TELEGRAM_COMMANDS, dynamicCommands = [], maxTotal = 100) {
|
|
54
|
+
const normalizedBase = normalizeTelegramCommands(baseCommands)
|
|
55
|
+
const out = [...normalizedBase]
|
|
56
|
+
const seen = new Set(out.map((c) => c.command))
|
|
57
|
+
|
|
58
|
+
for (const item of dynamicCommands || []) {
|
|
59
|
+
if (out.length >= maxTotal) break
|
|
60
|
+
if (!item || typeof item !== 'object') continue
|
|
61
|
+
let command = String(item.command || item.name || '').trim().replace(/^\//, '').toLowerCase()
|
|
62
|
+
// Telegram format: lowercase alphanumeric and underscore only, 1-32 chars
|
|
63
|
+
command = command.replace(/[^a-z0-9_]/g, '_').slice(0, 32)
|
|
64
|
+
if (!command || seen.has(command)) continue
|
|
65
|
+
let description = String(item.description || item.title || `Skill ${command}`).trim()
|
|
66
|
+
if (!description) description = `Skill ${command}`
|
|
67
|
+
description = description.slice(0, 256)
|
|
68
|
+
seen.add(command)
|
|
69
|
+
out.push({ command, description })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return out.slice(0, maxTotal)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildQuickActionsKeyboard() {
|
|
76
|
+
return {
|
|
77
|
+
keyboard: [
|
|
78
|
+
[{ text: '🔄 /new' }, { text: '🛑 /stop' }],
|
|
79
|
+
[{ text: '🎙️ /voice' }, { text: '📊 /status' }],
|
|
80
|
+
],
|
|
81
|
+
resize_keyboard: true,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const REMOVE_REPLY_KEYBOARD = { remove_keyboard: true }
|
|
86
|
+
|
|
87
|
+
export const HELP_TEXT = [
|
|
88
|
+
'📖 <b>Messenger Gateway Help:</b>',
|
|
89
|
+
'',
|
|
90
|
+
'💬 <b>Session & Chat:</b>',
|
|
91
|
+
'• /help — show this command reference',
|
|
92
|
+
'• /new — start fresh session',
|
|
93
|
+
'• /stop — interrupt current response',
|
|
94
|
+
'• /model — interactive model selector (/model list)',
|
|
95
|
+
'• /role [name] — switch persona or role (/role list)',
|
|
96
|
+
'• /bind [role] — bind persona to topic / chat',
|
|
97
|
+
'• /preset [name] — bind preset to topic / chat',
|
|
98
|
+
'• /lang [en|zh] — switch user language',
|
|
99
|
+
'• /rewind [N] — rewind last N turns',
|
|
100
|
+
'• /fork — fork session into new branch',
|
|
101
|
+
'• /export — export history to Markdown',
|
|
102
|
+
'',
|
|
103
|
+
'🛠️ <b>Tools, Files & Cron:</b>',
|
|
104
|
+
'• /skills / /tools — list active tools & skills',
|
|
105
|
+
'• /files [dir] — workspace file explorer',
|
|
106
|
+
'• /get <path> — download file from workspace',
|
|
107
|
+
'• /remind <time> <text> — set reminder (/remind 10m check deploy)',
|
|
108
|
+
'• /cron <interval> <prompt> — recurring autonomous task',
|
|
109
|
+
'',
|
|
110
|
+
'⚙️ <b>Settings & Stats:</b>',
|
|
111
|
+
'• /status — gateway and active model status',
|
|
112
|
+
'• /top — system resource usage (RAM, uptime)',
|
|
113
|
+
'• /keyboard on|off — quick action keyboard',
|
|
114
|
+
'• /voice on|off|status — voice replies preference',
|
|
115
|
+
'• /tts on|off|status — speech synthesis in this chat',
|
|
116
|
+
'• /mute / /unmute — mute notifications in this chat',
|
|
117
|
+
'',
|
|
118
|
+
'🔒 <b>Access & Channels:</b>',
|
|
119
|
+
'• /whoami — your messenger user ID',
|
|
120
|
+
'• /pair CODE — approve pairing code',
|
|
121
|
+
'• /sethome [name] — set home notification channel',
|
|
122
|
+
'• /setalert — set alert channel',
|
|
123
|
+
].join('\n')
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|