@goodandready/dsh-messenger-gateway 0.4.4 → 0.4.5

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.
@@ -1,187 +1,188 @@
1
- import { t } from './locales/index.js'
2
- import {
3
- parseCallbackData, parseAskCallback, targetMatchesAsk,
4
- releaseCallbacks, buildMultiSelectKeyboard, indexCallbacks, REMOVE_KEYBOARD
5
- } from './ask.js'
6
- import {
7
- listModelCatalog, buildProvidersKeyboard, buildModelsKeyboard, storeModelSelection
8
- } from './models.js'
9
-
10
- export async function handleGatewayCallback(gw, cb) {
11
- if (cb.userId && !gw.isUserAllowed(cb.userId)) {
12
- try { await cb.answer(t('msg.not_allowed', {}, 'en')) } catch (err) { gw.recordApiFailure('cb.answer.not_allowed', err) }
13
- return
14
- }
15
- const indexed = gw.callbackIndex.get(cb.data)
16
- const { token, buttonId } = parseCallbackData(cb.data)
17
- const askToken = indexed || token
18
- if (askToken && gw.pendingAsks.has(askToken)) {
19
- const pending = gw.pendingAsks.get(askToken)
20
- if (!targetMatchesAsk(pending, cb)) {
21
- await cb.answer(t('ask.other_chat', {}, 'en'))
22
- return
23
- }
24
- const action = parseAskCallback(cb.data)
25
- if (pending.isMulti) {
26
- if (action.kind === 'toggle') {
27
- if (pending.selected.has(action.id)) pending.selected.delete(action.id)
28
- else pending.selected.add(action.id)
29
- releaseCallbacks(gw.callbackIndex, pending.callbackKeys)
30
- const nextKb = buildMultiSelectKeyboard(askToken, pending.options, pending.selected, pending.page, pending.pageSize, pending.payload)
31
- pending.callbackKeys = nextKb.callbackKeys
32
- indexCallbacks(gw.callbackIndex, nextKb.callbackKeys, askToken)
33
- try {
34
- if (cb.editReplyMarkup) await cb.editReplyMarkup(nextKb.replyMarkup)
35
- else await cb.editMessage(cb.message?.text || 'Selection', nextKb.replyMarkup)
36
- } catch (err) {
37
- gw.recordApiFailure('cb.editMessage.ask_multi_toggle', err)
38
- }
39
- await cb.answer(pending.selected.has(action.id) ? 'Selected' : 'Deselected')
40
- return
41
- }
42
- if (action.kind === 'page') {
43
- pending.page = action.page
44
- releaseCallbacks(gw.callbackIndex, pending.callbackKeys)
45
- const nextKb = buildMultiSelectKeyboard(askToken, pending.options, pending.selected, pending.page, pending.pageSize, pending.payload)
46
- pending.callbackKeys = nextKb.callbackKeys
47
- indexCallbacks(gw.callbackIndex, nextKb.callbackKeys, askToken)
48
- try {
49
- if (cb.editReplyMarkup) await cb.editReplyMarkup(nextKb.replyMarkup)
50
- else await cb.editMessage(cb.message?.text || 'Selection', nextKb.replyMarkup)
51
- } catch (err) {
52
- gw.recordApiFailure('cb.editMessage.ask_multi_page', err)
53
- }
54
- await cb.answer()
55
- return
56
- }
57
- if (action.kind === 'done') {
58
- gw.pendingAsks.delete(askToken)
59
- clearTimeout(pending.timer)
60
- releaseCallbacks(gw.callbackIndex, pending.callbackKeys)
61
- await cb.answer('OK')
62
- try { await cb.editMessage(cb.message?.text || 'Done', REMOVE_KEYBOARD) } catch (err) { gw.recordApiFailure('cb.editMessage.ask_multi_done', err) }
63
- pending.resolve({ buttonId: 'done', selected: Array.from(pending.selected), data: cb.data })
64
- return
65
- }
66
- if (action.kind === 'cancel') {
67
- gw.pendingAsks.delete(askToken)
68
- clearTimeout(pending.timer)
69
- releaseCallbacks(gw.callbackIndex, pending.callbackKeys)
70
- await cb.answer('Cancelled')
71
- try { await cb.editMessage(cb.message?.text || 'Cancelled', REMOVE_KEYBOARD) } catch (err) { gw.recordApiFailure('cb.editMessage.ask_multi_cancel', err) }
72
- pending.resolve({ buttonId: 'cancel', selected: [], data: cb.data })
73
- return
74
- }
75
- }
76
- gw.pendingAsks.delete(askToken)
77
- clearTimeout(pending.timer)
78
- releaseCallbacks(gw.callbackIndex, pending.callbackKeys)
79
- await cb.answer('OK')
80
- try { await cb.editMessage(cb.message?.text || 'Done', REMOVE_KEYBOARD) } catch (err) { gw.recordApiFailure('cb.editMessage.ask_single_done', err) }
81
- pending.resolve({ buttonId: action.id || buttonId, data: cb.data })
82
- return
83
- }
84
-
85
- // Model picker interactive flow
86
- if (cb.data?.startsWith('mdl:')) {
87
- const parts = cb.data.split(':')
88
- const sub = parts[1]
89
- // Step 2: Selected provider -> show its models (10 per page)
90
- if (sub === 'p') {
91
- const providerId = parts.slice(2).join(':')
92
- const current = gw.resolveAgentModel()
93
- const catalog = await listModelCatalog(gw.ctx, current)
94
- const models = catalog.modelsByProvider.get(providerId) || []
95
- if (!models.length) {
96
- await cb.answer(t('model.no_models', {}, 'en'))
97
- return
98
- }
99
- const kb = buildModelsKeyboard(providerId, models, current.model, 0)
100
- await cb.answer()
101
- const text = [
102
- `🤖 <b>Provider:</b> <code>${providerId}</code>`,
103
- `Select model (page ${kb.page + 1}/${kb.totalPages}):`,
104
- ].join('\n')
105
- try {
106
- if (cb.editMessage) await cb.editMessage(text, kb)
107
- } catch (err) {
108
- gw.recordApiFailure('cb.editMessage.model_picker_provider', err)
109
- }
110
- return
111
- }
112
- // Pagination for models
113
- if (sub === 'pg') {
114
- const page = parseInt(parts[parts.length - 1], 10) || 0
115
- const providerId = parts.slice(2, -1).join(':')
116
- const current = gw.resolveAgentModel()
117
- const catalog = await listModelCatalog(gw.ctx, current)
118
- const models = catalog.modelsByProvider.get(providerId) || []
119
- const kb = buildModelsKeyboard(providerId, models, current.model, page)
120
- await cb.answer()
121
- const text = [
122
- `🤖 <b>Provider:</b> <code>${providerId}</code>`,
123
- `Select model (page ${kb.page + 1}/${kb.totalPages}):`,
124
- ].join('\n')
125
- try {
126
- if (cb.editMessage) await cb.editMessage(text, kb)
127
- } catch (err) {
128
- gw.recordApiFailure('cb.editMessage.model_picker_page', err)
129
- }
130
- return
131
- }
132
- // Back to providers
133
- if (sub === 'back') {
134
- const current = gw.resolveAgentModel()
135
- const catalog = await listModelCatalog(gw.ctx, current)
136
- const kb = buildProvidersKeyboard(catalog.providers, current)
137
- await cb.answer()
138
- const text = [
139
- '🤖 <b>Choose Provider:</b>',
140
- `Current: <code>${current.provider}/${current.model}</code>`,
141
- ].join('\n')
142
- try {
143
- if (cb.editMessage) await cb.editMessage(text, kb)
144
- } catch (err) {
145
- gw.recordApiFailure('cb.editMessage.model_picker_back', err)
146
- }
147
- return
148
- }
149
- // Select model
150
- if (sub === 's') {
151
- const key = parts[2]
152
- const stored = getStoredModelSelection(key)
153
- if (!stored) {
154
- await cb.answer(t('ask.expired', {}, 'en'))
155
- return
156
- }
157
- const { provider, model } = stored
158
- try {
159
- const adm = gw.ctx.get('agentDefaultModel')
160
- if (adm?.saveSelection) {
161
- await adm.saveSelection({ provider, model })
162
- }
163
- gw.config.agent = { ...gw.config.agent, provider, model }
164
- try {
165
- await gw.hooks?.persistAgentModel?.({ provider, model })
166
- } catch (e) {
167
- gw.ctx.logger?.warn?.(`persist agent model: ${e.message}`)
168
- }
169
- await cb.answer(t('ask.chose', { choice: model }, 'en'))
170
- try {
171
- if (cb.editMessage) await cb.editMessage(t('model.switched', { provider, model }, 'en'), REMOVE_KEYBOARD)
172
- } catch (err) {
173
- gw.recordApiFailure('cb.editMessage.model_picker_switched', err)
174
- }
175
- } catch (err) {
176
- await cb.answer(`Error: ${err.message}`)
177
- }
178
- return
179
- }
180
- if (sub === 'cur') {
181
- await cb.answer()
182
- return
183
- }
184
- }
185
-
186
- await cb.answer()
187
- }
1
+ import { t } from './locales/index.js'
2
+ import {
3
+ parseCallbackData, parseAskCallback, targetMatchesAsk,
4
+ releaseCallbacks, buildMultiSelectKeyboard, indexCallbacks, REMOVE_KEYBOARD
5
+ } from './ask.js'
6
+ import {
7
+ listModelCatalog, buildProvidersKeyboard, buildModelsKeyboard, storeModelSelection,
8
+ getStoredModelSelection
9
+ } from './models.js'
10
+
11
+ export async function handleGatewayCallback(gw, cb) {
12
+ if (cb.userId && !gw.isUserAllowed(cb.userId)) {
13
+ try { await cb.answer(t('msg.not_allowed', {}, 'en')) } catch (err) { gw.recordApiFailure('cb.answer.not_allowed', err) }
14
+ return
15
+ }
16
+ const indexed = gw.callbackIndex.get(cb.data)
17
+ const { token, buttonId } = parseCallbackData(cb.data)
18
+ const askToken = indexed || token
19
+ if (askToken && gw.pendingAsks.has(askToken)) {
20
+ const pending = gw.pendingAsks.get(askToken)
21
+ if (!targetMatchesAsk(pending, cb)) {
22
+ await cb.answer(t('ask.other_chat', {}, 'en'))
23
+ return
24
+ }
25
+ const action = parseAskCallback(cb.data)
26
+ if (pending.isMulti) {
27
+ if (action.kind === 'toggle') {
28
+ if (pending.selected.has(action.id)) pending.selected.delete(action.id)
29
+ else pending.selected.add(action.id)
30
+ releaseCallbacks(gw.callbackIndex, pending.callbackKeys)
31
+ const nextKb = buildMultiSelectKeyboard(askToken, pending.options, pending.selected, pending.page, pending.pageSize, pending.payload)
32
+ pending.callbackKeys = nextKb.callbackKeys
33
+ indexCallbacks(gw.callbackIndex, nextKb.callbackKeys, askToken)
34
+ try {
35
+ if (cb.editReplyMarkup) await cb.editReplyMarkup(nextKb.replyMarkup)
36
+ else await cb.editMessage(cb.message?.text || 'Selection', nextKb.replyMarkup)
37
+ } catch (err) {
38
+ gw.recordApiFailure('cb.editMessage.ask_multi_toggle', err)
39
+ }
40
+ await cb.answer(pending.selected.has(action.id) ? 'Selected' : 'Deselected')
41
+ return
42
+ }
43
+ if (action.kind === 'page') {
44
+ pending.page = action.page
45
+ releaseCallbacks(gw.callbackIndex, pending.callbackKeys)
46
+ const nextKb = buildMultiSelectKeyboard(askToken, pending.options, pending.selected, pending.page, pending.pageSize, pending.payload)
47
+ pending.callbackKeys = nextKb.callbackKeys
48
+ indexCallbacks(gw.callbackIndex, nextKb.callbackKeys, askToken)
49
+ try {
50
+ if (cb.editReplyMarkup) await cb.editReplyMarkup(nextKb.replyMarkup)
51
+ else await cb.editMessage(cb.message?.text || 'Selection', nextKb.replyMarkup)
52
+ } catch (err) {
53
+ gw.recordApiFailure('cb.editMessage.ask_multi_page', err)
54
+ }
55
+ await cb.answer()
56
+ return
57
+ }
58
+ if (action.kind === 'done') {
59
+ gw.pendingAsks.delete(askToken)
60
+ clearTimeout(pending.timer)
61
+ releaseCallbacks(gw.callbackIndex, pending.callbackKeys)
62
+ await cb.answer('OK')
63
+ try { await cb.editMessage(cb.message?.text || 'Done', REMOVE_KEYBOARD) } catch (err) { gw.recordApiFailure('cb.editMessage.ask_multi_done', err) }
64
+ pending.resolve({ buttonId: 'done', selected: Array.from(pending.selected), data: cb.data })
65
+ return
66
+ }
67
+ if (action.kind === 'cancel') {
68
+ gw.pendingAsks.delete(askToken)
69
+ clearTimeout(pending.timer)
70
+ releaseCallbacks(gw.callbackIndex, pending.callbackKeys)
71
+ await cb.answer('Cancelled')
72
+ try { await cb.editMessage(cb.message?.text || 'Cancelled', REMOVE_KEYBOARD) } catch (err) { gw.recordApiFailure('cb.editMessage.ask_multi_cancel', err) }
73
+ pending.resolve({ buttonId: 'cancel', selected: [], data: cb.data })
74
+ return
75
+ }
76
+ }
77
+ gw.pendingAsks.delete(askToken)
78
+ clearTimeout(pending.timer)
79
+ releaseCallbacks(gw.callbackIndex, pending.callbackKeys)
80
+ await cb.answer('OK')
81
+ try { await cb.editMessage(cb.message?.text || 'Done', REMOVE_KEYBOARD) } catch (err) { gw.recordApiFailure('cb.editMessage.ask_single_done', err) }
82
+ pending.resolve({ buttonId: action.id || buttonId, data: cb.data })
83
+ return
84
+ }
85
+
86
+ // Model picker interactive flow
87
+ if (cb.data?.startsWith('mdl:')) {
88
+ const parts = cb.data.split(':')
89
+ const sub = parts[1]
90
+ // Step 2: Selected provider -> show its models (10 per page)
91
+ if (sub === 'p') {
92
+ const providerId = parts.slice(2).join(':')
93
+ const current = gw.resolveAgentModel()
94
+ const catalog = await listModelCatalog(gw.ctx, current)
95
+ const models = catalog.modelsByProvider.get(providerId) || []
96
+ if (!models.length) {
97
+ await cb.answer(t('model.no_models', {}, 'en'))
98
+ return
99
+ }
100
+ const kb = buildModelsKeyboard(providerId, models, current.model, 0)
101
+ await cb.answer()
102
+ const text = [
103
+ `🤖 <b>Provider:</b> <code>${providerId}</code>`,
104
+ `Select model (page ${kb.page + 1}/${kb.totalPages}):`,
105
+ ].join('\n')
106
+ try {
107
+ if (cb.editMessage) await cb.editMessage(text, kb)
108
+ } catch (err) {
109
+ gw.recordApiFailure('cb.editMessage.model_picker_provider', err)
110
+ }
111
+ return
112
+ }
113
+ // Pagination for models
114
+ if (sub === 'pg') {
115
+ const page = parseInt(parts[parts.length - 1], 10) || 0
116
+ const providerId = parts.slice(2, -1).join(':')
117
+ const current = gw.resolveAgentModel()
118
+ const catalog = await listModelCatalog(gw.ctx, current)
119
+ const models = catalog.modelsByProvider.get(providerId) || []
120
+ const kb = buildModelsKeyboard(providerId, models, current.model, page)
121
+ await cb.answer()
122
+ const text = [
123
+ `🤖 <b>Provider:</b> <code>${providerId}</code>`,
124
+ `Select model (page ${kb.page + 1}/${kb.totalPages}):`,
125
+ ].join('\n')
126
+ try {
127
+ if (cb.editMessage) await cb.editMessage(text, kb)
128
+ } catch (err) {
129
+ gw.recordApiFailure('cb.editMessage.model_picker_page', err)
130
+ }
131
+ return
132
+ }
133
+ // Back to providers
134
+ if (sub === 'back') {
135
+ const current = gw.resolveAgentModel()
136
+ const catalog = await listModelCatalog(gw.ctx, current)
137
+ const kb = buildProvidersKeyboard(catalog.providers, current)
138
+ await cb.answer()
139
+ const text = [
140
+ '🤖 <b>Choose Provider:</b>',
141
+ `Current: <code>${current.provider}/${current.model}</code>`,
142
+ ].join('\n')
143
+ try {
144
+ if (cb.editMessage) await cb.editMessage(text, kb)
145
+ } catch (err) {
146
+ gw.recordApiFailure('cb.editMessage.model_picker_back', err)
147
+ }
148
+ return
149
+ }
150
+ // Select model
151
+ if (sub === 's') {
152
+ const key = parts[2]
153
+ const stored = getStoredModelSelection(key)
154
+ if (!stored) {
155
+ await cb.answer(t('ask.expired', {}, 'en'))
156
+ return
157
+ }
158
+ const { provider, model } = stored
159
+ try {
160
+ const adm = gw.ctx.get('agentDefaultModel')
161
+ if (adm?.saveSelection) {
162
+ await adm.saveSelection({ provider, model })
163
+ }
164
+ gw.config.agent = { ...gw.config.agent, provider, model }
165
+ try {
166
+ await gw.hooks?.persistAgentModel?.({ provider, model })
167
+ } catch (e) {
168
+ gw.ctx.logger?.warn?.(`persist agent model: ${e.message}`)
169
+ }
170
+ await cb.answer(t('ask.chose', { choice: model }, 'en'))
171
+ try {
172
+ if (cb.editMessage) await cb.editMessage(t('model.switched', { provider, model }, 'en'), REMOVE_KEYBOARD)
173
+ } catch (err) {
174
+ gw.recordApiFailure('cb.editMessage.model_picker_switched', err)
175
+ }
176
+ } catch (err) {
177
+ await cb.answer(`Error: ${err.message}`)
178
+ }
179
+ return
180
+ }
181
+ if (sub === 'cur') {
182
+ await cb.answer()
183
+ return
184
+ }
185
+ }
186
+
187
+ await cb.answer()
188
+ }